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
15 Commits
Author SHA1 Message Date
jschoubben dff0ce2abb 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…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:41:46 +01:00
jschoubben c816666f83 Name the enrolment an address is reserved for, from its ask, and the issued event's id in one place
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
2026-11-02 01:41:36 +01:00
jschoubben 2d717777b1 Carry the machine's tunnel key on its enrolment, so the holder places it on the operator's yes
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
2026-11-02 01:41:36 +01:00
jschoubben f913918a60 Take the tunnel key at issue and place a joining node on the operator's yes, so the hub peers it before redeem
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
2026-11-02 01:41:36 +01:00
jschoubben 38a0bbca44 Let enrolment-token take the first tunnel, so the token the vault makes carries it as today
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
2026-11-02 01:41:36 +01:00
jschoubben 79ad8b539c Name the expired enrolment's issued event by its id, and carry a node's login fall back in its health report
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
2026-11-02 01:41:36 +01:00
jschoubben 90a5fc5797 Let placement take an add-node with its enrolment's warrant, so one Approve lets a machine join
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
2026-11-02 01:41:03 +01:00
jschoubben 362dddae40 Compose mesh-enrolment's grants: its bucket written by it alone, the vault's enrolment verbs called by it alone
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
2026-11-02 01:41:03 +01:00
jschoubben 9169830210 Grant an enrolling node's user the seat's redeem alone, so from the lease write no token reaches the controller
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
2026-11-02 01:41:03 +01:00
jschoubben 8c2ce57eee Compose a node's two logins from the vault's node-logins, so a rotation can switch with both loaded
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
2026-11-02 01:41:03 +01:00
jschoubben 69270a30d5 Add the vault's rotation verbs and the state node-logins, so a node's account rotates over two logins on one contract
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
2026-11-02 01:41:03 +01:00
jschoubben d7a00e1f26 Add the vault's enrolment verbs to its seat, so mesh-enrolment asks the vault to mint and sign and holds nothing
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
2026-11-02 01:41:03 +01:00
jschoubben 2ee75954ad Move the joining token's shape into mesh-sdk, so the vault that makes it, mesh-cli and the engine read one type
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
2026-11-02 01:41:03 +01:00
jschoubben 959f57294a Add the contract of the enrolment state and of redeem, so the holder, the grants part and the engine read one shape
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
2026-11-02 01:41:03 +01:00
jschoubben 59b4c7910b Add the seat mesh-enrolment: six verbs and five events, so the module that joins a machine has a role the judge knows
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
2026-11-02 01:41:03 +01:00