ADR 0272 (accepted) and issue 343: the controller's terminal has no command; mesh-cli on every node #243

Merged
mesh-admin merged 10 commits from decision/0272-mesh-cli-the-operators-command into main 2026-10-09 11:38:36 +00:00
4 changed files with 479 additions and 1 deletions
+14 -1
View File
@@ -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 <command line>` 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
@@ -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 <node> <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 <command line>` 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 <node>` is
`settings set zsh … --node <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.<node>.cli`, as a request on core NATS. Only that node's engine may
publish under `mesh.control.<node>.`: 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 <command>` at the terminal means `mesh-cli <command>` 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 <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 <node>`) 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.
@@ -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 '<json>'` and
`mesh-controller node agent-account <node> <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 <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.
@@ -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/<name>`. 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.<node>.` 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.<node>.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 <command>` at the terminal mean `mesh-cli <command>` 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.