From 69c6ff73e6dd72be9e4355ae7596569d38d4761b Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 12:30:31 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200259:=20after=20the=20review=20of=202026-?= =?UTF-8?q?10-09=20=E2=80=94=20root-free=20as=20a=20positive=20measure,=20?= =?UTF-8?q?warrants=20under=20ids=20nobody=20can=20guess,=20canonical=20di?= =?UTF-8?q?gests,=20the=20code=20shown=20to=20a=20person=20or=20not=20at?= =?UTF-8?q?=20all,=20unlinking=20bound=20to=20the=20account=20and=20possib?= =?UTF-8?q?le=20in=20the=20first=20hour,=20break-glass=20named,=20give=20a?= =?UTF-8?q?s=20built;=20the=20operator's=20desktop=20client=20and=20the=20?= =?UTF-8?q?controller's=20write=20to=20the=20router's=20state=20stated=20a?= =?UTF-8?q?s=20known=20gaps=20(issue=20344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...wers-it-and-the-asker-acts-on-a-warrant.md | 120 +++++++++++++++--- 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md b/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md index 48ba07e0..050499ec 100644 --- a/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md +++ b/02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md @@ -220,7 +220,10 @@ says the refusal on K's channel: 3. S is on the router's list of the operator's identities for K; 4. K's declared capabilities, **read from the controller's record of its claim and never from the channel**, meet the option's level: `acknowledge` needs `choice` and `exact-render`; `approve` needs - `verified-sender` as well (P1); `destroy` needs two proofs, one of them a code (P2); + `verified-sender` as well (P1); `destroy` needs two proofs, one of them a code (P2); and for `approve` + and above, **the controller's `root-free` verb answers, at that moment, that no agent can become root + without a person** on the router's machine and on K's channel's machine (§8 rule 2, added on 2026-10-09: + an absent condition is no pass); 5. the ask is closed with a compare-and-set on its record, so a second answer loses. Then it publishes the warrant on `decided.`, and sends every channel that showed the ask an edit @@ -243,12 +246,27 @@ met, and when. It carries no secret. The router puts the digest of the ask as it took it in the warrant. The asker acts only when that digest is the one of the ask it keeps, and only when the act it is about to perform is the one the option bound: an ask published again under the same id, or a record of the act changed after the ask, is refused and - nothing is done. + nothing is done. **Both digests are over named fields in a canonical encoding** (each value written as its + length, a colon and its bytes, in a fixed order, versioned), never over a language's encoding of a + structure, so any language reproduces them and a field added to an ask changes no digest until it is + added to the encoding on purpose (the review of 2026-10-09). +- **The router publishes each warrant under a message id nobody can guess** (a random one per publish), on + the asker's `decided` subject, which only the router may publish; the asker deduplicates on its own record + (a compare-and-set claim), never on the bus's duplicate window. A guessed id published first by another + principal into the same stream would otherwise have the real warrant dropped as a duplicate (the review of + 2026-10-09). - **Its record of the act names the warrant**: a hand-act's `by`, `via`, `ask` and `proofs` (ADR 0234 §8), a delivery's transition, a silence's why. The hand-act log counts it as a person's decision, not a repair. - **The controller only asks and acts.** For a condition that needs the operator it asks with the condition's actions as options, and on the warrant it calls the verb it named, with its own grant. It holds no channel, no identity and no factor. +- **A warrant is not yet the only way to these acts.** The acts a warrant authorises today — + `mesh-delivery.release` and `stop`, the controller's `plans` go and stop, `node-service-manager.restart`, + and `conditions silence` — are still verbs an agent may call directly with its own grant. Until + [research 039](../01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md) (a tool that needs a + person's approval) withholds them from every caller without a warrant, a warrant proves that the operator + chose an act, not that only the operator could have caused it. Said here so no reader takes the warrant for + that second guarantee. ### 7. Who the operator is on a channel: linking @@ -260,26 +278,48 @@ met, and when. It carries no secret. and the name the service shows. The router shows a six-digit code on the desk, with that name, valid for ten minutes. The operator sends the code to the bot; the channel passes it on the `proof` subject; the router links the identity when it matches, and says so on every channel. +- **A code is shown to a person or not at all** (added on 2026-10-09, after the review). The router issues a + code only while the desk channel says it is ready, and tells the sender the desk's reason when it is not; + a code is never queued for later. The desk is ready only while a named desk machine's notifier said within + five minutes that it is not paused (the notifier's `paused` event), and the code goes only to such a + machine; shown nowhere, it is said back to the router as failed. Every message carrying a code is marked secret: no + channel and no router record keeps a copy, the desk channel stores none and hands it to the node's + notifier as transient, and the notifier keeps it out of its history, where an agent could read it later. +- **The desk channel runs as an account of its own**, like the router and Telegram (§8 rule 1): the codes + pass through it. - **One identity per kind.** A second link of a kind is refused until the first is unlinked. - **Unlinking is an approve ask, answered on the account that is linked** (revised after the security review of 2026-10-08). It is never a plain tool: whoever unlinks the operator's account could link one of their own in its place. The notice of an unlink, and of any link for an hour after it, is sent to the account being replaced by name, before anything changes, and to every other channel. - **A link within a day of an unlink waits for the replaced account's approval**, asked there. When that - account is lost with its phone, the operator unlinks at a terminal on the control node as root (`messenger - unlink --at-the-machine`, refused over the bus), recorded and announced as break-glass, as ADR 0234 §8 does - for a lost factor. -- **Linking is bounded**: at most three links asked per kind an hour; the code's notice on the desk names the - account by the name and the number the service gives, and only a channel declaring `private` (the desk) - may show a code. + account is lost with its phone, the operator acts as root at the router's machine (break-glass, as ADR 0234 + §8 does for a lost factor): a regular file `unlink.` or `allow-link.`, owned by root, in the + router's own state directory (mode 0700, owned by the router's account). The router acts on it within a + minute, deletes it, ends every open ask of its own, and announces on every channel what root did. A file of + any other owner, or not a regular file, is refused and said: an agent without root cannot make one. +- **Linking is bounded**: at most three links asked per account an hour and ten per kind, counted in the + router's own state so a restart forgets nothing (a count that cannot be read or written refuses the link); the code's notice on the desk names the account by + the name and the number the service gives, and only a channel declaring `private` (the desk) may show a + code. +- **The ask to unlink or relink binds the account**, not only the kind (added on 2026-10-09): a warrant for + unlinking one account never unlinks the one linked after it. A link re-checks that its kind has no account + when it is made, and an account just unlinked cannot link again at once from a code shown before. +- **An account still in its first hour can be unlinked without its own approval** (added on 2026-10-09, after + the review): it cannot approve yet, so its consent protects nothing. When another account sends `/start` + while one is in its first hour, the router asks at the desk, as an acknowledgement bound to that account, + whether to unlink it, and tells the sender so. If the hour has passed when the answer arrives, the unlink + is refused. After the hour, unlinking needs the linked account's approval, or break-glass. - **A new link answers `acknowledge` at once and `approve` only after one hour.** The hour runs from the moment the link is made, is kept in the identity's record, and is said in the announcement ("it can approve from 15:40"). An approve answer inside the hour is refused and said on the channel. Decided by the operator on 2026-10-08, until an authenticator-app code exists. - **Once a TOTP factor exists**, linking and unlinking are destroy asks (ADR 0234 §8). -- **A residual risk is accepted until then:** on the X11 desk an agent can read the code and send it from - an account of its own. It is said loudly on every channel when it happens, the hour gives the operator - time to unlink it before it can approve anything, and the factor closes it. +- **A residual risk is accepted until then:** on the X11 desk an agent running as the operator can read the + code and send it from an account of its own. It is said loudly on every channel when it happens; during + the hour the account cannot approve, and the operator can unlink it from the desk without its approval (the + rule above — corrected on 2026-10-09: before that rule, the hour gave no way to unlink an account that + would not consent); and the factor closes it. ### 8. Who may say what on the bus: the trusted parties' own accounts @@ -314,11 +354,34 @@ The revision rests on three rules. **2. `verified-sender` is honoured only where the controller can show who holds it.** The controller passes a kind's `verified-sender` to the router only when its holder runs as its own -account, with its own bus account, on a machine where the self-check finds that **no account an agent runs -as can become root without a person** (no passwordless sudo, no membership of a group that grants root). -Otherwise it strips the capability from the membership, and the router answers that kind only as far as -acknowledgements, saying why. The same check holds the router: while it fails on the router's machine, the -router issues no approve warrant, and the condition says what to change. +account, with its own bus account, on a machine where **no account an agent runs as can become root without +a person**. Otherwise it strips the capability from the membership, and the router answers that kind only +as far as acknowledgements, saying why. The same check holds the router: while it fails on the router's +machine, the router issues no approve warrant, and the condition says what to change. + +*Revised on 2026-10-09, after the review of the first build.* **Only a positive, recent measure is a pass; +the absence of a condition never is.** The first build let the router approve while no `agent-root` +condition was open — so before the first self-check, after a probe error, with the conditions view lost, or +with the router not knowing its own machine, it approved. Now: + +- **The controller's `root-free` verb** answers, for each machine asked, whether no agent can become root + there without a person, judged at the moment it is asked. It is a pass only when the machine names an + account its agents run as, the machine's node-engine judged that account unable to become root (ADR 0266: + the node-engine reads it as root, on every look, and the controller takes a verdict at most fifteen + minutes old), and the login shell's `execute` is not served there (ADR 0268). Anything else — no agent + account named, so agents run as the operator; a verdict missing, stale or unhealthy; a store or the bus not + answering — is not a pass, and says why. +- **The measure is the node-engine's, not the measured account's.** The first build asked the sudo module, + served by the machine's runtime — whose account is the one an agent's calls run as — so the account + being measured answered its own measure. The node-engine runs as root, which an agent that cannot become + root cannot be. The machine's runtime account is counted as able to become root, always: the mesh's + acting tools give it passwordless sudo. +- **The router asks `root-free`** for its own machine and the answering channel's, when it shows an + approve option and again when an approve answer arrives; an error, a missing machine, or a router that does + not know its own machine is not a pass. **The controller asks it too when it composes**, and strips + `verified-sender` from a kind without a pass; the router's live question covers a pass that ends between + compositions. The `agent-root` condition is raised on the same judgement, so the condition and the verb + never disagree. **3. What remains, said plainly.** @@ -342,9 +405,21 @@ router issues no approve warrant, and the condition says what to change. ADR 0266 (hq pull request 225, not yet merged): agents on the control node run under an account of their own, without sudo. - **What cannot be measured is not a pass** (added on 2026-10-09). A machine where a trusted party runs and - the self-check cannot measure who can become root — the sudo module not assigned there, or not answering — - raises the same urgent condition, saying it was not measured, so the router approves nothing proven - there until it is. + the node-engine's verdict on its agent account is missing, stale or not given raises the same urgent + condition, saying it was not measured, and `root-free` answers no for it. +- **A known gap, accepted by the operator on 2026-10-09: the operator's own Telegram client on a machine + where agents run as the operator** (issue 344, hq pull request 244, not yet merged). The operator's Telegram may also run on such a + machine — their laptop, with Telegram Desktop. There an agent can read the client's data (found readable by + everyone) and drive the X display, and tap Approve as the operator. The router cannot tell such a tap from + one on the phone. Closing it needs agents under an account of their own on that machine, without access to + the operator's X display, and the client's data readable by the operator alone; none of it is built or + measured now, and approvals are not held back for it. Until then, approve from the phone. +- **A known gap, accepted on 2026-10-09: the controller's credential can rewrite the router's state** + (issue 344, hq pull request 244, not yet merged). The controller composes every account and grant on the bus, so a grant withheld from + its own credential — no write to the router's identities or asks — would be withheld by the very principal + that writes the grants, and undone by its next composition. Moving the router's buckets where the + controller cannot write therefore protects nothing against a controller that is not trusted, and is not + done; the controller is trusted with every record, and root on the control node is the mesh. - **The controller holds the whole JetStream API** today. Narrowing it to the streams and buckets it owns is planned work of its own; until then the controller is trusted with every record. - **On the X11 desk an agent can read a code as it is shown or typed** (ADR 0234's accepted risk). The hour @@ -378,7 +453,12 @@ router issues no approve warrant, and the condition says what to change. 6. **The controller as an asker**: conditions that need the operator are asked; it acts on warrants and records them; the words "from the mesh MCP server" give way to the router's own line, below. 7. **The vault** gains a verb by which the operator gives a module's own secret through a hidden prompt - on the desk, so a bot's token never passes through an agent's session. + on the desk, so a bot's token never passes through an agent's session. *As built (2026-10-09):* the + controller's `give`, through its `secret accept` path. It refuses `broker` and any secret the mesh makes + itself; the prompt is written by the desk's launcher from names alone (module, secret, machine), never + from words a caller chose, and says the controller asks; the bus lets only the controller publish the + prompt's verb; no secret value is accepted through a verb otherwise; and every value given raises the + urgent condition `secret-given` on every channel until the operator silences it. **Where an ask can be answered is said by the router**, from the channels that qualify: "Answer on Telegram, or from the mesh MCP server." The asker's words never name a channel.