ADR 0272 (proposed) and issue 343: the controller's terminal has no command
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

The records send the operator to "the controller's terminal", and on the
control-node `mesh-controller` is not a command: the controller is a mesh
process with its own account and environment, and nothing puts anything on
PATH. The record proposes mesh-cli, a module on every node whose program
asks the node-engine over a local socket; the node-engine vouches for the
account from the kernel and asks the controller on its node's own subject;
the controller runs the line as the terminal only for the operator's account
where no agent can be that account. The glossary gains mesh-cli, nox (its
zsh alias) and the controller's terminal.
This commit is contained in:
jochen
2026-10-09 11:59:34 +02:00
parent d81ae38970
commit 95d78f7011
4 changed files with 377 additions and 1 deletions
+14 -1
View File
@@ -272,6 +272,11 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
`release`, an identifier to rename.
- **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's
touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**.
- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a
verb, so a change that is the terminal's alone is taken (issue 339, issue 340). In the controller's code it is a
line run without `MESH_VERB`. The operator reaches it with `mesh-cli` from their own account on a node where no
agent can run as that account: the control-node, and a node whose agents run under an account of their own
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
## Change and delivery — a commit on its way to the nodes
@@ -437,7 +442,7 @@ it should not. **Recorded by** the controller. **Upstream of** Provisioning and
trust. **Recorded by** the router and the controller (authorising asks).
**Downstream of** Health and repair, Change and delivery, and Identity and access. An authorising
answer acts in whichever domain asked, through that domain's own verb; the conversation owns the
asking, never the act. **Decided by** ADR 0152, 0175, 0234.
asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0272.
**Uses:** operator, person, proof (Identity and access), release (Change and delivery).
- **mesh MCP server** — the endpoint on a node's loopback through which every agent and person on that
@@ -446,6 +451,14 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234.
not a module of its own. "Console" suggested a terminal or a shell, and the module `mesh-console` it
once named no longer exists; "the tool bridge" and "the brain" named the predecessor's program.
*Not:* ~~console~~ (hq), ~~mesh-console~~, ~~tool bridge~~
- **mesh-cli** — the module on every node that places the operator's command, and the command itself:
`/usr/local/bin/mesh-cli <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,226 @@
---
topic: the tiers
status: proposed
date: 2026-10-09
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
---
# 272. The operator reaches the controller with `mesh-cli` on every node, and a node is the controller's terminal only where no agent runs as its operator
## Context
Some changes to the mesh are made **at the controller's terminal** only:
- the places and accesses a module's settings name, and the keys a provider's trust or a trusted file reads
([issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md));
- the agent module's managed settings, tool servers and shared text
([issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md));
- a node's agent account (ADR 0266, hq pull request 225, not yet merged), typed as
`mesh-controller node agent-account <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 answered as any other account (§4).
`mesh-cli` holds no credential and reads no file of the mesh. It does one thing: it asks the node-engine on its own
machine over a local socket, and prints what comes back. A process that carries `MESH_VERB` (a verb's own process)
is refused at once, and told why: a verb is never a person.
### 3. The node-engine vouches for the account
The node-engine serves a socket every local account may connect to. For each connection it:
1. reads the connecting process's user id from the kernel (`SO_PEERCRED`), and names the account from the
machine's user database;
2. sends the line, the account and the user id to the controller on `mesh.control.<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, so **the node is a fact the bus server enforces and the account is a
fact the kernel gives**. The CONTROL stream names its own subjects, so the request is never persisted;
3. says with it whether the node's declaration names an agent account, and the engine's latest judgement of it
(ADR 0266 §3);
4. answers the program with what the controller answered, or says that the controller did not answer within its
call bound.
The node-engine is trusted for its own node. Root on a node could claim any account there; root on a node that is
the terminal is already the operator's, and §4 makes no other node the terminal.
### 4. The controller judges, and runs the line
A line from node N, vouched as account A, **runs as the controller's terminal** when A is N's operator account
([ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)) and either:
- N is the control-node; or
- N names an agent account of its own (ADR 0266 §1), and the controller's own record of the engine's judgement,
heard within ADR 0266 §4's fifteen minutes, says it cannot become root without a person.
The controller runs it as it runs a verb's line, in a fresh process of its own binary with its own environment, but
without `MESH_VERB`, and records the caller as *A through mesh-cli on N*.
**Every other line is an ordinary call.** It goes through exactly what the `command` verb does with a line: its
refusals today, and its list of reads once ADR 0266 §7 builds one. It runs with `MESH_VERB=mesh-cli`, so every
terminal-only change is refused with the controller's own reason. The answer adds one sentence saying why this
account on this node is not the terminal: an agent account, root, an account that is not the operator's, or a node
whose agents may run as its operator.
**The control-node rule is no wider than today.** On the control-node the operator's account holds `sudo`, and
until ADR 0266 is switched on so do the agents running as it: either could run the controller's binary as root
with its environment. `mesh-cli` gives that account the same reach and no more. Once agents there run as `agent`,
`agent` is never the terminal.
### 5. What comes back
The answer is the command's standard output, its standard error, its exit status, whether it ran as the terminal,
and why or why not. `mesh-cli` prints the two streams and exits with the status. An answer larger than one bus
message is cut there and the cut is said; it is never silently short. Standard input is not carried: a command
that reads it gets nothing and fails saying so. Streaming the output and carrying the input are later steps.
### 6. What is shared lives in the SDK
The socket's path, the program's request and answer, and the request on the bus with its subject are one package
of the SDK (`mesh-sdk`), imported by `mesh-cli`, the node-engine and the controller. None of them keeps its own
copy. The list of what is terminal-only is not shared: it stays in the controller, its one judge, and `mesh-cli`
shows the controller's words.
### 7. In what order it is built
1. **Phase 1.** The SDK package; the node-engine's socket and its request; the controller's answer, with the
control-node as the terminal for its operator account; the `mesh-cli` module on every node. From then on the
operator types `nox …` on the control-node, and gets the ordinary calls on every other node.
2. **Phase 2.** The agent-account route of §4 on a node other than the control-node, once ADR 0266 runs there
(on the operator's own machines after the follow-up decision ADR 0266 names).
3. **Later.** Streamed output and standard input; a second factor for a node whose agents must share the
operator's account.
## Consequences
- The operator has one command on every node, built, checked and moved like every other module, and the records'
"at the controller's terminal" names something that exists: `mesh-cli` (or `nox`) on the control-node.
- A terminal-only change typed on any other node is refused there with the reason, instead of being impossible to
type at all.
- No agent on the control-node gains anything it did not have; once ADR 0266 is switched on there, agents lose the
terminal entirely.
- **Same-account limits stay ADR 0266's boundary.** While an agent runs as the operator, it can do what the
operator's account can: put a `mesh-cli` of its own earlier on `PATH` (the zsh module puts the account's
`~/.local/bin` first), or redefine the alias outside the mesh's block. On a node where agents run as the
operator, the operator's account is not the terminal, so this reaches nothing terminal-only there.
- The alias hides any other `nox` in interactive zsh (Arch's `python-nox` installs `/usr/bin/nox`);
`command nox` still reaches it.
- The node-engine gains a listener every local account may reach. It accepts one line per connection, bounded in
size and in time, and runs nothing itself.
- The controller's own binary is still never on `PATH`. A record that says to type
`mesh-controller <command>` at the terminal means `mesh-cli <command>` on the control-node from this record on.
## How it is checked
- **The SDK:** the request and the answer survive a round trip, and the subject names the node.
- **The node-engine:** a test connects to the socket as the test's own process and the request carries that
process's user id and account; a connection whose credentials cannot be read is refused, never sent as
anyone; a line over the bound is refused.
- **The controller:** tests hold that a line from the control-node's operator account runs without `MESH_VERB`;
that a line from any other account, or from another node without a judged agent account, runs with
`MESH_VERB=mesh-cli` and a terminal-only change is refused with the reason; that a judgement older than
fifteen minutes is not one; and that an ordinary line meets every refusal of the `command` verb.
- **The catalogue:** the module's check passes, its archive places one file, and its shell contribution is the
alias.
- **Live, at acceptance (the operator's view):** on the control-node, in the operator's own shell,
`nox settings show zsh --node <node>` answers as the terminal, and a terminal-only `nox settings set …` is taken.
On another node, the same `settings set` is refused and says why. From `agent`, once that account exists, it is
refused too.
## References
- [Issue 343](../04-ISSUES/343-the-controllers-terminal-has-no-command/00-report.md): the gap.
- [Issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md),
[issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md): what is
terminal-only, and how a verb is told from the terminal.
- [ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md): the operator account.
- [ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md): shell contributions.
- [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md):
a module builds from its own source.
- [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md): proof and assurance.
- ADR 0266 (hq pull request 225) and ADR 0259 (hq pull request 211): not yet merged, so named without links.
@@ -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)
(proposed): a module `mesh-cli` on every node places `/usr/local/bin/mesh-cli`, with the alias `nox` in
interactive zsh. It asks the node-engine over a local socket; the node-engine reads the account from the kernel and
asks the controller on its own node's subject; the controller runs the line as the terminal for the control-node's
operator account, and as an ordinary call for every other account and node. The diagnosis says what was read and
what each repository does.
## How it will be checked
The checks ADR 0272 lists, and the acceptance test above, run live on the control-node and on one other node.
@@ -0,0 +1,75 @@
# 343 — Diagnosis
## 2026-10-09: what was read
The record was searched first: `records_search` for "command not found mesh-controller" found nothing, and for
"controller's terminal" found issue 340 and research 039, which use the words and say nothing of how the terminal
is reached.
### The controller
- Its module declares one process, run as the `mesh-controller` account, `./mesh-controller serve`, with an
environment of credential files under its own directory (mode 0700, owned by that account) and the ports of the
store and the bus as `${seat:…}` references the controller resolves at composition.
- `throughAVerb` (`cmd/mesh-controller/modules.go`) is the whole of the terminal: a command line runs as the
terminal when `MESH_VERB` is not in its environment. `runVerb` (`cmd/mesh-controller/seatverbs.go`) sets it for
every line a verb runs, the named verbs and the generic `command` alike, and runs the line in a fresh process of
the controller's own binary with the serving process's environment.
- The generic `command` verb runs any command line today, except `settings set` and `settings clear` and a repair
without `--why`. ADR 0266 §7 makes it a list of reads; that is not built.
### The node-engine
- It writes a process's unit under `/etc/systemd/system`, and unpacks its bundle into
`/var/lib/mesh/daemons/<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, the program's request and answer, the bus request and its subject.
2. **`mesh-host`:** the node-engine serves the socket, reads `SO_PEERCRED`, names the account, and asks the
controller on `mesh.control.<node>.cli`.
3. **mesh-controller:** the serving controller answers `mesh.control.*.cli`, judges the terminal (ADR 0272 §4),
and runs the line through `runVerb`'s path, without `MESH_VERB` for the terminal and with
`MESH_VERB=mesh-cli` otherwise, after the `command` verb's refusals.
4. **mesh-catalog:** the module `mesh-cli` (its program, its archive into `/usr/local/bin`, its zsh alias `nox`),
assigned to every node; and the agent module's README, whose `mesh-controller settings set …` becomes
`mesh-cli settings set …`.
Each code change is a branch and a pull request, merged by the operator after ADR 0272 is accepted. The records
that tell the operator to type `mesh-controller <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.