From ee314a26cac02c0ca5608c600041084f88a707a4 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 11:03:27 +0200 Subject: [PATCH] ADR 0266: say where the boundary stands on 2026-10-09, and the rollout's order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The routes found around the agent account since the record was written are closed on main (the store's reader, places and served keys, what every session obeys, execute withheld on the control node); the record names them, drops the store gap it said kept the boundary from holding, and says plainly what still remains, first the login shell's execute on the other nodes. To-be 36 §9 gains the prerequisites and the order the parts go out in. --- ...hat-cannot-become-root-without-a-person.md | 61 ++++++++++++++++--- .../36-the-operators-agent-on-a-machine.md | 13 +++- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md b/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md index 838422cc..a357c17c 100644 --- a/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md +++ b/02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md @@ -245,7 +245,7 @@ here: - the controller's `command` verb ran any of the controller's command lines. It could set the agent account (§1), set the operator key, issue a credential or hand out a secret. It is now an allow list of reads: `status`, `version`, `help`, `seats`, `healers`, `hand-acts`, `durations`, `collection`, `images`, - `artifacts`, `data`, `builds`, `queue`, `plan`, `plans` but not its stop, close and go, `doctor` and its + `artifacts`, `data`, `builds`, `queue`, `plan`, `plans` but not its go, stop, close or retry, wherever the word stands in the line, `doctor` and its probes and signals but not `doctor run`, `conditions` and its list, show and history but not silence, `node list` and `node show`, `module list`, `settings show` and `settings preferences`, `retire list`, `cleanup list`, `delivery plan` and `delivery walks`, `bus` alone, and `mirrors` without recording. Every @@ -260,19 +260,62 @@ here: symbolic link put there by the agent would have pointed root at any file. Below a home it now refuses a symbolic link in the path, opens each part without following a link (a directory as a directory), and refuses an opened file of the wrong kind or with more than one hard link before it changes owner or mode - on it. It still unpacks an archive, and writes into an existing file, after a check of the path; the - catalogue's own check refuses either under `${machine:agent-home}`, so no module opens that gap; + on it. It still unpacks an archive, and writes into an existing file, only after a check of the path, so + it refuses either below the home of any account the declaration says never becomes root, and the + catalogue's own check refuses either under `${machine:agent-home}`, so no module asks for that gap; - the backup's restore wrote, as root, beside the live data. Beside a directory another account owns, such as the agent's home, it now restores into its own module's state instead. A tool that runs a caller's command as root, or reads, writes or removes a caller's path as the operator or as root, is a defect of that tool, found by review of its manifest and code. -**The boundary does not hold yet.** On 2026-10-08 review found that the store's read-only login, which the -mesh MCP server's `mesh-store.query` uses, can read every database, including every sealed secret and the key -that signs every node's declaration. Its fix is tracked in an issue of its own. Until it lands, any caller of -the mesh MCP server, the agent account included, can forge what a node applies as root, and this record's -boundary is not in force. +**Where the boundary stands, 2026-10-09.** *Added 2026-10-09, after review of the routes found since this +record was written; what was decided above is unchanged.* The tool runner acts for any caller of the mesh MCP +server, so the agent account's boundary is only as good as the tools that caller reaches. These routes were +found around it and are closed on the mesh's main line: + +- **The store's reader** ([issue 337](../04-ISSUES/337-the-stores-reader-can-read-the-controllers-signing-key/00-report.md)): + `mesh-store.query` could read every sealed secret and the key that signs every node's declaration, so any + caller could forge what a node applies as root. The reader now reads only what a module declares readable, + never a secret. Live on the control node and the home server since 2026-10-08. +- **A module's places and accesses, and the keys a provider serves** + ([issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md)): a settings + call could have the node-engine, as root, hand a system directory to any account, or mount one into a + container. Those keys, every key a provider serves, every setting a trusted file asks for, and the whole + layer of a module with a trusted mergeable file are now set at the controller's terminal alone. The + node-engine refuses a placement or a mount at the machine's own trees, root's or another account's + directory below a home, and uses a directory it did not make as found. This record's own copy of that + engine guard was dropped for it; the one rule kept here is the archive and written-into file below an + agent's home (§7). +- **What every Claude Code session obeys** + ([issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)): + an agent could register a hook, managed settings, a permission rule or a tool server for every node, and + have it run as the operator at the operator's next session elsewhere. Those, and shared agent text, are now + set at the controller's terminal alone. The home scope is written only into the home of an agent account + of its own, on the node serving the call, never into the operator's home. +- **The login shell's `execute` on the control node** + ([ADR 0268](0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md)): + it ran any command as the operator account. The control node withholds it; the setting that withholds it + is a trusted file's, so it is changed at the terminal alone. + +**What still remains, said plainly.** None of these is closed by this record or its pull requests: + +- **The login shell's `execute` on the other nodes**, and the desk's `node-terminal-emulator.open` and + `rofi_run`, run a caller's command as the operator account of their node, which has passwordless sudo + there. So an agent on the control node can still become root **on another node** through the mesh MCP + server. Whether the operator account on one of those nodes holds a key that logs into the control node + was not measured; where one does, this is also a way back to root on the control node. ADR 0268 §4 closes it + once a call to such a tool needs a person's approval (research 039); until then, withholding `execute` on + every node is the operator's choice, at the terminal. +- **Tool servers already on the bus** keep rendering until their own change lands (issue 340, "still + open"). Text written straight into the bus state needs a tool runner's credential, which the agent account + cannot read. +- **The tool runner on the control node is still the operator account** (Considered Options), so a tool + found tomorrow that reads, writes or runs a caller's path or command is a way round this record until it is + bounded. Each is a defect of that tool, found by review. +- **The self-check judges this record's account only.** ADR 0259's own root probe (mesh-controller pull + request 154) asks the same question through the `sudo` module; it must call this record's judgement, one + function, rather than keep a second answer. The agent account holds a forge key of its own, registered by the operator, so its pushes are its own. It works on branches and pull requests, and a merge needs a person anyway. @@ -306,7 +349,7 @@ works on branches and pull requests, and a merge needs a person anyway. | An older engine is not sent `root` | controller test: a user resource's `root` is taken out for an engine whose statement says contract 2 or less | | The node-engine judges each way §3 lists, before any session logic, in the C locale, and an unanswered question is never healthy | node-engine tests over a fake reader, for each way and for none, for doas without sudo, for a search that ran out of time; a parsing test of `sudo -l -U` | | `command` runs only reads, and no verb sets an account, the operator key or a credential, hands out a secret or the joining token | controller tests: about 60 writing, issuing or revealing lines refused through `command` and the named verbs; about 33 reads pass | -| No archive and no file written into is placed under the agent's home | the catalogue's `checks/agenthome`, in its `merge-check.sh` | +| No archive and no file written into is placed under the agent's home | the catalogue's `checks/agenthome`, in its `merge-check.sh`; node-engine test: an archive or a written-into file below the home of a `root: never` account is refused, a whole file passes | | Root follows no link below a home | node-engine tests: a linked `~/.claude`, a linked parent, a file that is a link, a link swapped in after the check, a hard-linked file, an opened file of the wrong kind | | A restore beside a directory another account owns goes to the module's state | backup module test | | `claude_code_add_api_key` takes only an API key from the agent's own home | `claude-code` tests: a path outside, `..`, a link, a file that is not a key; nothing removed, nothing repeated; licence manager test refusing a key of another shape | diff --git a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md index 736de870..9d1e0ff8 100644 --- a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md +++ b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md @@ -2,7 +2,7 @@ layer: to-be status: in-progress code: [mesh-catalog modules/claude-code] -updated: 2026-10-08 +updated: 2026-10-09 decisions: - 02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md - 02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md @@ -339,8 +339,15 @@ root that no verb serves. the send in step 3, so everything that needs one comes before it. The operator's own shell, and its sudo, are never refused: the way back never needs Claude. -1. The node-engine, the controller and this module are built with ADR 0266 and sent to every node. The - engines say contract 3, and nothing changes on any node. +0. **Before any of it**, the routes round the boundary that ADR 0266 §7 names as closed are closed and + running: the store's reader (issue 337), places, accesses and served keys at the terminal (issue 339), + what every session obeys at the terminal (issue 340), and `execute` withheld on the control node + (ADR 0268). All four ran on 2026-10-09; the tool servers already on the bus stop rendering with their + own change. What ADR 0266 §7 says still remains is the operator's to weigh before step 3. +1. The node-engine, the controller and this module are built with ADR 0266 and sent to every node, in this + order: the node-engine first, until every engine says contract 3; then the controller, its node-engine + pin moved to the engine's merged commit; then the module's code; then its manifest, which the running + controller refuses until it knows the new machine facts. Nothing changes on any node. 2. Before naming the account, the operator finishes or ends every Claude session running as themselves on the control node; from step 3 on they can do nothing. 3. The operator states the agent account at the controller's terminal. The next send creates the account,