diff --git a/00-META/glossary.md b/00-META/glossary.md index 1ffaa22b..66991ed4 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -276,6 +276,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 @@ -441,7 +446,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 @@ -450,6 +455,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..51d555d6 --- /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,321 @@ +--- +topic: the tiers +status: accepted +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](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 +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 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) +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; the socket grants nothing + +**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. 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 +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. + +**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 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: 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 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 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. + +**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, 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. 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 + +- **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. **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. + +## 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 local socket every account on the machine may reach. It accepts one line per + 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 + +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, 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 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 + 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 + +- [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 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. +- [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md): proof and assurance. +- [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. 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..622325b3 --- /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) +(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 +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..9c1147f4 --- /dev/null +++ b/04-ISSUES/343-the-controllers-terminal-has-no-command/01-diagnosis.md @@ -0,0 +1,82 @@ +# 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 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`; 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. +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. + +## 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.