From 7bd868786f4d91fe17955be2c90f6b3b5cb8513a Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 00:12:30 +0200 Subject: [PATCH] ADR 0268: the login shell's execute is withheld on the control-node until a call to it needs a person's approval The operator's decision of 2026-10-09: execute runs any command as an account that can become root without a person, so the control-node stops offering it now, and every node marks it as needing approval once the mark exists. To-be 41 gains WP7 for the seat and the shell module. --- ...l-a-call-to-it-needs-a-persons-approval.md | 139 ++++++++++++++++++ ...-the-shell-and-the-accounts-environment.md | 46 +++++- 2 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md diff --git a/02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md b/02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md new file mode 100644 index 00000000..0a3d68a2 --- /dev/null +++ b/02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md @@ -0,0 +1,139 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-09 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md +--- + +# 268. The login shell's `execute` is withheld on the control-node until a call to it needs a person's approval + +## Context + +The mesh's `node-login-shell` seat has one verb, `execute` +([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), +[ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)). It runs any +command it is given as the node's operator account, in a login shell. That account has passwordless +sudo, so one call is root on that node, and any caller of the mesh's tools can make it: an agent +session on any node, through the mesh MCP server, on any other node. + +ADR 0266 (hq pull request 225, not yet merged) moves agents on the control-node to an account of their +own that cannot become root without a person. From then on **the tools are the boundary** on that node, +and `execute` is a way round it. ADR 0259 §8 (hq pull request 211, not yet merged) counts the same +path in its self-check of who can become root where the trusted parties run. + +The operator first decided, on 2026-10-09, to drop the verb on every node. Agents use it a lot, though: +research 039 (hq pull request 233, not yet merged) counted about 300 calls across the four nodes on +2026-10-08, about a third of them running something with sudo. Most of the rest were reads: reading or +searching a file, a container's details or logs, a unit's state, the listening sockets, a user or a +name. Dropping it everywhere at once leaves those reads with no tool. So the operator narrowed the +decision the same day: withhold it on the control-node now, and keep it on the other nodes until a +call to it needs a person's approval. + +Two things in the mesh stood in the way of withholding it on one node only: + +- `execute` was a **required** verb of the seat. A holder that does not serve a required verb is judged + silent by the self-check, and holding the seat is conditional on serving it + ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)). +- A seat's verbs are the same on every node. Nothing let one node's holder serve fewer than another's. + +## Considered Options + +1. **Remove `execute` from the seat on every node.** This was the first decision. The verb would be + gone from every machine at once, and so would every read agents use it for. Removing a verb is + also a new seat version under + [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md): three steps across two + repositories. +2. **Refuse the address in the mesh MCP server, per node.** That refuses one client's calls, not the + verb. Any tool runner may call any tool on any node + ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)), so the + verb would still answer on the bus. +3. **Wait for the approval mark.** The mark is not designed yet (research 039), and the control-node + is the node whose root undoes ADR 0266 and ADR 0259 §8. +4. **Withhold it per node, through the holder's own setting.** The seat marks `execute` optional, and + the module holding the seat reads a per-node setting and does not register the verb where the + setting says so. The tool runner on that node then offers no `execute`, so nothing on the bus + answers it there. + +## Decision + +Option 4. + +1. **`execute` is an optional verb of `node-login-shell`.** This is + [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md)'s mark, used here for a verb + a node may withhold rather than one its holders are catching up to. A holder that does not serve it + on a node still holds the seat there, and is not judged silent. +2. **The holder has a setting, `execute`,** a preference whose default is `serve` + ([ADR 0262](0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)). + A node's own value `withhold` withholds the verb there. The mesh writes the value into a file that + root owns and the tool runner's account can only read. The holder's tools bundle reads it when the + tool runner starts it, and a change to the file restarts the tool runner. **Anything but `serve` + withholds the verb**: a wrong value, a missing line, a missing file. A verb that roots a node is not + served by accident. The holder says why, on stderr and in its own read tool. +3. **The control-node's value is `withhold`, from now on.** Its tool runner offers no `execute`, so a + search finds none there and a call to it is refused as an address nobody serves. The other nodes keep + the default for now. +4. **Once the approval mark exists, `execute` needs a person's approval on every node, the + control-node included.** The mark is being designed in research 039 (hq pull request 233): a tool + or verb declares that a call needs a person's approval, and the mesh refuses the call without a + warrant from ADR 0259's flow. The control-node may then serve `execute` again, under the mark. The + other nodes keep it unmarked until then. +5. **Narrow read tools replace the common reads first,** each a tool of the module that owns the area, + so that an approval is asked for an act and not for a read. Otherwise nearly every approval is for a + read, the operator learns to approve without reading, and approval is worth nothing. The reads + measured on 2026-10-08, as follow-ups: + - reading or searching a file on a node (`cat`, `grep`), bounded to what the account may read; + - a container's details and logs (`docker inspect`, `docker logs`), in the module that holds the + container runtime; + - a unit's state and journal (`systemctl`, `journalctl`), checking first whether + `node-service-manager`'s `status`, `journal` and `failed` cover the calls, and widening them where + they do not; + - the listening sockets (`ss`); + - an account or a name (`getent`, `id`); `node-hostname`'s `entries` already covers `getent hosts`. + +## Consequences + +- On the control-node, no mesh tool runs an arbitrary command any more. Agents there use the narrow + tools that exist, or ask the operator. +- On the other nodes, any caller of the mesh's tools can still become root through `execute` until the + approval mark exists. This is the operator's choice, made with the count above. +- The seat now has no required verb, so a holder's silence on the bus is not judged on any node, the + nodes that still serve `execute` included. A node whose holder stops answering is noticed when a call + fails, not by the self-check. +- ADR 0259 §8's self-check asks whether the login shell runs commands as an account that can become + root on a node. It must read whether `execute` is served there, not whether the seat is held. Its code + is on an open branch (mesh-controller pull request 154) and is changed there. +- Someone with root on the control-node can still change the file and restart the tool runner. That is + the root ADR 0266 takes away from agents. This decision closes the remote path, not that one. + +## How it is checked + +- **The seat:** `TestTheLoginShellsExecuteIsOptionalSoAMachineMayWithholdIt` (mesh-controller) holds + `execute` optional, in the compiled seat and in a seat row read back from the store, and a holder + that serves none still holds the seat. `TestALoginShellWithholdingExecuteIsNotSilent` holds that the + self-check does not expect it to answer. Both are in mesh-controller pull request 169. +- **The holder:** the zsh module's tests (mesh-catalog pull request 145) hold that only `serve` serves, + that every other case withholds and says why, that the file is root's and named exactly by the + bundle's word, and that the bundle registers no seat verb when withheld. +- **Live, on the control-node:** `mesh-controller.settings` for `zsh` lists the control-node's value + as `withhold`. The holder's `zsh_config` there answers `execute` as not served, with the reason. + `mesh_search` for `node-login-shell execute` names the other three nodes and not the control-node, + and a call to the control-node's address is refused. +- **Not yet checked:** that the follow-up read tools exist, and that `execute` is marked once the mark + exists. Research 039 carries both until a decision does. + +## References + +- [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), + [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md): the seat and its + verb. +- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), + [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md): required and optional verbs. +- [ADR 0262](0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md): + a preference and a node's own value. +- [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md): any tool + runner may call any tool on any node. +- ADR 0266 (hq pull request 225), ADR 0259 (hq pull request 211), research 039 (hq pull request 233): + not yet merged, so named without links. +- mesh-controller pull request 169, mesh-catalog pull request 145. diff --git a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md index 901745b0..a150db14 100644 --- a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md +++ b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-host, mesh-controller, mesh-catalog] -updated: 2026-10-08 +updated: 2026-10-09 decisions: + - 02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md - 02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md - 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md - 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md @@ -51,7 +52,8 @@ Its two files are written by the node-engine from placeholders the controller fi keybindings, the aliases and the two small functions, with the three slots in place; - contributes its own environment: the editor, the configuration home, and `~/.local/bin` plus the two script directories on `PATH`; -- serves `execute` and its own `zsh_config`. +- serves `execute` where the node's setting serves it, and its own `zsh_config`. The control-node + withholds `execute` (WP7). **The prompt module** (`powerlevel10k`) ships the theme as a pinned vendored archive, and the prompt's configuration as its own file in a directory it owns. It contributes the zsh code that loads both. @@ -85,6 +87,7 @@ WP3 the modules (mesh-catalog) needs WP WP4 the service manager's module, finished (mesh-catalog) independent WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled WP6 a module puts the account in a group (mesh-host, mesh-controller, mesh-catalog) issue 247 +WP7 the control-node withholds execute (mesh-controller, mesh-catalog) ADR 0268 ``` WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository @@ -292,3 +295,42 @@ on the first workstation the operator sends the lighting module to: **Order.** The three repositories merge in any order. A node-engine older than WP6 already applies the lighting module's resource: it puts the account in the group. It does not say the login, and does not take the group back. + +## WP7 — The control-node withholds `execute` + +*`mesh-controller`, `mesh-catalog`. Half a day. +[ADR 0268](../../02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md).* + +`execute` runs any command as the operator account, which can become root without a person. The +control-node withholds it until a call to it needs a person's approval. The other nodes keep it for now. + +``` + settings zsh on the control-node: execute = withhold (the default is serve) + │ + node-engine writes /tools.conf, root's, 0644; its change restarts the tool runner + │ + zsh bundle reads it at start: anything but serve ── registers no execute + │ + tool runner offers no node-login-shell.execute on the control-node; a call to it is refused +``` + +**What changes.** + +- **The seat table.** `execute` becomes an optional verb of `node-login-shell` + ([ADR 0246](../../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)'s mark). + A holder that withholds it still holds the seat and is not judged silent. The seat now has no + required verb, so no node's holder is judged silent for it. +- **The shell module.** It gains the setting `execute` (default `serve`), a root-owned file the mesh + writes it into, and a word naming that file for its tools bundle. The bundle registers `execute` only + when the file says `serve`. `zsh_config` also answers whether `execute` is served on the node, and why. + +**Merge order.** The controller first, then the catalogue. Either order composes. With the catalogue +alone and the control-node's value set, the self-check would say the holder there is silent. + +**Proof.** The tests ADR 0268 lists under *how it is checked*. Live, after both are merged and rolled +out, the control-node's value is set to `withhold` and zsh is pushed to it. Then: + +1. `mesh-controller.settings` for `zsh` lists the control-node's value as `withhold`; +2. the control-node's `zsh_config` answers `execute` as not served, and says why; +3. `mesh_search` for `node-login-shell execute` names the other three nodes and not the control-node; +4. a call to the control-node's `node-login-shell.execute` is refused, and the other nodes still answer.