Issue 356: the subject proves nothing, the signature does; the apply refuses a link at use
The review of 2026-10-09 proved a responder can reach the hand-over's subject through a reply, and that the apply followed a link the hand-over had refused. The record no longer says only the controller may publish there: it says the ask is signed with the mesh's key and verified by the engine, and that a handed-over directory is changed through a descriptor that followed no link. To-be 45's writers table gains the row for a hand-over asked of a machine.
This commit is contained in:
@@ -72,6 +72,7 @@ Every kind of state the core keeps, and the one component that writes it. Anyone
|
||||
| State | Writer | Kept in | Others |
|
||||
|---|---|---|---|
|
||||
| a machine's declaration | controller (lease holder) | the bus, last per subject | read |
|
||||
| a hand-over asked of a machine ([issue 356](../../04-ISSUES/356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel/00-report.md)) | controller, at its terminal | the machine, beside its state | the engine verifies the mesh's signature and records it, or refuses |
|
||||
| a machine's applied state and its report | the node-engine's apply queue (§6) | the machine; the report on the bus | the reconcile and a delivery *enqueue*, never apply |
|
||||
| the controller lease | the controller instance holding it | key-value `mesh-controller_lease` | a candidate waits |
|
||||
| plans and their tiers | controller (lease holder), compare-and-set on the plan's revision | the controller's store | read through `plans` |
|
||||
@@ -96,7 +97,8 @@ this table marks it shared. Changing a writer is a change to this table, through
|
||||
|
||||
**How it is enforced** ([ADR 0229](../../02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)):
|
||||
the table is compiled into the controller, each row the bus carries with the subjects a write of it
|
||||
publishes to — a declaration `mesh.node.*.declare`, a report `mesh.control.*.report` (its own machine's
|
||||
publishes to — a declaration `mesh.node.*.declare`, a hand-over asked `mesh.node.*.ask.hand-over` (signed as
|
||||
well, because a responder can reach a subject through a reply, which no grant refuses), a report `mesh.control.*.report` (its own machine's
|
||||
only), the controller's buckets, stream creation, update and deletion and durable consumer creation, a
|
||||
build's outcome, a merge announced, a provider's standing — and its writer among the principals. A
|
||||
grant that overlaps another writer's subject is refused at composition, naming the state and its
|
||||
|
||||
+18
-9
@@ -87,15 +87,24 @@ So two faults, one visible through the other:
|
||||
terminal-only rule refuses it through every verb and through `mesh-cli` outside the terminal — tested
|
||||
through `terminalOnly`, the generic command verb's composition and the ordinary mesh-cli line. The line's
|
||||
node and path are judged before anything is asked (absolute, clean, no `..`, no doubled or trailing
|
||||
separator; a node the mesh knows). The ask is a request on the machine's own subject, which only the
|
||||
controller may publish and only that machine's engine may hear and answer (the node's grant gains the subject
|
||||
and the right to answer what it was asked, and the golden composition shows exactly that); a test asks over a
|
||||
bus and reads the engine's record and its refusal back, says "not answering" for a node with no engine
|
||||
listening, and refuses an answer that is neither. The engine judges every value it is sent before it records
|
||||
anything — the ask's size, its shape, who asked (one short line), the path (absolute, clean, one its state
|
||||
records as used as found, on disk a directory and not a link, reached through no link) — and a refusal
|
||||
records nothing; nine refusals and the one acceptance are tested, and the ask's judgement separately with a
|
||||
handler that counts what it was given. The request's and the answer's field names are held by a test on each
|
||||
separator; a node the mesh knows). The ask is a request on the machine's own subject. **The subject
|
||||
proves nothing about who asked**: only the controller is granted a publish there (a row of the writers table,
|
||||
held at composition), but the bus lets any principal allowed to answer reply to a message it received on the
|
||||
reply subject that message named, so a tool server running as the operator's account could deliver a body there
|
||||
(found by the review of 2026-10-09). So the controller **signs the ask** with the mesh's key — the node, the
|
||||
path, who asked, an expiry a minute ahead and a fresh nonce, over a fixed context so it is never a declaration's
|
||||
signature — and the engine verifies it with the key it verifies declarations with before it reads anything out
|
||||
of it, then refuses an ask for another node, expired, good for too long, without a nonce of its own, or taken
|
||||
before. Tests on the engine's side refuse each of those (unsigned, another key, signed as a declaration, the
|
||||
bare ask, another node, expired, no expiry, too long, no or a short nonce, a replay) with the handler given
|
||||
nothing; the controller's side holds the signature, the context, the nonce and the expiry. The node's grant gains
|
||||
the subject and the right to answer what it was asked (the golden composition shows exactly that). The engine
|
||||
then judges every value before it records anything — who asked (one short line), the path (absolute, clean,
|
||||
one its state records as used as found, on disk a directory and not a link, reached through no link) — and a
|
||||
refusal records nothing. **At the apply, the link is refused again at the moment of use**: a handed-over
|
||||
directory must still resolve to itself, and is changed only through one descriptor opened from `/` with no
|
||||
link followed on any component; a test swaps its parent, its grandparent and itself for links to a victim
|
||||
tree after the hand-over, and the victim keeps its mode. The request's and the answer's field names are held by a test on each
|
||||
side.
|
||||
- **The words**: the engine's statement, the controller's wait and condition name
|
||||
`nox node hand-over <node> <directory>` on the control-node, and never the engine's binary; the condition's
|
||||
|
||||
+11
-3
@@ -49,9 +49,17 @@ and the command on the machine is the engine that runs it.
|
||||
**The hand-over's channel.** `nox node hand-over <node> <path>` on the control-node. The controller's `node`
|
||||
subcommands that are not reads are terminal-only already (ADR 0266's allow list), so the line is refused through
|
||||
every verb and through `mesh-cli` from any other node without a new rule. The controller judges the line, asks
|
||||
the node's engine on `mesh.node.<node>.ask.hand-over` — a request on core NATS, which only the controller may
|
||||
publish and only that node's engine may hear and answer — and prints the engine's answer. The engine judges every
|
||||
value and records the hand-over, or refuses and records nothing. The engine's local `hand-over` stays, for a
|
||||
the node's engine on `mesh.node.<node>.ask.hand-over` — a request on core NATS — and prints the engine's answer,
|
||||
failing on a refusal. Only the controller is granted a publish on that subject (a writers-table row), but that
|
||||
does not say who the engine hears: the bus lets a principal allowed to answer reply to a message it received on
|
||||
any reply subject the message named, and the tool runner, which runs as the operator's account and so as every
|
||||
agent, is allowed to answer (the review of 2026-10-09 proved it on a permissioned server). So the ask is signed
|
||||
with the mesh's key (node, path, who asked, a minute's expiry, a nonce; over a fixed context, so the signature is
|
||||
never a declaration's), and the engine verifies it before reading anything, refusing another node's, an expired
|
||||
one and a replayed nonce. The engine judges every value and records the hand-over, or refuses and records
|
||||
nothing. **The apply checks again**: a component of the path may have become a link after the hand-over was
|
||||
recorded, so a handed-over directory must still resolve to itself and is changed only through a descriptor opened
|
||||
from `/` that followed no link. The engine's local `hand-over` stays, for a
|
||||
machine no mesh can reach; its usage says the operator's line.
|
||||
|
||||
**The words.** Every text addressed to the operator names the `nox` line. hq's `words.py` gains the daemons'
|
||||
|
||||
Reference in New Issue
Block a user