Design 47 and the glossary: a placement change is answered by the nodes it places before it merges (ADR 0559, issue 1126)
mesh/delivery merged: source.main moved to novox/hq@420751b214c0
mesh/delivery-group group feat/1126-l2b merged: every member is merged
mesh/check pass 043f8ecf9cfa on 420751b214c0 | unit 39 run, 0 kept, 1 skipped | integration 39 run, 0 kept, 1 skipped | composition passed

The seed and every later placement pull request were to be checked as equal to what runs,
which no composition outside the node can show; the nodes now answer a placement candidate.
This commit is contained in:
2026-10-27 02:14:09 +01:00
parent 132aae8433
commit 043f8ecf9c
2 changed files with 42 additions and 7 deletions
+7 -1
View File
@@ -237,7 +237,7 @@ every other domain has something to run on. **Recorded by** the controller and t
**Purpose.** To decide, send and apply what each node runs, and to say why.
**Recorded by** the controller's `inventory` database; from ADR 0549, by the catalogue's placement files, which
the indexer reads onto the bus. **Upstream of** Provisioning, Connectivity, Data
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261, 0411, 0539, 0549.
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261, 0411, 0539, 0549, 0559.
**Uses:** node, machine, module, seat, manifest (Module), config value (Config).
- **assignment** — a module put on a node. Its config values on that node (module-assignment config,
@@ -247,6 +247,12 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
and the indexer reads it at each trunk merge into its state `placement`, one key per node, which every reader
of placement watches (ADR 0549 §1 to §3). Until ADR 0547's third milestone the controller's assignments are
kept equal to it.
- **placement candidate** — a placement file of one node as an open catalogue pull request has it at its head,
kept by the indexer in its state `placement-candidates` under `<group>.<node>` while the pull request is open.
Every node composes it in **shadow** (composed and compared, never applied) and answers in its own have whether
what it would run is the same; the composition check passes only when every placed node has answered the same
or differs only where the change intends (ADR 0559). Not the indexer's candidates of a group, which are
fingerprints (Delivery).
- **pending assignment** — an assignment of a module whose build is still running, kept, and made by the
controller when that build registers the module, or ended with why when it does not
(ADR 0261).
@@ -2,8 +2,9 @@
layer: to-be
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
updated: 2026-10-27
decisions:
- "ADR 0559"
- "ADR 0555"
- "ADR 0551"
- "ADR 0550"
@@ -129,7 +130,11 @@ source revision is derived from the keys: every **want** equals its module's **f
group and the modules of the trunk. At each trunk merge of the catalogue it also reads the catalogue's
**placement files**, one per node, into its state `placement`: one key `placement.<node>` with that node's
modules, their pins, host ports and bindings, and the source revision. It adds the nodes to `modules.<module>`
and says `placement-changed` only for a node whose key differs (ADR 0549 §1, §2).
and says `placement-changed` only for a node whose key differs (ADR 0549 §1, §2). While a catalogue pull request
that changes a placement file is open, the forge bridge carries its placement files at the head and the indexer
keeps, at each head, each node's file that differs from the trunk's as a **placement candidate** in a state of its
own, `placement-candidates`, one key `<group>.<node>`, deleting the group's other keys, and all of them when the
pull request merges or closes; nothing that composes what runs reads it (ADR 0559 §2, §3).
- **The builder** claims a build by creating its key, once per fingerprint, and orders it as an ask on the stream of the artifact's
language; it makes no artifact's bytes since R4e and chooses no node, because the build modules pull the asks (ADR 0498 §1, ADR 0525 §1, §4). From R4g it
delivers its own module, the build modules and the controller from its keys, and nothing falls back to build-agent, which is removed
@@ -165,7 +170,15 @@ source revision is derived from the keys: every **want** equals its module's **f
It also runs the **composition check**: whether the machines a change touches still compose with it, read
from the keys the indexer and the builder hold and from the machines, settings and grants it reads, with
mesh-sdk's composition, the one every node agent composes with (ADR 0446, ADR 0547 §2). Its module was made in
R4g with release 5; from release 6 it writes no difference (ADR 0524 §4, ADR 0528, ADR 0539 §8).
R4g with release 5; from release 6 it writes no difference (ADR 0524 §4, ADR 0528, ADR 0539 §8). For a
change that carries placement candidates it composes the change with them laid over the trunk's placement,
counts every node whose placement changes among the machines it touches (ADR 0446 §3, as ADR 0559 §5 changes it), and names each refusal; then it reads each
placed node's answer to the candidate and fails on a module that differs where the change does not intend it (on
a node the trunk does not place yet, as at the seed, no difference is intended), on
a refusal, and on a node that has not answered within 10 minutes, named, so a laptop asleep is never passed;
that node's later answer at the same head asks the check again on its event, as for any part of the mesh not
heard (ADR 0548 §1, ADR 0559 §1, §5). Its own composition stands in for every sealed value, so only a node's answer can say that
what the node would run is the same.
- **The node agent**, the node-engine's own process, follows its node's wants, **composes each wanted module for
its own node** with mesh-sdk's composition from the manifest by digest, the module's resolved values from
mesh-config, its secrets and bus credential asked of the vault sealed to the node's key, its licences and what
@@ -691,7 +704,7 @@ backup holder backs up nothing, and the have says so.
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:
it merges by the nodes it places (ADR 0549 §1, ADR 0555, ADR 0559). 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
@@ -707,6 +720,21 @@ backup holder backs up nothing, and the have says so.
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.
- **A node answers a placement change before it merges.** The node agent watches every placement candidate. For
each open group it composes its own node in shadow, from the trunk's placement with the group's candidates laid
over it and its own inputs, the sealed values and vault boxes it already holds among them, and applies nothing.
It compares the result with what it runs, module by module by composition digest, the bus's user-list resource
left out, and writes the answer into its own have key `<node>.node` under `candidates`, by group: the head
answered, the modules the same, each module that differs with both digests, each module added or removed, and
each refusal. A module the candidate adds is answered as added, naming the values not yet issued for it, since
its credential and boxes are made only once it is placed; a refusal is a value missing for a module the node
runs. It writes only when the answer changes, removes it when the candidates go, and answers
whether or not the node is placed yet, so before the seed it answers against what it runs from the controller's
compositions. The candidate's key is the node's event, the answer is a key the node alone writes, and the
composition check hears that key's change; nothing polls. `indexer_placement`, served by the status view, also
shows each open group's candidates and every node's answer, and the indexer's `indexer_placement_reread` reads
placement and the candidates again now, writing only what differs. The contract (the state, its key, the answer,
the composition with a given placement and the comparison) is mesh-sdk's (ADR 0559 §4, §6, §7, §8).
- **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
@@ -738,8 +766,8 @@ backup holder backs up nothing, and the have says so.
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
controller's export, and its composition check passes only when every node it places has composed it in shadow
and answered that what it would run is the same (ADR 0559 §1); 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
@@ -861,6 +889,7 @@ assignments bucket is kept equal to it until ADR 0547's third milestone and is a
| want | the fingerprint a node should run of a module, and the digest of the one composition it allows; the node's own want names its node composition | the assigner | `mesh-delivery-assigner_wants` |
| have | what the node runs of a module: the fingerprint, the composition's digest, the phase and the health | the node agent | `node-engine_have` |
| placement | each node's modules with their pins, host ports and bindings, from the catalogue's placement file of that node at each trunk merge of the catalogue | the indexer | the indexer's state `placement` |
| placement candidate | while a catalogue pull request that changes placement is open: one node's placement file at its head, beside the chain and never composed into what runs (ADR 0559 §3) | the indexer | the indexer's state `placement-candidates` |
| assignment | until ADR 0547's third milestone: the module and node, pinned or not, ended or not, written by `assign` and `unassign` after a merged placement change, so that it equals the catalogue (ADR 0549 §5) | the controller | `mesh-controller_assignments` |
| rollout | read-only from release 6: each plan of releases 1 to 5, its tiers, the builds it carried and the assignments it changed | nobody from release 6 | `mesh-controller_rollouts` |