Files
hq/01-RESEARCH/006-mesh-from-scratch/code-skeleton.md
T
jschoubben 77f3a4cea7 Consolidate: 65 decision records to 23
Every remaining cluster merged. Each was one design that had been split across
several records because it was worked out over days rather than at once.

  the node host          8 -> 1    applies not decides, depends on nothing,
                                   per operating system, root service, the
                                   launcher, episodic, what a declaration is,
                                   actions from the bundle only
  a node and how it joins 4 -> 1   what a node is, joining, the link as
                                   security boundary, the enrolment token
  modules and the graph   7 -> 1   everything is a module, no domain modules,
                                   three edges, provisioning, the core library
  substrate and control   6 -> 1   the test, seven contexts, one control plane,
    plane                          the authority is not a database, the named
                                   products, the pinned bundle
  connectivity            3 -> 1   a route is a grant, reachability declared,
                                   filter rules
  delivery                5 -> 1   reconciliation not a pipeline, artifacts,
                                   the three silos, a failed step, the verdict
  the lab                 5 -> 1   (earlier)
  how this repository     10 -> 1  (earlier)
    works

Nothing was dropped. Each consolidated record carries the reasoning of the ones
it absorbs -- the measurements, the incidents, the alternatives rejected --
because that reasoning is the only reason to keep a record at all. What is gone
is the fragmentation: eight files to read to understand tier 0, when tier 0 is
one component.

The four superseded records went too. They existed to point at their
successors, and the successors now contain what they said.

The checker made this safe. Each merge left dangling links -- 38 files after
the host merge alone -- and it named every one. Nothing was found by reading,
and a manual pass would certainly have missed some, including references inside
AGENTS.md which every session loads.
2026-08-28 20:03:24 +02:00

273 lines
14 KiB
Markdown

---
effort: 006-mesh-from-scratch
updated: 2026-08-23
---
# The code skeleton
[`skeleton.md`](skeleton.md) laid out tiers and repositories. This is the level below: what a
module looks like on disk, and — the question that forced a correction — where a given piece of
today's catalogue actually lands.
## The tier test
A decision procedure, so placement is answerable rather than argued. Ask in order; first match
wins:
1. **Does it apply state on a machine?** → tier 0, inside the host.
2. **Can the control plane *start* without it?** If *no* → tier 1, substrate. Note the verb:
*start*, not *function fully*. A capability the control plane loses without something is not
the same as a thing it cannot come up without — see "becoming self-hosting" below.
3. **Does it decide what should be true across nodes?** → tier 2, a control-plane context.
4. **Is it a way to talk to tier 2, holding no logic of its own?** → tier 3, a surface.
5. **Otherwise** → tier 4, a workload.
## Worked example — where postgres ends up
The obvious answer is "twice": once as the mesh's own database in the substrate, once as a
hosted database in the catalogue. That answer is wrong, and seeing why fixes something.
Run the test. *Can the control plane exist without a relational store?* No. **Postgres is
tier 1.** It lives once:
```
mesh-substrate/store/postgres/
```
There is no second copy in the catalogue, because the thing that differs between the mesh's own
database and a project's database is **not the module**. It is how that instance is brought up:
| | The mesh's own instance | A project's database |
|---|---|---|
| Brought up by | the host, from the pinned bundle, with no control plane present | the ordinary delivery and provisioning path |
| Declared in | `mesh-substrate/bundle.yml` | the consuming module's `requires:` |
| Exists because | the control plane cannot start without it | something asked for it |
Same module, two roles. **Tier is a property of the module** — what must exist before what — and
**the bundle is a property of the mesh's own instance.**
This is also legal under the dependency rule, which is worth checking rather than assuming: a
tier-4 workload that requires a database depends on tier 1, which points *downward*. The
inverse — substrate reaching into the catalogue for a module — would not be, and is the shape
the naive "twice" answer would have created.
The same reasoning places the rest of the substrate: the bus, the object store, the image
registry. Each fails step 2, each lives once, each is pinned. That is the whole substrate: four
services, and the deliberate absence of a fifth.
**The identity provider was raised as a boundary case and is settled: it is not substrate.**
The mesh does not require one — tier 2 authenticates its own callers natively, and an identity
provider is a service the mesh hosts like any other. The substrate is four services, not five.
The test earned its keep here by turning a vague unease into one answerable question — *does the
control plane delegate authentication?* — rather than a debate about how important identity
feels.
## Becoming self-hosting — the forge and the registries
Self-improvement means the mesh hosts the things it improves itself with: a forge, an image
registry, a package registry. The obvious worry is that these duplicate — a `mesh-gitea`
for the mesh and a `gitea` for everyone else. **They do not, and the reason is worth stating
carefully, because it is the same reason the bootstrap keeps failing today.**
### They are not substrate
Run the test with the sharpened verb. *Can the control plane start without a forge?* **Yes.** It
comes up, holds inventory, answers questions and manages nodes with the modules it already has.
What it cannot do is **change itself**. That is a capability, not a precondition.
So: forge, image registry and package registry are **tier 4 workloads**. One module each, in the
catalogue, exactly like the media server. There is no mesh-specific copy.
This is not a technicality. It buys a property worth having: **if the forge dies, the mesh keeps
running.** Nodes stay managed, services stay up, only self-modification stops. Putting the forge
in the substrate would make losing it fatal, for no gain.
### But delivery needs them — is that not an upward dependency?
It would be, stated naively, and that would break the one rule the whole skeleton rests on.
It is resolved the way the constitution already says to resolve it — **depend on abstractions,
not on concrete dependencies**. Tier 2's delivery context does not require *the forge module*.
It declares requirements:
| Delivery requires | Satisfied by |
|---|---|
| a source of record for module code | whichever module provides it |
| somewhere to publish images | whichever module provides it |
| somewhere to publish packages | whichever module provides it |
| somewhere to put build artifacts | the substrate's object store |
Tier 2 defines the requirement; tier 4 provides the implementation; the binding is data. The
dependency points **downward from the provider to the interface**, which is legal, and the
control plane never names a concrete module.
The mechanism for this already exists and is the mesh's most valuable one: **provisioning**. A
module declares what it provides; a consumer declares what it requires; the mesh binds them.
The only new idea is that **the control plane is itself a consumer** — it has requirements, and
they are satisfied the same way a workload's are.
That generalisation is significant enough to need its own study, and is not settled here.
### Self-hosting is a state the mesh reaches, not a precondition
This is the part today's mesh gets wrong, and it explains a recurring class of pain.
A first node comes up from **pinned external artifacts** — upstream images, by digest, carried
in the bundle. It has to: the mesh's own registry does not exist yet, and cannot. The mesh at
this point is running and manages nodes, and is not yet self-hosting.
Self-hosting is then **reached**: the forge is installed as an ordinary workload, the mesh's own
source moves into it, the registries come up, and delivery's requirements are re-bound from
external providers to internal ones. From that point the mesh builds and deploys itself.
Stated as a lifecycle:
```
pinned external artifacts ─► mesh runs, manages nodes
│
│ forge + registries installed as workloads
│ delivery's requirements re-bound
▼
mesh builds and deploys itself
```
**Today's mesh assumes the second state from the first moment.** Its source, its packages and
its images are all expected to be self-hosted before there is anything to host them — which is
why raising a first node needs a script that exists solely to paper over the impossibility, and
why that script is the least-exercised path in the system.
Making the transition explicit has a second benefit: it is reversible. A mesh whose forge is
broken can re-bind delivery to external providers and keep improving itself while it repairs
the forge. Today that escape hatch does not exist, because the dependency is not expressed
anywhere it could be changed.
### So, concretely
One `gitea` module. One image-registry module. One package-registry module. Each a tier-4
workload. The mesh's own instances are distinguished from any other instance **by what they are
bound to, not by being different modules** — precisely the same answer as postgres, arrived at
by the same test.
## The five fates of a module
Research 005 asked whether a catalogue module should be **grouped into a domain** or **leave
the repository**. Working the tier test across the catalogue surfaces a third answer that
neither option covers, and it is the most common one.
| Fate | Means | Examples from today |
|---|---|---|
| **Absorbed into the host** | It is not a module at all. It is part of what "managing a machine" means, and belongs in tier 0. | overlay membership, packet filtering, package management, service supervision, container runtime, filesystem management |
| **Substrate** | The control plane cannot exist without it. Pinned, host-applied. | relational store, bus, object store, image registry |
| **Control-plane context** | It decides something across nodes. | connectivity policy, inventory, delivery, provisioning, observability |
| **Workload module** | The mesh hosts it. Grouped per [ADR 0044](../../02-DECISIONS/0044-modules-and-the-graph.md). | media library, desktop session, collaboration tooling |
| **Leaves the repository** | A standalone application, per [ADR 0010](../../02-DECISIONS/0010-applications-live-in-their-own-repository.md). | the applications identified in research 005 |
**The first fate is the finding.** Research 005 measured the reachability cluster — proxy,
resolver, firewall, overlay — as the only place in the catalogue where modules genuinely change
together under one intent. The skeleton explains *why*: they are not four modules that ought to
be one domain module. They are four facets of one thing the host should own, currently
expressed as modules because a module was the only unit available.
Under this skeleton the overlay module and the firewall module **stop existing**. The host holds
membership and applies filtering; tier 2 decides the policy; the swappable backends stay
modules. That is a different and better answer than grouping them, and it was not visible from
inside the current frame.
It also partly answers research 005's other open question — the fifty modules that co-change
with nothing. Several are host concerns rather than domains: package management, container
runtime, filesystem tooling. Silence was the right signal after all; the wrong conclusion was
that grouping was the only available fix.
## What a module looks like on disk
Per [`skeleton.md`](skeleton.md) Move 4, a module declares **parts** — independently selectable
pieces of desired state — and produces **artifacts** — things built once per version.
```
<module>/
module.yml identity, what it provides, what it requires,
which host profiles it can land on
parts/
service/ desired state: container, volumes, exposure
provisioner/ how it grants its resource to consumers
migrations/ its own persistent state
tools/ capabilities it contributes
artifacts/
<name>/ source for something built and published
```
A module with no artifacts consumes an upstream image and builds nothing. A module with no
parts is not a module.
Worked through for the substrate's relational store:
```
mesh-substrate/store/postgres/
module.yml provides: database · profiles: [managed]
parts/
service/ the container, its volume, its network exposure
provisioner/ grants a database and role to a consumer
migrations/ none — it holds no state of its own
artifacts/ none — upstream image, pinned by digest in bundle.yml
```
## The tree, at file level
```
mesh-host/ TIER 0
cmd/host/
internal/
apply/ reconcile declared state
inventory/ what this machine is and can do
link/ outbound connection to the control plane
overlay/ membership: address, keys, tunnel
filter/ packet filtering from tier-2 policy
packages/ package management
services/ supervision
containers/ container runtime
store/ embedded local state
profile/ managed · user · edge
substrate.lock pinned tier-1 descriptor
mesh-substrate/ TIER 1
bundle.yml the pinned set, by digest
store/postgres/
bus/<broker>/
objects/<object-store>/
images/<registry>/
mesh-control/ TIER 2
record/ the event log contexts integrate through
inventory/ nodes · modules · assignments · versions
config/ settings · secrets · derivation
connectivity/ addresses · resolution · exposure · filtering · certificates
provisioning/ grants between modules
delivery/ source → artifact → node
observability/ health · logs · metrics
identity/ agents · humans · services · authorisation
work/ tasks · workflows · runs
knowledge/ memory · documents · retrieval
api/ the one interface surfaces speak to
mesh-surfaces/ TIER 3
tools/ web/ cli/
mesh-catalog/ TIER 4
<domain>/<module>/ layout as above
mesh-lab/ mesh-sdk/ hq/ (company-scoped — ADR 0028)
```
## What this does not settle
- **The identity boundary case.** Four substrate services or five.
- **Where the record lives.** Still the open question from
[`00-overview.md`](00-overview.md), and the tier test does not resolve it: the record is
needed by tier 2 and is *of* tier 2, which is exactly the shape that produces a circularity.
- **Whether absorbing into the host makes the host too large.** Six internal concerns is already
a lot for a binary whose whole argument is that it has no dependencies. The counter-argument
is that each is small and none can be optional — but this is the skeleton's biggest unproven
claim, and it should be tested by writing the host's interface before anything else.
- **The migration.** Nothing here says how today becomes this.