From 95d78f7011cccea6ad0e975043b65e03bd6af8d4 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 11:59:34 +0200 Subject: [PATCH 1/9] ADR 0272 (proposed) and issue 343: the controller's terminal has no command The records send the operator to "the controller's terminal", and on the control-node `mesh-controller` is not a command: the controller is a mesh process with its own account and environment, and nothing puts anything on PATH. The record proposes mesh-cli, a module on every node whose program asks the node-engine over a local socket; the node-engine vouches for the account from the kernel and asks the controller on its node's own subject; the controller runs the line as the terminal only for the operator's account where no agent can be that account. The glossary gains mesh-cli, nox (its zsh alias) and the controller's terminal. --- 00-META/glossary.md | 15 +- ...nly-where-no-agent-runs-as-its-operator.md | 226 ++++++++++++++++++ .../00-report.md | 62 +++++ .../01-diagnosis.md | 75 ++++++ 4 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md create mode 100644 04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md create mode 100644 04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md diff --git a/00-META/glossary.md b/00-META/glossary.md index 9062b7fa..2a7dd534 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -272,6 +272,11 @@ and the operator act. **Recorded by** the controller's `identity` and `licences` `release`, an identifier to rename. - **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**. +- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a + verb, so a change that is the terminal's alone is taken (issue 339, issue 340). In the controller's code it is a + line run without `MESH_VERB`. The operator reaches it with `mesh-cli` from their own account on a node where no + agent can run as that account: the control-node, and a node whose agents run under an account of their own + ([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)). ## Change and delivery — a commit on its way to the nodes @@ -437,7 +442,7 @@ it should not. **Recorded by** the controller. **Upstream of** Provisioning and trust. **Recorded by** the router and the controller (authorising asks). **Downstream of** Health and repair, Change and delivery, and Identity and access. An authorising answer acts in whichever domain asked, through that domain's own verb; the conversation owns the -asking, never the act. **Decided by** ADR 0152, 0175, 0234. +asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0272. **Uses:** operator, person, proof (Identity and access), release (Change and delivery). - **mesh MCP server** — the endpoint on a node's loopback through which every agent and person on that @@ -446,6 +451,14 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234. not a module of its own. "Console" suggested a terminal or a shell, and the module `mesh-console` it once named no longer exists; "the tool bridge" and "the brain" named the predecessor's program. *Not:* ~~console~~ (hq), ~~mesh-console~~, ~~tool bridge~~ +- **mesh-cli** — the module on every node that places the operator's command, and the command itself: + `/usr/local/bin/mesh-cli ` asks the controller to run one of its command lines. It asks the + node-engine on its own machine, which says to the controller which account is asking; the controller runs the + line as the **controller's terminal** or as an ordinary call + ([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)). +- **nox** — `mesh-cli`'s alias, in interactive zsh only. A script, `sudo` and a document meant to work everywhere + say `mesh-cli`. The operator also calls the mesh itself *nox mesh*; on this page and in the records *nox* is the + alias alone. - **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel` sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** from the fixed vocabulary `channel-capabilities/1`. Not *notifier* (that is the desktop's node seat, one diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md new file mode 100644 index 00000000..172377fd --- /dev/null +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -0,0 +1,226 @@ +--- +topic: the tiers +status: proposed +date: 2026-10-09 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md +--- + +# 272. The operator reaches the controller with `mesh-cli` on every node, and a node is the controller's terminal only where no agent runs as its operator + +## Context + +Some changes to the mesh are made **at the controller's terminal** only: + +- the places and accesses a module's settings name, and the keys a provider's trust or a trusted file reads + ([issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md)); +- the agent module's managed settings, tool servers and shared text + ([issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)); +- a node's agent account (ADR 0266, hq pull request 225, not yet merged), typed as + `mesh-controller node agent-account `. + +In the controller's code "the terminal" is a negative: a command line that did not come through a verb. The +serving controller names the verb in the environment of every command it runs for one (`MESH_VERB`, issue 327), +and a terminal-only change is refused when that variable is set. Nothing says who the terminal is, and nothing +lets the operator reach it. + +On 2026-10-09 the operator typed `mesh-controller …` on the control-node and the shell answered +`zsh: command not found: mesh-controller` +([issue 343](../04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md)). The controller runs as a +mesh process: the node-engine unpacks its bundle under the mesh's own directory, and writes a unit that runs it as +the `mesh-controller` account with an environment composed from the controller's module (its stores' credential +files under a directory only that account may enter, the stores' and the bus's ports). Nothing puts a command on +any `PATH`, and running the binary by hand would mean rebuilding that environment under `sudo`. + +The operator's directions of 2026-10-09 shape what follows: no wrapper script and no link (the mesh creates no +symbolic links); the operator's command is a real program, its own module, on every node; it reaches the +controller over the mesh, not by running the controller's binary; and the person is proven by the account the +command runs from, vouched for by the operating system, never by a credential file a copy of which is as good as +the original. + +## Considered Options + +### How the operator reaches the controller + +1. **A link to the controller's binary on `PATH`.** Refused by the hard rule, and not enough: the binary needs the + unit's account and environment. +2. **A wrapper the controller's module places on the control-node**, running the controller's binary as its + account with the unit's environment through `sudo`. Rejected. It is a second copy of the unit's environment to + keep in step (or a new process field for the node-engine to render), it reaches the controller from the + control-node only, and its proof is `sudo`, which every agent on the control-node holds until ADR 0266 is + switched on. +3. **A program on every node that asks the controller over the mesh.** Chosen. The controller already runs every + command line a verb asks for in a fresh process of its own binary, with its own environment (`runVerb`); one + more way of asking needs no copy of that environment anywhere. + +### Where the program comes from + +1. **A second artifact of the controller's module.** Rejected: that module is assigned to the control-node only, + so its files reach no other node. +2. **A module of its own, assigned to every node.** Chosen. It builds from its own directory, so a change to it + rebuilds it alone and a change to the controller does not rebuild it + ([ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md)). + +### How the person is proven + +1. **A credential file in the operator's home.** Rejected. Any agent running as the operator reads it, and a copy + works anywhere. +2. **`sudo`.** Rejected as the proof. It asks the account's password, or nothing where the rule says so, and + agents on the control-node hold the same `sudo` today. +3. **The account the program runs from, read by the node-engine from the kernel.** Chosen. The program connects to + a local socket the node-engine serves; the kernel tells the node-engine the connecting process's user id + (peer credentials). Nothing is presented, so nothing can be copied. The node-engine is on every node and runs + as root, so it is the one part of the mesh that can vouch for a local account. +4. **A second factor**: a one-time code or an approval on a verified channel + ([ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md), ADR 0259). Not now. It is the route for + a node whose agents must share the operator's account, and it waits for the approval flow (research 039). + +### Which account on which node is the terminal + +1. **The operator's account on any node.** Rejected. On every node but the control-node, and on the control-node + until ADR 0266 is switched on, agents run as that account. An agent would be the terminal. +2. **The operator's account on the control-node only.** Safe, and too narrow for later. +3. **The operator's account on a node where agents cannot be that account**: the control-node, and any node that + names an agent account of its own judged unable to become root without a person (ADR 0266 §1, §3, §4). + Chosen. + +## Decision + +### 1. `mesh-cli` is a module and the command it places + +**`mesh-cli` is a module of the catalogue, assigned to every node.** It builds one Go bundle from its own +directory, and an `archive` resource unpacks it into `/usr/local/bin`, so the program is +`/usr/local/bin/mesh-cli` on every node. It is the bundle's own file: no link, no second copy. The node-engine +moves it in beside what is there, refuses to write over a file it did not place, and takes out only what it +placed when the module goes. + +**`nox` is its alias**, `alias nox=mesh-cli`, contributed by the module to zsh's `normal` slot +([ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)). `/usr/local/bin` is +already on every account's `PATH`, so the module contributes no environment. The alias exists in interactive zsh +only: a script, `sudo`, a cron line and any document meant to work everywhere say `mesh-cli`. + +### 2. What the operator types + +`mesh-cli ` takes the controller's own command line, in the words the controller's commands and +their refusals already use: `mesh-cli settings set zsh '{"execute":"withhold"}' --node ` is +`settings set zsh … --node ` asked of the controller. It is run as the operator's own account, without +`sudo`: root is not the operator's account, and is answered as any other account (§4). + +`mesh-cli` holds no credential and reads no file of the mesh. It does one thing: it asks the node-engine on its own +machine over a local socket, and prints what comes back. A process that carries `MESH_VERB` (a verb's own process) +is refused at once, and told why: a verb is never a person. + +### 3. The node-engine vouches for the account + +The node-engine serves a socket every local account may connect to. For each connection it: + +1. reads the connecting process's user id from the kernel (`SO_PEERCRED`), and names the account from the + machine's user database; +2. sends the line, the account and the user id to the controller on `mesh.control..cli`, as a request on + core NATS. Only that node's engine may publish under `mesh.control..`: its bus account is granted that + prefix and nothing of any other node's, so **the node is a fact the bus server enforces and the account is a + fact the kernel gives**. The CONTROL stream names its own subjects, so the request is never persisted; +3. says with it whether the node's declaration names an agent account, and the engine's latest judgement of it + (ADR 0266 §3); +4. answers the program with what the controller answered, or says that the controller did not answer within its + call bound. + +The node-engine is trusted for its own node. Root on a node could claim any account there; root on a node that is +the terminal is already the operator's, and §4 makes no other node the terminal. + +### 4. The controller judges, and runs the line + +A line from node N, vouched as account A, **runs as the controller's terminal** when A is N's operator account +([ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)) and either: + +- N is the control-node; or +- N names an agent account of its own (ADR 0266 §1), and the controller's own record of the engine's judgement, + heard within ADR 0266 §4's fifteen minutes, says it cannot become root without a person. + +The controller runs it as it runs a verb's line, in a fresh process of its own binary with its own environment, but +without `MESH_VERB`, and records the caller as *A through mesh-cli on N*. + +**Every other line is an ordinary call.** It goes through exactly what the `command` verb does with a line: its +refusals today, and its list of reads once ADR 0266 §7 builds one. It runs with `MESH_VERB=mesh-cli`, so every +terminal-only change is refused with the controller's own reason. The answer adds one sentence saying why this +account on this node is not the terminal: an agent account, root, an account that is not the operator's, or a node +whose agents may run as its operator. + +**The control-node rule is no wider than today.** On the control-node the operator's account holds `sudo`, and +until ADR 0266 is switched on so do the agents running as it: either could run the controller's binary as root +with its environment. `mesh-cli` gives that account the same reach and no more. Once agents there run as `agent`, +`agent` is never the terminal. + +### 5. What comes back + +The answer is the command's standard output, its standard error, its exit status, whether it ran as the terminal, +and why or why not. `mesh-cli` prints the two streams and exits with the status. An answer larger than one bus +message is cut there and the cut is said; it is never silently short. Standard input is not carried: a command +that reads it gets nothing and fails saying so. Streaming the output and carrying the input are later steps. + +### 6. What is shared lives in the SDK + +The socket's path, the program's request and answer, and the request on the bus with its subject are one package +of the SDK (`mesh-sdk`), imported by `mesh-cli`, the node-engine and the controller. None of them keeps its own +copy. The list of what is terminal-only is not shared: it stays in the controller, its one judge, and `mesh-cli` +shows the controller's words. + +### 7. In what order it is built + +1. **Phase 1.** The SDK package; the node-engine's socket and its request; the controller's answer, with the + control-node as the terminal for its operator account; the `mesh-cli` module on every node. From then on the + operator types `nox …` on the control-node, and gets the ordinary calls on every other node. +2. **Phase 2.** The agent-account route of §4 on a node other than the control-node, once ADR 0266 runs there + (on the operator's own machines after the follow-up decision ADR 0266 names). +3. **Later.** Streamed output and standard input; a second factor for a node whose agents must share the + operator's account. + +## Consequences + +- The operator has one command on every node, built, checked and moved like every other module, and the records' + "at the controller's terminal" names something that exists: `mesh-cli` (or `nox`) on the control-node. +- A terminal-only change typed on any other node is refused there with the reason, instead of being impossible to + type at all. +- No agent on the control-node gains anything it did not have; once ADR 0266 is switched on there, agents lose the + terminal entirely. +- **Same-account limits stay ADR 0266's boundary.** While an agent runs as the operator, it can do what the + operator's account can: put a `mesh-cli` of its own earlier on `PATH` (the zsh module puts the account's + `~/.local/bin` first), or redefine the alias outside the mesh's block. On a node where agents run as the + operator, the operator's account is not the terminal, so this reaches nothing terminal-only there. +- The alias hides any other `nox` in interactive zsh (Arch's `python-nox` installs `/usr/bin/nox`); + `command nox` still reaches it. +- The node-engine gains a listener every local account may reach. It accepts one line per connection, bounded in + size and in time, and runs nothing itself. +- The controller's own binary is still never on `PATH`. A record that says to type + `mesh-controller ` at the terminal means `mesh-cli ` on the control-node from this record on. + +## How it is checked + +- **The SDK:** the request and the answer survive a round trip, and the subject names the node. +- **The node-engine:** a test connects to the socket as the test's own process and the request carries that + process's user id and account; a connection whose credentials cannot be read is refused, never sent as + anyone; a line over the bound is refused. +- **The controller:** tests hold that a line from the control-node's operator account runs without `MESH_VERB`; + that a line from any other account, or from another node without a judged agent account, runs with + `MESH_VERB=mesh-cli` and a terminal-only change is refused with the reason; that a judgement older than + fifteen minutes is not one; and that an ordinary line meets every refusal of the `command` verb. +- **The catalogue:** the module's check passes, its archive places one file, and its shell contribution is the + alias. +- **Live, at acceptance (the operator's view):** on the control-node, in the operator's own shell, + `nox settings show zsh --node ` answers as the terminal, and a terminal-only `nox settings set …` is taken. + On another node, the same `settings set` is refused and says why. From `agent`, once that account exists, it is + refused too. + +## References + +- [Issue 343](../04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md): the gap. +- [Issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md), + [issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md): what is + terminal-only, and how a verb is told from the terminal. +- [ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md): the operator account. +- [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md): shell contributions. +- [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md): + a module builds from its own source. +- [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md): proof and assurance. +- ADR 0266 (hq pull request 225) and ADR 0259 (hq pull request 211): not yet merged, so named without links. diff --git a/04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md b/04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md new file mode 100644 index 00000000..34948b33 --- /dev/null +++ b/04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md @@ -0,0 +1,62 @@ +--- +status: located +opened: 2026-10-09 +located-in: [mesh-catalog (a new module, mesh-cli), mesh-host (a local socket the node-engine serves), mesh-controller (the answer on the bus), mesh-sdk (the shared request)] +fixed-by: +amended-design: +--- + +# 343. The controller's terminal has no command + +## Symptom + +On 2026-10-09 the operator, on the control-node in their own shell, typed a `mesh-controller` command line to make +a change the records say is made at the controller's terminal. The shell answered: + + zsh: command not found: mesh-controller + +The same day, read through the mesh's tools: the controller runs on the control-node as the unit +`mesh-controller.service`, which the mesh declared and wrote (the control-node's `node-service-manager.status`: active, running, +`FragmentPath` `/etc/systemd/system/mesh-controller.service`, `mesh_declared` true). + +## Why it is an issue + +**The records name a command that does not exist.** Issue 339, issue 340, ADR 0266 (hq pull request 225, not yet +merged) and the agent module's README say that certain changes are made "at the controller's terminal" alone, and +some give the line to type, such as `mesh-controller settings set claude-code ''` and +`mesh-controller node agent-account `. None of them says how the operator reaches that terminal, and +nothing in the mesh provides it: + +- The controller is a mesh process. The node-engine unpacks its bundle under the mesh's own directory and writes a + unit that runs it as the `mesh-controller` account, with an environment composed from the controller's module: + the files holding its stores' credentials, under a directory only that account may enter, and the ports of the + store and the bus as the mesh resolved them. No module puts a command on any `PATH`. +- Run by hand, the binary needs that account and that environment, rebuilt by hand under `sudo`. That is a + work-around, and the operator rejects it. +- In the controller's code the terminal is only a negative: a command line that did not come through a verb (no + `MESH_VERB`). Nothing says who the terminal is. + +**Impact on the operator.** Every terminal-only change waits: the agent module's managed settings, tool servers and +shared text (issue 340's steps), the places and accesses of issue 339, and a node's agent account (ADR 0266). The +refusals the controller gives through a verb send the operator to a terminal they cannot open. + +## Acceptance test (the operator's view) + +On the control-node, in my own shell and without `sudo`, I type `nox settings set zsh '{"execute":"withhold"}' +--node ` (or any other command line) and it runs against the live controller as the terminal. On any other +node the same terminal-only line is refused and says why. An agent session cannot use it to get round a +terminal-only refusal: as `agent` on the control-node it is refused, and on a node where agents run as the operator +that account is not the terminal. + +## The fix + +[ADR 0272](../../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md) +(proposed): a module `mesh-cli` on every node places `/usr/local/bin/mesh-cli`, with the alias `nox` in +interactive zsh. It asks the node-engine over a local socket; the node-engine reads the account from the kernel and +asks the controller on its own node's subject; the controller runs the line as the terminal for the control-node's +operator account, and as an ordinary call for every other account and node. The diagnosis says what was read and +what each repository does. + +## How it will be checked + +The checks ADR 0272 lists, and the acceptance test above, run live on the control-node and on one other node. diff --git a/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md new file mode 100644 index 00000000..22e038ec --- /dev/null +++ b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md @@ -0,0 +1,75 @@ +# 343 — Diagnosis + +## 2026-10-09: what was read + +The record was searched first: `records_search` for "command not found mesh-controller" found nothing, and for +"controller's terminal" found issue 340 and research 039, which use the words and say nothing of how the terminal +is reached. + +### The controller + +- Its module declares one process, run as the `mesh-controller` account, `./mesh-controller serve`, with an + environment of credential files under its own directory (mode 0700, owned by that account) and the ports of the + store and the bus as `${seat:…}` references the controller resolves at composition. +- `throughAVerb` (`cmd/mesh-controller/modules.go`) is the whole of the terminal: a command line runs as the + terminal when `MESH_VERB` is not in its environment. `runVerb` (`cmd/mesh-controller/seatverbs.go`) sets it for + every line a verb runs, the named verbs and the generic `command` alike, and runs the line in a fresh process of + the controller's own binary with the serving process's environment. +- The generic `command` verb runs any command line today, except `settings set` and `settings clear` and a repair + without `--why`. ADR 0266 §7 makes it a list of reads; that is not built. + +### The node-engine + +- It writes a process's unit under `/etc/systemd/system`, and unpacks its bundle into + `/var/lib/mesh/daemons/`. A witnessed process's running build is moved aside when the next is placed, so + the path holds the build the unit runs, not always the build that is running (issue 289). +- It runs as root, and holds one bus account per node, granted to publish under `mesh.control..` and + nothing of another node's. It knows nothing of the node's operator account; the controller does (the node + record's `account`). +- An `archive` resource unpacks a bundle into a directory, moves its files in one by one where the directory was + not made by the mesh, refuses to write over a file it did not place, and on removal takes out only what it + placed. The node-engine's own module places its versions this way. +- No part of the mesh serves a local socket that reads a peer's credentials. + +### The bus + +- The controller subscribes `mesh.control.>` and may answer a request. The CONTROL stream names its subjects + (`mesh.control.*.report`, `mesh.control.enrol`), so a request on another subject under the prefix is not + persisted. + +## Ruled out + +- **A link to the binary on `PATH`.** The mesh creates no symbolic links, and the binary needs the unit's account + and environment. +- **A wrapper the controller's module places.** Designed first, on 2026-10-09: a file under `/usr/local/bin` + that runs the binary through `sudo` with the unit's environment, or a new process field from which the + node-engine writes one. Rejected by the operator: it is a work-around, it is a second copy of the unit's + environment, it reaches the control-node only, and its proof is `sudo`, which agents on the control-node hold. +- **The program as a second artifact of the controller's module.** That module is on the control-node only. + +## Located + +Four repositories, in the order they merge: + +1. **mesh-sdk:** the socket's path, the program's request and answer, the bus request and its subject. +2. **`mesh-host`:** the node-engine serves the socket, reads `SO_PEERCRED`, names the account, and asks the + controller on `mesh.control..cli`. +3. **mesh-controller:** the serving controller answers `mesh.control.*.cli`, judges the terminal (ADR 0272 §4), + and runs the line through `runVerb`'s path, without `MESH_VERB` for the terminal and with + `MESH_VERB=mesh-cli` otherwise, after the `command` verb's refusals. +4. **mesh-catalog:** the module `mesh-cli` (its program, its archive into `/usr/local/bin`, its zsh alias `nox`), + assigned to every node; and the agent module's README, whose `mesh-controller settings set …` becomes + `mesh-cli settings set …`. + +Each code change is a branch and a pull request, merged by the operator after ADR 0272 is accepted. The records +that tell the operator to type `mesh-controller ` at the terminal mean `mesh-cli ` on the +control-node once phase 1 is live; ADR 0266's `mesh-controller node agent-account` line is changed on its own +branch, which is not yet merged. + +## Later + +- Phase 2 of ADR 0272: a node other than the control-node becomes the terminal for its operator account once its + agents run under an account of their own (ADR 0266), judged. +- A node whose agents must share the operator's account (the operator's own machines) needs proof that it is the + operator: a second factor, or an approval on a verified channel (ADR 0234, ADR 0259, research 039). Not built + here. -- 2.54.0 From 1ad9959b16c812c3dfe06dc10a53a99b7ba2a7f2 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 12:01:45 +0200 Subject: [PATCH 2/9] ADR 0272: say how the socket keeps ADR 0004, answer the operator and agent accounts only, and keep the bus request out of the SDK (ADR 0039) --- ...nly-where-no-agent-runs-as-its-operator.md | 60 +++++++++++++------ .../01-diagnosis.md | 4 +- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md index 172377fd..f6e0b67d 100644 --- a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -105,7 +105,7 @@ only: a script, `sudo`, a cron line and any document meant to work everywhere sa `mesh-cli ` takes the controller's own command line, in the words the controller's commands and their refusals already use: `mesh-cli settings set zsh '{"execute":"withhold"}' --node ` is `settings set zsh … --node ` asked of the controller. It is run as the operator's own account, without -`sudo`: root is not the operator's account, and is answered as any other account (§4). +`sudo`: root is not the operator's account, and is refused (§4). `mesh-cli` holds no credential and reads no file of the mesh. It does one thing: it asks the node-engine on its own machine over a local socket, and prints what comes back. A process that carries `MESH_VERB` (a verb's own process) @@ -129,6 +129,13 @@ The node-engine serves a socket every local account may connect to. For each con The node-engine is trusted for its own node. Root on a node could claim any account there; root on a node that is the terminal is already the operator's, and §4 makes no other node the terminal. +**This is no listening control surface** in [ADR 0004](0004-a-node-and-how-it-joins.md)'s sense, and that record's +rule stands. The socket is a file under the node-engine's runtime directory with no network address, so nothing +dials it from another machine. Nothing sent to it acts on the node: the node-engine runs nothing it receives and +changes nothing; it adds what the kernel says and sends the request out over its own link, to a controller that +judges it like any other caller. Only the node's operator account and its agent account are answered, and both +already reach the mesh's verbs through the mesh MCP server (§4). + ### 4. The controller judges, and runs the line A line from node N, vouched as account A, **runs as the controller's terminal** when A is N's operator account @@ -141,11 +148,16 @@ A line from node N, vouched as account A, **runs as the controller's terminal** The controller runs it as it runs a verb's line, in a fresh process of its own binary with its own environment, but without `MESH_VERB`, and records the caller as *A through mesh-cli on N*. -**Every other line is an ordinary call.** It goes through exactly what the `command` verb does with a line: its -refusals today, and its list of reads once ADR 0266 §7 builds one. It runs with `MESH_VERB=mesh-cli`, so every -terminal-only change is refused with the controller's own reason. The answer adds one sentence saying why this -account on this node is not the terminal: an agent account, root, an account that is not the operator's, or a node -whose agents may run as its operator. +**A line from the node's operator account or its agent account that is not the terminal is an ordinary +call.** It goes through exactly what the `command` verb does with a line: its refusals today, and its list of +reads once ADR 0266 §7 builds one. It runs with `MESH_VERB=mesh-cli`, so every terminal-only change is refused +with the controller's own reason. The answer adds one sentence saying why this account on this node is not the +terminal: it is the agent account, or the node's agents may run as its operator. These two accounts already +reach the mesh's verbs through the mesh MCP server, so this widens nothing. + +**A line from any other account is refused**, root and every service account included, and the answer says +which account asked and that `mesh-cli` answers the node's operator and agent accounts only. Run as yourself, +not under `sudo`. **The control-node rule is no wider than today.** On the control-node the operator's account holds `sudo`, and until ADR 0266 is switched on so do the agents running as it: either could run the controller's binary as root @@ -159,18 +171,25 @@ and why or why not. `mesh-cli` prints the two streams and exits with the status. message is cut there and the cut is said; it is never silently short. Standard input is not carried: a command that reads it gets nothing and fails saying so. Streaming the output and carrying the input are later steps. -### 6. What is shared lives in the SDK +### 6. What is shared, and where it lives -The socket's path, the program's request and answer, and the request on the bus with its subject are one package -of the SDK (`mesh-sdk`), imported by `mesh-cli`, the node-engine and the controller. None of them keeps its own -copy. The list of what is terminal-only is not shared: it stays in the controller, its one judge, and `mesh-cli` -shows the controller's words. +- **The local protocol is the SDK's.** The socket's path and the program's request and answer are one package of + the Go SDK (`mesh-sdk`), imported by `mesh-cli` and by the node-engine. It is a local protocol a module's + program speaks to its node's runtime, which is what the SDK holds + ([ADR 0039](0039-what-the-sdk-holds-and-refuses.md), as [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md) + reads it). +- **The bus request is the node-engine's and the controller's**, in their own link code, with a test on each side + holding the field names, as for the engine's health statement. A transport lives in no SDK (ADR 0188's note on + ADR 0039). +- **The list of what is terminal-only is not shared.** It stays in the controller, its one judge, and `mesh-cli` + shows the controller's words. ### 7. In what order it is built 1. **Phase 1.** The SDK package; the node-engine's socket and its request; the controller's answer, with the control-node as the terminal for its operator account; the `mesh-cli` module on every node. From then on the - operator types `nox …` on the control-node, and gets the ordinary calls on every other node. + operator types `nox …` on the control-node, and gets the ordinary calls on every other node. An agent account + is answered as soon as a node names one (ADR 0266 §1), whichever phase is live. 2. **Phase 2.** The agent-account route of §4 on a node other than the control-node, once ADR 0266 runs there (on the operator's own machines after the follow-up decision ADR 0266 names). 3. **Later.** Streamed output and standard input; a second factor for a node whose agents must share the @@ -190,21 +209,24 @@ shows the controller's words. operator, the operator's account is not the terminal, so this reaches nothing terminal-only there. - The alias hides any other `nox` in interactive zsh (Arch's `python-nox` installs `/usr/bin/nox`); `command nox` still reaches it. -- The node-engine gains a listener every local account may reach. It accepts one line per connection, bounded in - size and in time, and runs nothing itself. +- The node-engine gains a local socket every account on the machine may reach. It accepts one line per + connection, bounded in size and in time, runs nothing itself, and is never reachable from another machine + (ADR 0004 stands, §3). - The controller's own binary is still never on `PATH`. A record that says to type `mesh-controller ` at the terminal means `mesh-cli ` on the control-node from this record on. ## How it is checked -- **The SDK:** the request and the answer survive a round trip, and the subject names the node. +- **The SDK:** the request and the answer survive a round trip, and a request over the bound is refused. +- **The bus request:** a test in the node-engine and one in the controller hold the same field names. - **The node-engine:** a test connects to the socket as the test's own process and the request carries that process's user id and account; a connection whose credentials cannot be read is refused, never sent as anyone; a line over the bound is refused. - **The controller:** tests hold that a line from the control-node's operator account runs without `MESH_VERB`; - that a line from any other account, or from another node without a judged agent account, runs with - `MESH_VERB=mesh-cli` and a terminal-only change is refused with the reason; that a judgement older than - fifteen minutes is not one; and that an ordinary line meets every refusal of the `command` verb. + that a line from the agent account, or from another node's operator account without a judged agent account, + runs with `MESH_VERB=mesh-cli` and a terminal-only change is refused with the reason; that a line from any + other account, root included, is refused and runs nothing; that a judgement older than fifteen minutes is not + one; and that an ordinary line meets every refusal of the `command` verb. - **The catalogue:** the module's check passes, its archive places one file, and its shell contribution is the alias. - **Live, at acceptance (the operator's view):** on the control-node, in the operator's own shell, @@ -218,7 +240,9 @@ shows the controller's words. - [Issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md), [issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md): what is terminal-only, and how a verb is told from the terminal. +- [ADR 0004](0004-a-node-and-how-it-joins.md): a node has no listening control surface. - [ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md): the operator account. +- [ADR 0039](0039-what-the-sdk-holds-and-refuses.md), [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md): what the SDK holds. - [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md): shell contributions. - [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md): a module builds from its own source. diff --git a/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md index 22e038ec..24c039fb 100644 --- a/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md +++ b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md @@ -51,9 +51,9 @@ is reached. Four repositories, in the order they merge: -1. **mesh-sdk:** the socket's path, the program's request and answer, the bus request and its subject. +1. **mesh-sdk:** the socket's path and the program's request and answer, the local protocol (ADR 0039). 2. **`mesh-host`:** the node-engine serves the socket, reads `SO_PEERCRED`, names the account, and asks the - controller on `mesh.control..cli`. + controller on `mesh.control..cli`; the bus request is its own link code. 3. **mesh-controller:** the serving controller answers `mesh.control.*.cli`, judges the terminal (ADR 0272 §4), and runs the line through `runVerb`'s path, without `MESH_VERB` for the terminal and with `MESH_VERB=mesh-cli` otherwise, after the `command` verb's refusals. -- 2.54.0 From 322f3aec363dbb3caa26eb9c12bb971373212e76 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 12:11:25 +0200 Subject: [PATCH 3/9] ADR 0272: a path in a line is read where the controller runs --- ...-is-the-terminal-only-where-no-agent-runs-as-its-operator.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md index f6e0b67d..65089322 100644 --- a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -170,6 +170,8 @@ The answer is the command's standard output, its standard error, its exit status and why or why not. `mesh-cli` prints the two streams and exits with the status. An answer larger than one bus message is cut there and the cut is said; it is never silently short. Standard input is not carried: a command that reads it gets nothing and fails saying so. Streaming the output and carrying the input are later steps. +A path in a line names a file where the controller runs, read by the controller's account, never a file beside +`mesh-cli`: settings are given inline, `"$(cat settings.json)"` in the operator's own shell. ### 6. What is shared, and where it lives -- 2.54.0 From 57bb4dab747a77faf0586bfcbe53850e3816dfaf Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 12:12:23 +0200 Subject: [PATCH 4/9] Issue 343: phase 1 of ADR 0272 is open as four pull requests --- .../01-diagnosis.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md index 24c039fb..8b2ddaff 100644 --- a/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md +++ b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md @@ -73,3 +73,10 @@ branch, which is not yet merged. - A node whose agents must share the operator's account (the operator's own machines) needs proof that it is the operator: a second factor, or an approval on a verified channel (ADR 0234, ADR 0259, research 039). Not built here. + +## 2026-10-09: phase 1, opened + +Phase 1 of ADR 0272 is open as four pull requests, to merge in this order once the record is accepted: mesh-sdk +pull request 14 (`go/cli`), mesh-host pull request 69 (the engine's socket and request), mesh-controller pull +request 176 (the answer and the judge), mesh-catalog pull request 156 (the `mesh-cli` module, and the agent +module's and the route adapter's texts). After the last, the operator assigns `mesh-cli` to every node. -- 2.54.0 From e143f6b193a477b0a9760a79f72e19c8abe9eb4b Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 12:12:34 +0200 Subject: [PATCH 5/9] Issue 343: name the node-engine's repository as an identifier --- .../343-the-controllers-terminal-has-no-command/01-diagnosis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md index 8b2ddaff..9c1147f4 100644 --- a/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md +++ b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md @@ -77,6 +77,6 @@ branch, which is not yet merged. ## 2026-10-09: phase 1, opened Phase 1 of ADR 0272 is open as four pull requests, to merge in this order once the record is accepted: mesh-sdk -pull request 14 (`go/cli`), mesh-host pull request 69 (the engine's socket and request), mesh-controller pull +pull request 14 (`go/cli`), `mesh-host` pull request 69 (the engine's socket and request), mesh-controller pull request 176 (the answer and the judge), mesh-catalog pull request 156 (the `mesh-cli` module, and the agent module's and the route adapter's texts). After the last, the operator assigns `mesh-cli` to every node. -- 2.54.0 From 41060224e6760e81b605e6b6eb788e5effd5630a Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 12:13:03 +0200 Subject: [PATCH 6/9] ADR 0272: the agent account's judgement comes from the engine's statement, never from the request --- ...e-terminal-only-where-no-agent-runs-as-its-operator.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md index 65089322..0b53b968 100644 --- a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -121,11 +121,13 @@ The node-engine serves a socket every local account may connect to. For each con core NATS. Only that node's engine may publish under `mesh.control..`: its bus account is granted that prefix and nothing of any other node's, so **the node is a fact the bus server enforces and the account is a fact the kernel gives**. The CONTROL stream names its own subjects, so the request is never persisted; -3. says with it whether the node's declaration names an agent account, and the engine's latest judgement of it - (ADR 0266 §3); -4. answers the program with what the controller answered, or says that the controller did not answer within its +3. answers the program with what the controller answered, or says that the controller did not answer within its call bound. +Whether the node's agents run under an account of their own is not in the request. The node-engine already +says it: its judgement of the agent account is in its health statement (ADR 0266 §3, §4), and the controller reads +its own record of that statement, so a request cannot claim a judgement. + The node-engine is trusted for its own node. Root on a node could claim any account there; root on a node that is the terminal is already the operator's, and §4 makes no other node the terminal. -- 2.54.0 From bac7feca77aba23841ea6ffcfdf19657fe8c3d0d Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 12:56:44 +0200 Subject: [PATCH 7/9] =?UTF-8?q?ADR=200272:=20the=20review's=20answers=20?= =?UTF-8?q?=E2=80=94=20every=20line=20a=20call,=20the=20socket=20grants=20?= =?UTF-8?q?nothing,=20the=20terminal=20does=20not=20follow=20assign,=20eac?= =?UTF-8?q?h=20claim=20and=20how=20it=20is=20checked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §3: the node-engine vouches and decides nothing; deadline first, other namespaces refused, each account bounded and answered busy; the subject is the machine's own in the writers table. - §4: phase 1 refuses the agent account (phase 2 answers it); an ordinary settings line takes the settings verb's path; the controller's module is assigned at the terminal only, so the terminal does not follow assign; 'no wider than today' holds only while its two measured facts hold. - §5: every line is a call, followed to its answer; 'nothing ran' only of a line nobody took, otherwise 'not known' with the call; the answer is its asker's alone. - §7: after the four merge, the operator assigns mesh-cli to every node. - How it is checked: each claim with its test or measurement, the MESH_VERB refusal in mesh-cli named a courtesy, and the sudo and agent-account facts read on the control-node today. --- ...nly-where-no-agent-runs-as-its-operator.md | 167 ++++++++++++------ 1 file changed, 112 insertions(+), 55 deletions(-) diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md index 0b53b968..f5766bcd 100644 --- a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -109,20 +109,28 @@ their refusals already use: `mesh-cli settings set zsh '{"execute":"withhold"}' `mesh-cli` holds no credential and reads no file of the mesh. It does one thing: it asks the node-engine on its own machine over a local socket, and prints what comes back. A process that carries `MESH_VERB` (a verb's own process) -is refused at once, and told why: a verb is never a person. +is refused at once, and told why. **That refusal is a courtesy, not the gate**: a process can unset a variable. +The gate is the controller's judgement of the account the node-engine vouches for (§4). -### 3. The node-engine vouches for the account +### 3. The node-engine vouches for the account; the socket grants nothing -The node-engine serves a socket every local account may connect to. For each connection it: +**The socket grants nothing; the controller judges.** What the node-engine adds is a fact only it can read, the +asking account, and it decides nothing with it. The node-engine serves a socket every local account may connect +to. For each connection it: -1. reads the connecting process's user id from the kernel (`SO_PEERCRED`), and names the account from the - machine's user database; -2. sends the line, the account and the user id to the controller on `mesh.control..cli`, as a request on - core NATS. Only that node's engine may publish under `mesh.control..`: its bus account is granted that - prefix and nothing of any other node's, so **the node is a fact the bus server enforces and the account is a - fact the kernel gives**. The CONTROL stream names its own subjects, so the request is never persisted; -3. answers the program with what the controller answered, or says that the controller did not answer within its - call bound. +1. sets the connection's deadline before anything else, and reads the connecting process's user id and process id + from the kernel (`SO_PEERCRED`); +2. refuses a process whose mount or user namespace is not the engine's own: a container running with the + operator's user id shares the number, not the machine; +3. takes one of at most two lines at once for that user id and eight for the machine, and answers *busy* at once + when there is none, so one account cannot hold the socket from another and accepting never waits; +4. names the account from the machine's user database, reads the line, and sends the line, the account and the + user id to the controller on `mesh.control..cli`, as a request on core NATS. Only that node's engine may + publish under `mesh.control..`: its bus account is granted that prefix and nothing of any other node's, + and the writers table names the subject as the machine's own, so **the node is a fact the bus server enforces + and the account is a fact the kernel gives**. The CONTROL stream names its own subjects, so the request is + never persisted; +5. follows the line to what came of it (§5) and answers the program. Whether the node's agents run under an account of their own is not in the request. The node-engine already says it: its judgement of the agent account is in its health statement (ADR 0266 §3, §4), and the controller reads @@ -135,45 +143,64 @@ the terminal is already the operator's, and §4 makes no other node the terminal rule stands. The socket is a file under the node-engine's runtime directory with no network address, so nothing dials it from another machine. Nothing sent to it acts on the node: the node-engine runs nothing it receives and changes nothing; it adds what the kernel says and sends the request out over its own link, to a controller that -judges it like any other caller. Only the node's operator account and its agent account are answered, and both -already reach the mesh's verbs through the mesh MCP server (§4). +judges it like any other caller. Only the node's operator account is answered in phase 1 (§4), and it already +reaches the mesh's verbs through the mesh MCP server. ### 4. The controller judges, and runs the line A line from node N, vouched as account A, **runs as the controller's terminal** when A is N's operator account ([ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)) and either: -- N is the control-node; or -- N names an agent account of its own (ADR 0266 §1), and the controller's own record of the engine's judgement, - heard within ADR 0266 §4's fifteen minutes, says it cannot become root without a person. +- N is the control-node: the one node the controller's module is assigned to. Two or more, and nobody is the + terminal; or +- (phase 2) N names an agent account of its own (ADR 0266 §1), and the controller's own record of the engine's + judgement, heard within ADR 0266 §4's fifteen minutes, says it cannot become root without a person. The controller runs it as it runs a verb's line, in a fresh process of its own binary with its own environment, but without `MESH_VERB`, and records the caller as *A through mesh-cli on N*. -**A line from the node's operator account or its agent account that is not the terminal is an ordinary -call.** It goes through exactly what the `command` verb does with a line: its refusals today, and its list of +**Which node is the control-node does not follow a verb.** Whoever chooses where the controller's module runs +chooses the terminal, so the module is assigned and unassigned at the controller's terminal only; `assign` and +`unassign` naming it through a verb are refused with the reason. + +**A line from N's operator account that is not the terminal is an ordinary call.** A `settings` line is composed as +the settings verb composes its own and meets that verb's refusals, the terminal-only keys among them; any other +line goes through exactly what the generic `command` verb does with a line: its refusals today, and its list of reads once ADR 0266 §7 builds one. It runs with `MESH_VERB=mesh-cli`, so every terminal-only change is refused with the controller's own reason. The answer adds one sentence saying why this account on this node is not the -terminal: it is the agent account, or the node's agents may run as its operator. These two accounts already -reach the mesh's verbs through the mesh MCP server, so this widens nothing. +terminal: the node's agents may run as its operator. That account already reaches the mesh's verbs through the +mesh MCP server, so this widens nothing. -**A line from any other account is refused**, root and every service account included, and the answer says -which account asked and that `mesh-cli` answers the node's operator and agent accounts only. Run as yourself, -not under `sudo`. +**A line from any other account is refused, and runs nothing**: root, every service account, and **in phase 1 the +agent account too**. The answer says which account asked and that `mesh-cli` answers the node's operator account. +Phase 2 answers a node's agent account as an ordinary call (§7). -**The control-node rule is no wider than today.** On the control-node the operator's account holds `sudo`, and -until ADR 0266 is switched on so do the agents running as it: either could run the controller's binary as root -with its environment. `mesh-cli` gives that account the same reach and no more. Once agents there run as `agent`, -`agent` is never the terminal. +**The control-node rule is no wider than today, while today lasts.** On the control-node the operator's account +holds passwordless `sudo`, and until ADR 0266 is switched on agents run as that account: either could run the +controller's binary as root with its environment, which is the terminal. `mesh-cli` gives that account the same +reach and no more. Once agents there run as `agent`, `agent` is refused. The claim rests on two measured facts and +fails if one changes alone: if the operator's account lost its passwordless `sudo` while agents still ran as it, +`mesh-cli` would give those agents a terminal they no longer had (§ How it is checked). ### 5. What comes back +**Every line is a call**, run, recorded and answered as a seat's verb is: the bus lets a request be answered for a +minute only, so a line answered when it ended lost every answer after that and was said as nothing having run. The +controller answers within ten seconds that a line is still running, with its call; the node-engine follows the +call, as the same account on the same node, until the line ends. The call's record keeps the line's command word +and who asked, never the rest of the line, and the controller's journal says each line, its call, who asked where +and how it ran. **The answer is its asker's alone**: kept in the memory of the controller that ran it for the asker +to follow, never on the bus, and never shown by `calls`, which answers anyone who may call the seat. Lines running +at once are bounded (eight); one over is answered busy, and nothing runs. + The answer is the command's standard output, its standard error, its exit status, whether it ran as the terminal, -and why or why not. `mesh-cli` prints the two streams and exits with the status. An answer larger than one bus -message is cut there and the cut is said; it is never silently short. Standard input is not carried: a command -that reads it gets nothing and fails saying so. Streaming the output and carrying the input are later steps. -A path in a line names a file where the controller runs, read by the controller's account, never a file beside -`mesh-cli`: settings are given inline, `"$(cat settings.json)"` in the operator's own shell. +and why or why not. `mesh-cli` prints the two streams and exits with the status. It says one of three things, +never confused: what the line printed; that **nothing ran**, and why, only of a line no controller took; or that +**what came of it is not known** — no answer in time, so it may have run — naming the call to look at. An answer +larger than one bus message is cut there and the cut is said; it is never silently short. Standard input is not +carried: a command that reads it gets nothing and fails saying so. Streaming the output and carrying the input +are later steps. A path in a line names a file where the controller runs, read by the controller's account, never +a file beside `mesh-cli`: settings are given inline, `"$(cat settings.json)"` in the operator's own shell. ### 6. What is shared, and where it lives @@ -191,11 +218,13 @@ A path in a line names a file where the controller runs, read by the controller' ### 7. In what order it is built 1. **Phase 1.** The SDK package; the node-engine's socket and its request; the controller's answer, with the - control-node as the terminal for its operator account; the `mesh-cli` module on every node. From then on the - operator types `nox …` on the control-node, and gets the ordinary calls on every other node. An agent account - is answered as soon as a node names one (ADR 0266 §1), whichever phase is live. -2. **Phase 2.** The agent-account route of §4 on a node other than the control-node, once ADR 0266 runs there - (on the operator's own machines after the follow-up decision ADR 0266 names). + control-node as the terminal for its operator account; the `mesh-cli` module. **After the four merge, the + operator assigns `mesh-cli` to every node** at the terminal; until then it is on no node. From then on the + operator types `nox …` on the control-node, and gets the ordinary calls on every other node. The agent account + is refused. +2. **Phase 2.** The agent account: answered as an ordinary call on a node that names one (ADR 0266 §1), and the + agent-account route of §4 to the terminal on a node other than the control-node, once ADR 0266 runs there (on + the operator's own machines after the follow-up decision ADR 0266 names). Its tests are written with it. 3. **Later.** Streamed output and standard input; a second factor for a node whose agents must share the operator's account. @@ -214,29 +243,57 @@ A path in a line names a file where the controller runs, read by the controller' - The alias hides any other `nox` in interactive zsh (Arch's `python-nox` installs `/usr/bin/nox`); `command nox` still reaches it. - The node-engine gains a local socket every account on the machine may reach. It accepts one line per - connection, bounded in size and in time, runs nothing itself, and is never reachable from another machine - (ADR 0004 stands, §3). + connection, bounded in size, in time and per account, runs nothing itself, and is never reachable from another + machine (ADR 0004 stands, §3). +- Assigning the controller's module moves to the terminal: an agent, or any caller of `assign`, can no longer move + the controller, and so the terminal, to a node of its choosing. - The controller's own binary is still never on `PATH`. A record that says to type `mesh-controller ` at the terminal means `mesh-cli ` on the control-node from this record on. ## How it is checked -- **The SDK:** the request and the answer survive a round trip, and a request over the bound is refused. -- **The bus request:** a test in the node-engine and one in the controller hold the same field names. -- **The node-engine:** a test connects to the socket as the test's own process and the request carries that - process's user id and account; a connection whose credentials cannot be read is refused, never sent as - anyone; a line over the bound is refused. -- **The controller:** tests hold that a line from the control-node's operator account runs without `MESH_VERB`; - that a line from the agent account, or from another node's operator account without a judged agent account, - runs with `MESH_VERB=mesh-cli` and a terminal-only change is refused with the reason; that a line from any - other account, root included, is refused and runs nothing; that a judgement older than fifteen minutes is not - one; and that an ordinary line meets every refusal of the `command` verb. -- **The catalogue:** the module's check passes, its archive places one file, and its shell contribution is the - alias. -- **Live, at acceptance (the operator's view):** on the control-node, in the operator's own shell, - `nox settings show zsh --node ` answers as the terminal, and a terminal-only `nox settings set …` is taken. - On another node, the same `settings set` is refused and says why. From `agent`, once that account exists, it is - refused too. +Each claim, and what holds it: + +- **The account is the kernel's** (§3): the node-engine's test connects as the test's own process and the request + carries that process's user id and account; credentials that cannot be read are refused and never sent as + anyone (mesh-host `internal/meshcli`). +- **The socket grants nothing; the controller judges** (§3, §4): the node-engine's package for it runs no command + and changes no file — its one act is the forward, which review holds, and its tests hold every refusal before + it. The controller's judge is tested case by case: the control-node's operator + account runs without `MESH_VERB` (the test reads the variable in the process it ran); another node's operator + account runs with `MESH_VERB=mesh-cli`; every other account, root and the agent account included, is refused + and runs nothing; two control-nodes make nobody the terminal (mesh-controller `cmd/mesh-controller`, the + mesh-cli tests). +- **The node is the bus server's fact**: the writers table names `mesh.control.*.cli` as the machine's own, and + its test refuses another machine or a module granted it (mesh-controller `internal/broker`). +- **Another namespace is refused, and one account cannot hold the socket**: the node-engine's tests refuse a peer + whose namespaces differ, and answer a second line of a busy account *busy* at once while the first is still + asked. +- **An ordinary `settings` line takes the settings verb's path**: the controller's test composes each form as the + verb does and refuses any other; an ordinary repair without `--why` meets the generic verb's refusal. +- **The control-node does not follow a verb**: the controller's test refuses assigning or unassigning its module + with `MESH_VERB` set, and lets the terminal do it. +- **A line is never lost, and never said as nothing having run when it may have**: the controller's test follows a + line that outlasts the answer window to its answer, over a bus, and refuses a follow by another account or + node; the node-engine's tests follow a running line to its answer, say *not known* with the call when no + answer comes, and say *nothing ran* only of a request nobody took. A line's record keeps neither its line's + values nor its answer on the bus, and `calls` never shows its answer (mesh-controller `internal/link`). +- **The `MESH_VERB` refusal in `mesh-cli` is a courtesy**: its test holds that the program refuses before asking; + nothing rests on it, since the controller judges the vouched account whatever the program does. +- **The module places one file**: the catalogue's test holds `mesh-cli` to one archive resource unpacking one + compiled Go bundle into `/usr/local/bin`; its shell contribution is the alias. +- **No wider than today, while today lasts** (§4), measured on the control-node, not assumed: the `sudo` module's + own check lists the operator account's rules (on 2026-10-08 it found every command as root without a password, + ADR 0266's context), and `mesh-controller.node` for the control-node shows whether an agent account is named. + Both are read before phase 1 is delivered and said in its pull request. Read on 2026-10-09: the `sudo` module's + `sudo_rules` on the control-node answered `passwordless_all: true` for the operator account (two rules, every + command as root, no password), and `mesh-controller.node` named no agent account there. Neither is checked by the controller at + each line: if the operator account's passwordless `sudo` goes while agents still run as it, this claim is false, + and the remedy is ADR 0266's agent account, not a check here. +- **Live, at acceptance (the operator's view):** after `mesh-cli` is assigned to every node, on the control-node, + in the operator's own shell, `nox settings show zsh --node ` answers as the terminal, and a terminal-only + `nox settings set …` is taken. On another node, the same `settings set` is refused and says why. A line that + runs longer than a minute (`nox push `) is followed to its answer. ## References -- 2.54.0 From d3cb9fedf4286284b99571740f39608aac1d3314 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 13:02:45 +0200 Subject: [PATCH 8/9] ADR 0272 accepted (the operator, 2026-10-09) --- ...-is-the-terminal-only-where-no-agent-runs-as-its-operator.md | 2 +- .../343-the-controllers-terminal-has-no-command/00-report.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md index f5766bcd..7e7c5d68 100644 --- a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -1,6 +1,6 @@ --- topic: the tiers -status: proposed +status: accepted date: 2026-10-09 deciders: jochen reconstructed: false diff --git a/04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md b/04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md index 34948b33..622325b3 100644 --- a/04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md +++ b/04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md @@ -51,7 +51,7 @@ that account is not the terminal. ## The fix [ADR 0272](../../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md) -(proposed): a module `mesh-cli` on every node places `/usr/local/bin/mesh-cli`, with the alias `nox` in +(accepted 2026-10-09): a module `mesh-cli` on every node places `/usr/local/bin/mesh-cli`, with the alias `nox` in interactive zsh. It asks the node-engine over a local socket; the node-engine reads the account from the kernel and asks the controller on its own node's subject; the controller runs the line as the terminal for the control-node's operator account, and as an ordinary call for every other account and node. The diagnosis says what was read and -- 2.54.0 From 4b5793144ddcdd57b7dec48bf6fcdec90088ccdc Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 13:15:59 +0200 Subject: [PATCH 9/9] =?UTF-8?q?ADR=200272:=20=C2=A74=20as=20built=20after?= =?UTF-8?q?=20the=20confirmation=20review,=20ADR=200266=20linked=20now=20i?= =?UTF-8?q?t=20is=20merged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An ordinary line meets the generic verb's reading forms and never a terminal-only command (node account and node agent-account by ADR 0266, token, operator, a secret's export); a terminal line drops ADR 0266's served mark, so it reads as the terminal. How it is checked says the withholding tests search base64 too and fail when what they guard goes, and the control-node refusal is tested through the acts. --- ...nly-where-no-agent-runs-as-its-operator.md | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md index 7e7c5d68..51d555d6 100644 --- a/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md +++ b/02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md @@ -17,7 +17,7 @@ Some changes to the mesh are made **at the controller's terminal** only: ([issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md)); - the agent module's managed settings, tool servers and shared text ([issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)); -- a node's agent account (ADR 0266, hq pull request 225, not yet merged), typed as +- a node's agent account ([ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)), typed as `mesh-controller node agent-account `. In the controller's code "the terminal" is a negative: a command line that did not come through a verb. The @@ -157,16 +157,20 @@ A line from node N, vouched as account A, **runs as the controller's terminal** judgement, heard within ADR 0266 §4's fifteen minutes, says it cannot become root without a person. The controller runs it as it runs a verb's line, in a fresh process of its own binary with its own environment, but -without `MESH_VERB`, and records the caller as *A through mesh-cli on N*. +without `MESH_VERB` and without the mark ADR 0266 puts on everything the serving controller starts, and records the +caller as *A through mesh-cli on N*. So the line reads as the terminal wherever the controller asks whether a +command was started there. **Which node is the control-node does not follow a verb.** Whoever chooses where the controller's module runs chooses the terminal, so the module is assigned and unassigned at the controller's terminal only; `assign` and `unassign` naming it through a verb are refused with the reason. **A line from N's operator account that is not the terminal is an ordinary call.** A `settings` line is composed as -the settings verb composes its own and meets that verb's refusals, the terminal-only keys among them; any other -line goes through exactly what the generic `command` verb does with a line: its refusals today, and its list of -reads once ADR 0266 §7 builds one. It runs with `MESH_VERB=mesh-cli`, so every terminal-only change is refused +the settings verb composes its own and meets that verb's refusals, the terminal-only keys among them. Any other +line meets exactly what the generic `command` verb does with a line: only its reading forms run +([ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md) §7), and +never a command that is the terminal's alone — `node account` and `node agent-account` above all, which ADR 0266 +makes terminal-only, `token`, `operator`, a secret's accept, recover or export. It runs with `MESH_VERB=mesh-cli`, so every terminal-only change is refused with the controller's own reason. The answer adds one sentence saying why this account on this node is not the terminal: the node's agents may run as its operator. That account already reaches the mesh's verbs through the mesh MCP server, so this widens nothing. @@ -269,15 +273,20 @@ Each claim, and what holds it: - **Another namespace is refused, and one account cannot hold the socket**: the node-engine's tests refuse a peer whose namespaces differ, and answer a second line of a busy account *busy* at once while the first is still asked. -- **An ordinary `settings` line takes the settings verb's path**: the controller's test composes each form as the - verb does and refuses any other; an ordinary repair without `--why` meets the generic verb's refusal. -- **The control-node does not follow a verb**: the controller's test refuses assigning or unassigning its module - with `MESH_VERB` set, and lets the terminal do it. +- **An ordinary `settings` line takes the settings verb's path, and every other ordinary line only the generic verb's + reads**: the controller's tests compose each settings form as the verb does and refuse any other, and refuse + `node account`, `node agent-account`, `token issue`, `secret export`, `operator` and `assign` from an ordinary + line, each running nothing (they fail on the commit before ADR 0266's checks were applied to it). +- **The control-node does not follow a verb**: the controller's test asks the `assign` and `unassign` acts + themselves with `MESH_VERB` set and has both refused before any store is touched; with the refusal taken out of + the acts, the test fails. - **A line is never lost, and never said as nothing having run when it may have**: the controller's test follows a line that outlasts the answer window to its answer, over a bus, and refuses a follow by another account or node; the node-engine's tests follow a running line to its answer, say *not known* with the call when no - answer comes, and say *nothing ran* only of a request nobody took. A line's record keeps neither its line's - values nor its answer on the bus, and `calls` never shows its answer (mesh-controller `internal/link`). + answer comes or the request fails after it may have been delivered, and say *nothing ran* only of a request the + link knows it never asked. A line's record keeps neither its line's values nor its answer on the bus, and + `calls` never shows its answer; those tests search both the text and the base64 JSON writes bytes in, and each + fails when the withholding it guards is taken out (mesh-controller `internal/link`, `cmd/mesh-controller`). - **The `MESH_VERB` refusal in `mesh-cli` is a courtesy**: its test holds that the program refuses before asking; nothing rests on it, since the controller judges the vouched account whatever the program does. - **The module places one file**: the catalogue's test holds `mesh-cli` to one archive resource unpacking one @@ -308,4 +317,5 @@ Each claim, and what holds it: - [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md): a module builds from its own source. - [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md): proof and assurance. -- ADR 0266 (hq pull request 225) and ADR 0259 (hq pull request 211): not yet merged, so named without links. +- [ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md): agents under an account of their own, and what is terminal-only. +- ADR 0259 (hq pull request 211): not yet merged, so named without a link. -- 2.54.0