Design 47 says what a node composes from: placement, its config layers, and the node record (issue 1126) #482

Merged
mesh-tools merged 11 commits from feat/1126-nodes-compose into main 2026-10-26 21:11:13 +00:00
@@ -4,6 +4,7 @@ status: in-progress
code: [mesh-controller, mesh-catalog modules/mesh-delivery-gate, mesh-delivery, mesh-host, mesh-sdk, mesh-vault, hq 00-META/checks]
updated: 2026-10-26
decisions:
- "ADR 0555"
- "ADR 0551"
- "ADR 0550"
- "ADR 0549"
@@ -663,7 +664,7 @@ backup holder backs up nothing, and the have says so.
## Compositions, wants and the apply
*ADR 0360 §8, §9, ADR 0377, ADR 0429 §4, §6, ADR 0539 §9, §14 to §16, ADR 0547 §2 to §9.*
*ADR 0360 §8, §9, ADR 0377, ADR 0429 §4, §6, ADR 0539 §9, §14 to §16, ADR 0547 §2 to §9, ADR 0549, ADR 0555.*
- **Each node composes its own.** For each want `want.<node>.<module>` the node agent composes that module for
its own node with mesh-sdk's composition from the manifest by digest the indexer wrote, the module's resolved
@@ -672,6 +673,74 @@ backup holder backs up nothing, and the have says so.
own composition (adoption, firewall openings, accounts, the resolver, identities and groups) from the same
inputs. Of other nodes it reads only their public records and the manifests wanted on them, which its firewall
and resolver need. Nothing composed crosses the bus, so nothing composed is signed (ADR 0547 §2).
- **What a node composes from, and who writes it.** One composer, mesh-sdk's, has three callers: the node agent for
its own node, the assigner's composition check for every node, and the grants part. Each fills the composition's
input from the bus with its own credential and nothing else (ADR 0547 §2, ADR 0549 §3):
- **placement**, the indexer's state `placement`, one key `placement.<node>` read at each catalogue merge from
the node's placement file and said by `placement-changed` (ADR 0549 §2);
- **the node records**, each in its node's own have key `<node>.node`, written by that node alone and heard as
the key's change (ADR 0555 §3);
- **the manifests**, the indexer's `modules.<module>` and its manifests by digest, or the digest the composing
node's want names where it runs another build; the private network's manifest comes with mesh-sdk;
- **the config values**, mesh-config's resolved state, the composing node's slice alone for a node agent; a module
in the settings form (its manifest declares no config, so mesh-config holds none for it) has its values in the
placement file instead;
- **its own secrets and credentials**, from the vault's verb `own`, sealed to the asking node's key and answered to
that node's engine alone, and composed again when the vault says `secret-changed` (ADR 0550 §5).
While both sources stand (ADR 0549 §5), mesh-config resolves values for a module either the placement or the
controller's assignments name on a node, and the grants part composes both, holding a seat held once for the mesh
where the placement says.
- **The placement file.** One file per node in the catalogue, changed only by a pull request and checked before
it merges (ADR 0549 §1, ADR 0555). It holds what a person decides of the node and nothing a machine observes:
- the node's decided facts: its endpoint and site, whether it is the private network's hub, its address on the
private network (a decision written once at enrolment; nothing allocates one at run time), the domain its
routed names are composed under, its operator and agent logins by name with their homes, whether it is
adopted with the modules taken on it, the tunnel found on it at adoption (no longer there to observe once taken
over), and on the hub the names given to that tunnel's peers. No secret, and keys only as public halves;
- its modules in the order the composition reads them, each with its host ports by the port the software
listens on, its bindings by provision, and its config placement (`places`, `accesses`, `networks`,
`endpoints`, `expose`, `reach`, `ports`) as two layers: the mesh-wide one, repeated in every node's file so a
node reads its own key alone, and the node's own, with a mark when the node has a layer that holds no placement,
since an empty layer still carries the module's config values; and, for a module in the settings form only,
the config values a person set in each of those layers (on the day of the seed the media modules of one node);
- its pins by provision, and the seats its modules hold.
The catalogue's check refuses a placement naming a module it does not hold or a node not enrolled, two modules
of one node on one host port, a pin, binding or holding naming a provider not placed where it says, a seat held
once for the mesh held twice, two nodes with one address and more than one hub, each named in one answer.
- **The node record.** What the machine observes of itself: its tunnel's public key, its capabilities, the
public halves of its sealing and serving keys, the certificate issued for its internal name, its outward links
and networks, and its node-engine's contract. Written at start and when an observed fact changes, only when it
differs (ADR 0555 §3). Whether the hub took its found tunnel over, and which node enrolled under a carried
peer's key, are read from the placement and the records together, never written.
- **What a node's account reads to compose.** The grants part's composer gives every node's account these reads,
each read only, and the controller's frozen copy none of them (ADR 0555):
- every node's `<node>.node` of the node-engine's have bucket: the other nodes' records;
- every key of the indexer's `placement`: where modules run and what a person decided of each node;
- every key of the indexer's `modules` and `manifests`: the manifests it composes from;
- every key of the builder's `builds`: the manifest of a build its want names when that is not the trunk's;
- mesh-config's `resolved` values of its own node alone, `<node>.>`.
Since the take-over the grants part's list is the one the bus loads; after a `hand-back` the controller's list
holds, without these reads, and nodes keep applying while composing their own fails loudly until the next
`take-over`. mesh-sdk's composer test holds the copy, user for user, to every grant of the controller's golden
plus exactly these reads and the bucket-creation advisories of issue 1790.
- **A node composes its own once the catalogue places it.** Its agent reads its key `placement.<node>` at start:
with one it composes its own, without one it applies the controller's compositions as before, so nothing changes
on a node before the seeded placement file reaches it; it follows the key, and when it appears the agent begins
again composing its own, with no restart. A node never applies a stand-in: a composition that stands
in for a value its input lacks (an own secret, a credential, a vault box, the certificate) is refused whole, said,
and the node keeps running what it runs; the bus's user list alone is held out of the apply, since the bus node
follows it apart. A secret or credential the vault does not hold is such a gap: the vault names it missing, and
the node composing it keeps what runs until the vault holds it. Likewise a consumer of `model-access` answered by a
licence record, placed before the vault serves licences (ADR 0550 §3, issue 569), is refused by the same guard and
its node keeps what runs; none is placed today.
- **The order a node takes to composing its own.** One delivery group carries every part: the controller's verb
`placement-export`, the node-engine (its record and its own composer), the vault's `own`, the grants part's reads,
mesh-config's and the assigner's reading of placement, and the indexer writing it. Nothing changes on a node by
that delivery alone, since no placement file exists yet: the node records appear, and the grants part's list gives
the nodes their reads. Then the seed: one catalogue pull request writes each node's placement file from the
controller's export, and its composition check composes every node from it with no difference to what the node
runs; only then is it merged. The indexer writes `placement`, each node begins composing its own, and the assigner
writes its wants as the build alone.
- **Composed again on events, never on a timer.** A changed input at the same fingerprint (a setting or given
value, a secret rotated or moved, a credential issued, a provider's contribution, a peer added to the hub) is
composed again when its event is heard, and applied as a revision of what runs (ADR 0547 §2).
@@ -697,7 +766,10 @@ backup holder backs up nothing, and the have says so.
and nothing is asserted (ADR 0547 §5).
- **The want names the build.** A want `want.<node>.<module>` names the fingerprint, the artifact's digest, the
source revision and whether the module holds data; the node's own want `want.<node>.node` names the node
(ADR 0547 §2).
(ADR 0547 §2). A want to a node the catalogue places says so: it carries `self` and names no composition,
since the node composes it; every other want still names the one composition it allows, so nothing written
before (the controller's bus upgrade among it) reads a want differently. A person's yes to a move the node
judged is written into the want as `yes`, the digest of the composition allowed (ADR 0547 §3).
- **The apply, one module at a time.** At its first start the node agent takes the declaration it kept last as
what each module runs and writes each have. Then it applies a module only when its want's fingerprint
differs from what runs or the composition it composed differs from the one it runs, and only that module,