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