From c08b6214915c9ceed42c185b0f30711038334464 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 17:35:14 +0200 Subject: [PATCH] Revise ADR 0259 after the security review: the router and verified channels get bus accounts of their own, verified-sender follows placement, and unlinking is asked of the linked account --- ...wers-it-and-the-asker-acts-on-a-warrant.md | 83 ++++++++++++++----- .../46-the-conversation-with-the-operator.md | 6 +- 2 files changed, 68 insertions(+), 21 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 b3279f24..5d610695 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 @@ -250,8 +250,18 @@ 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. -- **One identity per kind.** A second link of a kind is refused until the first is unlinked. Unlinking - (a tool of the router) is said on every channel, the unlinked one included. +- **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. - **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 @@ -261,26 +271,55 @@ met, and when. It carries no secret. 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. -### 8. The `telegram` module's own account +### 8. Who may say what on the bus: the trusted parties' own accounts -Decided by the operator on 2026-10-08. The bot token and the channel's state are kept from every agent, -because every agent runs as the operator. +*Revised after the security review of 2026-10-08, and not yet accepted by the operator.* The first version of +this section rested on the tool runner running as root with a credential only root reads. **On the control +node it runs as the operator's account, its bus credential belongs to that account, and that account may +become root without a password.** Every agent runs as that account. So an agent there could read the runner's +credential and publish anything any carried module may: a warrant, an answer from Telegram, a link. The +service account of the first version kept the bot token from nobody. -- **Its process runs as the account `telegram`**, which the module makes (a `user` resource: no shell, no - login, home in its state directory). The tool runner, which runs as root, starts the module's bundle - as that account when the bundle's environment names it (`MESH_RUN_AS`); it refuses to start a bundle as - root or as the operator's account. -- **Its state directory is owned by `telegram`, mode 0700, and its own secret `telegram-token` by - `telegram`, mode 0600** (the manifest's `owner` and `secrets-owner`). The operator's account can read - neither, and cannot attach to a process of another account. -- **The bus credential stays the tool runner's**, readable by root only. The bundle reaches the bus only - through its standard input and output, and the runner publishes for it only the kind it claims. -- **An agent reaches the runner only through the mesh MCP server's five tools**, which make tool calls. - No tool publishes an intake event or a proof: those come only from the bundle's own channel. -- **What remains if an agent has root on the control node:** nothing here holds. Root reads the token, - the runner's bus credential and the router's state, and can publish as anything. The boundary rests on - agents not holding root there; whether the operator's account may become root without a person is - checked by the self-check, which raises a condition when it can. +The revision rests on three rules. + +**1. The trusted parties hold bus accounts of their own, never a runner's.** + +- The **router** (`operator-channel`'s holder) and **every channel that declares `verified-sender`** run as + their own process, as their own service account (`messenger`, `telegram`), with a bus account of their + own: an own secret `broker`, owned by that account, mode 0600. They are not carried by the tool runner. + Their tools are served by their own process on their own tool subjects. +- **The controller refuses to compose** into any runner's user, or into any principal whose process runs as + the node's operator account: + - publishing on a by-caller event of `operator-channel` (a warrant); + - writing the router's state (asks, identities); + - publishing on, or answering, a kinded bench's subjects for a kind declaring `verified-sender`. + A manifest asking for it is refused at registration, and a runner carrying such a module is refused at + composition, naming the module. +- **Proofs are answered by the holder of `operator-channel` alone**, not by any module watching the bench; + and **a kind's accepts are published by that holder alone**, not by every user of the bench. +- **The tool runner hands a bundle no bus variable** of its own: the credential and the bus's address are + taken out of every bundle's environment. + +**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. + +**3. What remains, said plainly.** + +- **Root on the router's or the channel's machine defeats all of this.** The self-check raises an urgent + condition while an agent's account can become root there without a person; on the control node today it + can (the operator's account has passwordless sudo, measured on 2026-10-08). The operator chooses one of: + a password for sudo on that machine; agents there under an account of their own without sudo; or the + router and the channels placed on a machine where neither holds. +- **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 + after a link, the replaced account's approval and the announcements bound what a stolen code can do. ### 9. What is superseded in ADR 0234 @@ -347,6 +386,10 @@ Telegram, or from the mesh MCP server." The asker's words never name a channel. | An ask no ready channel can carry is refused at once, in words | router test | | Linking needs the code shown on the desk, and is said on every channel | router tests: a wrong code, a late code, a second link of a kind | | A new link approves only after one hour; it acknowledges at once | router tests: an approve answer at 59 minutes refused and said, at 60 accepted; an acknowledge at once accepted | +| No runner's user, and no principal running as the operator's account, may publish a warrant, write the router's state, or speak for a verified-sender kind | controller composition test over every such grant; registration refuses a manifest asking for it | +| `verified-sender` reaches the router only from a holder with its own account on a machine no agent can become root on | controller test stripping the capability; self-check probe raising the condition | +| Unlinking and a relink within a day are approve asks answered on the linked account; the notice reaches it first | router tests, with a fake channel that honours the addressee | +| No bus variable reaches a bundle | tool runner test over a bundle's environment | | The `telegram` bundle runs as its own account, never root or the operator's | tool runner tests: a bundle naming `MESH_RUN_AS` is started as that account; root and the operator's account refused; the manifest's owners checked by the catalogue's manifest test | | A tool call never publishes an intake event or a proof | tool runner test: only a bundle's own channel reaches the seat publish, for its own kind | | The asker acts once per warrant and records the warrant | shared library test; controller test reading the hand-act after a warrant | diff --git a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md index b8e5f3fc..503672b7 100644 --- a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md +++ b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md @@ -476,6 +476,10 @@ needs the lost factor's code. So: ### Telegram — kind `telegram`, both seats, the away channel +- *Revised after the security review of 2026-10-08 (ADR 0259 §8, not yet accepted):* the router and every + channel declaring `verified-sender` run as their own process and service account with a bus account of + their own, never carried by a tool runner; the controller passes `verified-sender` only for a holder so + placed, on a machine where no agent's account can become root without a person. - **Its own module**, `telegram`, holding the bot token as its own secret; the router keeps no Telegram client. *Amended 2026-10-08, ADR 0259:* **it runs on the control node under its own account `telegram`**, never root or the operator's: the tool runner starts its bundle as that account, its state directory and @@ -554,7 +558,7 @@ Each phase ends at its own *done when*. Owning repositories from [`repos.md`](.. | **3 — The seats, and the router** | the kinded bench in the seat set; `kind` and `capabilities` on a claim; `channel-capabilities/1` and its contract tests; publishing on a seat's event subjects and its grant; `channel` and `intake` seats; the output seat's holder becomes the router and holds `channel/desktop` and `intake/desktop`; presence as current state; the operator's identity list and identity check in the controller, the router's `trusted` stamping and untrusted input (§4); the content rule allowing machine names, references and `detail`, refusals naming the offending part (§9) | mesh-controller (seat set, claim fields, registration refusals, grants, the identity list and check); mesh-sdk and mesh-tools (seat-event publishing in the shared library and the tool runner); mesh-catalog (the seats' definitions, the router) | the catalogue refuses an unknown capability and a second holder of one kind; a message is routed to the desk by capability and context; an envelope from an identity not on the list is re-emitted untrusted; a path is refused to its sender by name, and travels as a reference the desk opens | | **4 — Asks, and operator messages** | `ask`, `ask cancel`, `asks`, `answer`; the events; kinds, life, limits, batching, history; escalation; addressing, the register of agents with kept messages, the responder; the untrusted-input rule in every agent module's instructions (§5) | mesh-catalog | the router tests of ADR 0234 pass; live: an agent's question answered at the desk, and with the desk locked, on the phone; `@mesh status` answered; a message to a stopped agent kept and said | | **5a — Asks on the bus** (ADR 0259) | the kinded bench, caller-named verbs, `proof` subjects and the records users read (controller); a bundle's seat traffic, compare-and-set state and its own account (tool runner); the asker's and channel's types (shared library); the router's asks, tokens, identities, judging, warrants, expiry and linking with its hour (messenger); `telegram` and `desk-channel`; the controller asking for its conditions' actions and acting on warrants | mesh-controller, mesh-tools, mesh-sdk, mesh-catalog | a held delivery released from the phone; the hand-act reads "the operator, via telegram, user id verified"; a desk Silence goes through an ask | -| **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the controller's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced | +| **5 — Authorise, with TOTP** | `authorises` in the verb table; the router's checks for the code; direct calls refused, break-glass; `retire approve` takes `expect`; the hand-act fields; the TOTP seed as the router's state, enrolled at a terminal with ten recovery codes; `factor recover`, `factor status`, the local-only break-glass enrolment; no P2 tier before recovery codes exist; optional key enrolment; the self-check probe for the away channel; agents' grants lose the authorising verbs | mesh-controller; mesh-catalog (agents' grants, the desk's code prompt) | one controller test per refusal passes; at the desk, approve needs a code; a recovery drill re-enrols with a recovery code, and is announced | | **6 — Telegram live** | the `telegram` module (built in 5a) with replies and forced-reply codes, under its own account on the control node; the operator's two bots configured; the watcher assigned off the control node; the dead-man ping | mesh-catalog | the self-check says the away channel carries every tier; live drills: an approve and a destroy on a test condition answered on the phone, with the hand-acts read | **Not touched:** the node engine (`mesh-host` repository). Presence comes from the screen-lock seat's