The enrolment seat and node-login rotation contract: a joining machine's first tunnel, add-node and remove-node, two logins per node (issue 774) #157

Merged
mesh-tools merged 15 commits from feat/l3-enrol into main 2026-11-02 07:56:24 +00:00
Member

Issue 774 (design 47): nothing is left with the controller — enrolment and rotation. ADRs 0587, 0589, 0583, 0588.

  • The seat mesh-enrolment (six verbs, five events); the enrolment, address, token, redeem and expiry shapes in go/delivery; the token (moved from the engine) carries a five-field first tunnel.
  • The vault's seat gains enrolment-token (with tunnel), node-account, rotate-node, node-account-confirm and abandon; NodeLogin (with Acts) for the state node-logins.
  • The composer: one bus user per login of a node; enrol.<node> may call only mesh-enrolment.redeem; mesh-enrolment alone asks the vault to enrol.
  • Placement: add-node (the tunnel key at the operator's yes, identity keys at redeem) and remove-node, mesh-enrolment's alone on the enrolment's warrant.
  • NodeHealth.Fallback for the watchman's node-account-fallback.

Delivery group: the four pull requests on branch feat/l3-enrol (mesh-sdk, mesh-catalog, mesh-delivery, mesh-host), one check, one Approve, merged in one pass in the order mesh-sdk → mesh-catalog → mesh-delivery → mesh-host. Rebased on main after the removal group merged (2026-11-02 01:35); every member pins mesh-sdk at dff0ce2abb (this group's head). Issuing and rotation stay off until the lease write (ADR 0561 §3, ADR 0587 §4).

Tested locally before the push (GOWORK=off, GOFLAGS=-mod=readonly, GOPROXY=off, GOSUMDB=off): go vet, go vet -tags integration and the untagged unit suite of every touched module pass. Integration tests that exercise the change, against an embedded bus: ./go/process: ok.

Reviewed-by: review-g2@shanks at dff0ce2abb
Pushed-by: lead-47@shanks

https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL

Issue 774 (design 47): nothing is left with the controller — enrolment and rotation. ADRs 0587, 0589, 0583, 0588. - The seat `mesh-enrolment` (six verbs, five events); the enrolment, address, token, redeem and expiry shapes in go/delivery; the token (moved from the engine) carries a five-field first tunnel. - The vault's seat gains enrolment-token (with `tunnel`), node-account, rotate-node, node-account-confirm and abandon; `NodeLogin` (with `Acts`) for the state `node-logins`. - The composer: one bus user per login of a node; `enrol.<node>` may call only `mesh-enrolment.redeem`; mesh-enrolment alone asks the vault to enrol. - Placement: `add-node` (the tunnel key at the operator's yes, identity keys at redeem) and `remove-node`, mesh-enrolment's alone on the enrolment's warrant. - `NodeHealth.Fallback` for the watchman's node-account-fallback. **Delivery group:** the four pull requests on branch `feat/l3-enrol` (mesh-sdk, mesh-catalog, mesh-delivery, mesh-host), one check, one Approve, merged in one pass in the order mesh-sdk → mesh-catalog → mesh-delivery → mesh-host. Rebased on main after the removal group merged (2026-11-02 01:35); every member pins mesh-sdk at dff0ce2abb21 (this group's head). Issuing and rotation stay off until the lease write (ADR 0561 §3, ADR 0587 §4). **Tested locally before the push** (GOWORK=off, GOFLAGS=-mod=readonly, GOPROXY=off, GOSUMDB=off): `go vet`, `go vet -tags integration` and the untagged unit suite of every touched module pass. Integration tests that exercise the change, against an embedded bus: `./go/process`: ok. Reviewed-by: review-g2@shanks at dff0ce2abb2100da90d0a5dad719c3e45d40079e Pushed-by: lead-47@shanks https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
mesh-tools added 15 commits 2026-11-02 00:52:38 +00:00
ADR 0587 §1 makes joining a machine a seat held once by the module mesh-enrolment, the vault's only caller for its
enrolment verbs. The seat is in the mesh's own set, its verbs not optional (the seat is new with its holder), and
its events the five that carry no secret. The test is a block in TestHoldingAMeshSeatRequiresServingItsVerbs, no new
test function.

(issue 774, 774/S34)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0587 §2 gives the seat mesh-enrolment the bucket enrolments with keys node.<node> (an Enrolment: node, address,
why, caller, ask, stage, issued, expires, keys digest) and address.<address> (the node's name alone), no secret in
either. The token's life rules (an hour by default, at most a day, §4), the event enrolment-expired's content (§2),
and redeem's call, proof bytes, keys digest and signed answer (§6) live here too, because the holder in
mesh-delivery and the engine in mesh-host both read them. The proof binds the node and its keys, not the token's
secret, which the holder never holds (§1). Tested as rows of TestNodeRecord, no new test function.

(issue 774, 774/S35)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
From the lease write the vault makes the token (ADR 0587 §3), mesh-cli opens and prints it, and the node-engine
parses it: three repositories, so the type leaves mesh-host's internal/identity (which keeps an alias) for
go/delivery, field names unchanged from the controller's token issue, the tunnel kept (the lead's settlement 7).
Encode is added, refusing a token with a part missing. The test reads a token the controller's own code made and
encodes it back to the same line. Paid by merging TestGateBuckets into TestKeysOfPlainNamesAreUnchanged.

(issue 774, 774/S36)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0587 §3 and §5 (changing ADR 0561 §5) have the vault make the token and its hash (enrolment-token), answer it
sealed to the operator key for tokens to relay (sealed, §8), drop both at every end (drop), and mint, answer again
and retire a joining node's account (node-account), with mesh-enrolment their only caller; sign names mesh-enrolment
for a joining node's certificate (with the serving key it presented, since the node has no record yet) and for a
redeem answer. Both verbs optional until the vault that runs serves them. Their answers' shapes are in go/delivery.
Tested as rows of TestTheVaultsSeatPromisesItsIntakeVerbs, no new test function.

(issue 774, 774/S37)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0583 §1 (insight 2026-11-01) keeps which login a node uses in the vault's state node-logins, one key per node:
the login in use, the one rotating and the stage (begun, ready, confirmed), with the list revision that held both.
§2, §3 and §5 give the vault's seat rotate-node (with the recorded hand act's id), node-account-confirm (the caller
is the proof) and abandon, optional until the vault serves them; the events rotation-begun, node-account-ready,
rotation-done and the engine's finding node-account-fallback are named beside them. The codec refuses a stage without
a rotation and a rotation to anything but the node's other login; an empty rotating reads as none. Tested as rows of
TestNodeRecord and TestTheVaultsSeatPromisesItsIntakeVerbs, no new test function.

(issue 774, 774/S38)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0583 §1: a node's account rotates between node.<n> and node.<n>--b, each with the node's permissions and its
inbox _INBOX.node.<n>. Records gains NodeLogins (each node's key of node-logins; the context file's NodeRotating,
renamed because a node that rotated and rotates no more logs in as its second login alone), and Users composes one
node user per login: the one in use and, while it rotates, the other. Each login calls in its own name, which is how
the vault tells the login that confirms (§3). While the vault serves rotate-node, each node reads its own node's key
of node-logins alone and may call node-account-confirm in its own name (§1 insight); the grants part is the state's
one named reader. With no rotation every list is the controller's golden one, unchanged. Tested as a subtest of
TestTheCopiedComposerComposesAsTheControllers, no new test function.

(issue 774, 774/S39)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0587 §4: the grants part composes enrol.<node> granted only to call mesh-enrolment.redeem and to hear its own
inbox; §6: from the lease write the controller's enrolment subject is not served. The enrolment principal now calls
redeem in its own name while the seat's holder serves it, and keeps mesh.control.enrol only until Records says the
lease is handed away (a field the grants part sets and the controller's composer never does). A user left able to
enrol nowhere is refused by name rather than composed empty. Golden lists unchanged. Tested as a subtest of
TestTheCopiedComposerComposesAsTheControllers, no new test function.

(issue 774, 774/S40)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0587 §1-§3 (changing ADR 0561 §5) make mesh-enrolment the only caller of the vault's enrolment-token and
node-account and a caller of sign, and §2 its bucket enrolments read by it and by the grants part as a named reader.
The holder calls the vault through its manifest's invokes, in its own name, as any caller does; while the vault serves
either enrolment verb every other principal is denied both, however wide its grant (a runtime's or an agent's `*`),
as ControllerOnly denies the desk's prompt. The writers table gains the open enrolments (mesh-enrolment) and the
node-logins (the vault), each one writer; NamedReaders names the grants part for enrolments. Golden lists unchanged.
Tested as a subtest of TestTheCopiedComposerComposesAsTheControllers, no new test function.

(issue 774, 774/S41)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0587 §7: at redeem mesh-enrolment proposes one change, the node with its decided fields (the approved address,
never the hub) and the public halves of its keys, carrying the issue's warrant; mesh-placement checks the warrant
names this node and address and that the caller is mesh-enrolment, and writes the key with no ask of its own
(changing ADR 0567 §2 and §5 for this one change by this one caller). The proposal codec refuses an add-node beside
other changes, without a warrant, or whose warrant names another node or address, and a warrant on any other
proposal; AddNodeRefusal adds the caller's check for the assigner; LayChanges adds the node and refuses one placed
already. Tested as rows of TestPlacement's proposal subtest, no new test function.

(issue 774, 774/S42)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
Blocker 6: enrolment-expired carries the enrolment-issued event's message id (IssuedID), since a bundle's publish
answers no stream sequence and ADR 0587 §2 asks only that the event be named; it gains a codec refusing no event named.
Blocker 7: NodeHealth gains Fallback (node, the login fallen back from, why, at), from which the watchman raises
node-account-fallback (ADR 0583 §6); a fall back naming another node is refused. A token missing its signer now says
the vault's signing key, not the retired word. Tested as rows of TestNodeRecord and the control records test; no new
test function (1438 unit tests).

(issue 774, review round 1)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
Lead's decision on milestone point 7 (a progressive insight of ADR 0587 §3): mesh-enrolment builds the joining
node's first tunnel at issue and passes it to the vault's enrolment-token as `tunnel`, an object of hub_key,
hub_endpoint and address, each required; the vault puts it in the token (delivery.TokenTunnel). Tested as a row of
TestTheVaultsSeatPromisesItsIntakeVerbs; no new test function (1438 unit tests).

(issue 774, review round 1)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
The operator's decision of 2026-11-02 01:05: issue requires overlay_key, the machine's tunnel public key; the vault's
enrolment-token tunnel declares all five fields a token needs (hub_key, hub_endpoint, address, key, range). An
add-node is proposed at the yes with the tunnel key alone; the identity keys come whole at redeem or not at all. A new
remove-node, mesh-enrolment's alone on the same warrant and the proposal's one change, takes a joining node running
nothing off placement when its enrolment ends unconfirmed (RemovedNodes names the key the holder deletes; LayChanges
lays nothing for it and refuses a node that runs modules or holds seats). NodeLogin gains Acts, the hand acts that
warranted its rotations. Tested as rows of TestPlacement, TestNodeRecord and the two seat tests; no new test function
(1438 unit tests).

(issue 774, review round 1b)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
The operator's decision of 2026-11-02 01:05: the tunnel key is given at issue. Enrolment gains overlay-key, required at
every stage and refused unless it is 32 bytes in base64. Tested as rows of TestNodeRecord; no new test function (1438
unit tests).

(issue 774, review round 1b)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
ADR 0589 §6: an address key gains the issue's ask and time, written when the ask is raised. The key is written at the
reservation, before the ask, so both are empty then and both set from the ask; one without the other is refused, as
is no node. EncodeEnrolmentAddress takes node, ask and issued. EnrolmentIssuedID builds the enrolment-issued event's
id, enrolment-issued.<node>.<issue time in Unix nanoseconds>, as the holder names it today. Tested as rows of
TestNodeRecord; no new test function (1438 unit tests).

(issue 774, review round 1b)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
Say why LeaseHandedAway and LeaseCut are two fields: the cut precedes the holder's write and outlasts it at a hand-back (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-sdk@4a4fac090e9e
mesh/delivery-group group feat/l3-enrol merged: every member is merged
mesh/check failed (2 tests): integration: mesh-mail-reader/cmd/mesh-mail-reader.TestAcceptanceOneMatteringMailSendsExactlyOneTellNamingIt: timed out w…
dff0ce2abb
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
Author
Member

Delivery novox/mesh-sdk@dff0ce2abb21 — merged since 2026-11-02T07:56:26Z

Source — source.main moved to novox/mesh-sdk@4a4fac090e. Whether it reached the machines is mesh-delivery-view's, derived from source.main, the wants and the haves.

Group feat/l3-enrol: novox/mesh-catalog@c84dfb5e0b, novox/mesh-delivery@63c8687166, novox/mesh-host@44db32c31b, novox/mesh-sdk@dff0ce2abb

  • composed together: fail — the checker's verdict at the members' heads (3820753740cd): integration: mesh-mail-reader/cmd/mesh-mail-reader.TestAcceptanceOneMatteringMailSendsExactlyOneTellNamingIt

Transitions

  • 2026-11-02 00:52 (new) → proposed (announced): novox/mesh-sdk#157's head announced
  • 2026-11-02 00:52 proposed → ready (accepted): a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and the repository's own check did not fail; or the check gates no merge (check-gates-merge false)
  • 2026-11-02 07:56 ready → merged (merged): on the trunk its modules follow: merged there

The commit's note under refs/notes/mesh-plan keeps every transition of this change up to its merge: git log --notes=mesh-plan.

<!-- mesh-delivery:view --> **Delivery** `novox/mesh-sdk@dff0ce2abb21` — **merged** since 2026-11-02T07:56:26Z **Source** — source.main moved to novox/mesh-sdk@4a4fac090e9e. Whether it reached the machines is `mesh-delivery-view`'s, derived from `source.main`, the wants and the haves. **Group** `feat/l3-enrol`: novox/mesh-catalog@c84dfb5e0bcd, novox/mesh-delivery@63c8687166e6, novox/mesh-host@44db32c31bbe, novox/mesh-sdk@dff0ce2abb21 - composed together: fail — the checker's verdict at the members' heads (3820753740cd): integration: mesh-mail-reader/cmd/mesh-mail-reader.TestAcceptanceOneMatteringMailSendsExactlyOneTellNamingIt **Transitions** - 2026-11-02 00:52 (new) → proposed (announced): novox/mesh-sdk#157's head announced - 2026-11-02 00:52 proposed → ready (accepted): a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and the repository's own check did not fail; or the check gates no merge (check-gates-merge false) - 2026-11-02 07:56 ready → merged (merged): on the trunk its modules follow: merged there The commit's note under `refs/notes/mesh-plan` keeps every transition of this change up to its merge: `git log --notes=mesh-plan`.
mesh-tools merged commit 4a4fac090e into main 2026-11-02 07:56:24 +00:00
mesh-tools deleted branch feat/l3-enrol 2026-11-02 07:56:25 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-sdk#157