ADR 0272 (accepted) and issue 343: the controller's terminal has no command; mesh-cli on every node #243
+14
-1
@@ -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
|
||||
|
||||
+321
@@ -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.
|
||||
Reference in New Issue
Block a user