ADR 0245: a verb says what it replaces, and the agent is guarded from working round the mesh
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 delivered

The agent reached machines over ssh because search never found the verbs by the commands it knew.
Records the three rules (generated instead-of table, search by replaced command, the shell guard
with the operator's recorded override) and how each is checked; to-be 36 names it.
This commit is contained in:
jochen
2026-10-07 21:04:24 +02:00
parent ede4b29cd1
commit 6cacf772dc
3 changed files with 114 additions and 0 deletions
@@ -0,0 +1,98 @@
---
topic: what runs on it
status: accepted
date: 2026-10-07
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md
---
# 245. A verb says what it replaces, and the agent is guarded from working round the mesh
## Context
The agent on the mesh's machines kept reading a service's log with `ssh <machine> journalctl …` and a
container's with `ssh <machine> docker logs …`, while the service manager's seat served `journal` and the
container module `docker_logs` on every machine. It was not refusing the tools; it never found them. The
mesh MCP server offers five tools and finds everything else by `mesh_search` with the agent's own words
([ADR 0195](0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)), and those words were the commands it would have
typed — `journalctl`, `logs`, `systemctl status`, `docker ps`. Search matched names and descriptions: the
journal verb's description says *journal*, never *logs* or *journalctl*, so the search answered nothing
and ssh worked. The managed instructions said "the console is the only way to the mesh" and nothing
stopped a session that did not believe it.
Each work-around also hid the gap it went round: a machine reached by ssh is a tool nobody learns is
missing.
## Considered Options
1. **Write the guidance by hand** in the agent's instructions — "use the journal verb, not journalctl".
Rejected: a hand-written list of tools drifts from the tools the mesh has, which is why the managed
instructions already name no machine and no module.
2. **Deny ssh in the agent's permission settings** (a `deny` rule on `Bash(ssh:*)`). Rejected: a prefix
rule cannot tell a mesh machine from the forge — git over ssh to the forge is legitimate — nor see a
command inside `bash -c` or `$(…)`, and its refusal names no tool.
3. **Teach search synonyms only.** Rejected alone: it helps the agent that searches, and the habit at
fault is not searching.
4. **The verb says what it replaces, and three things are built from that one statement** — chosen.
## Decision
1. **A verb or tool says which shell commands it replaces.** A seat's verb carries `replaces` in its
definition — what a role replaces is the role's, so a module never says it for a verb it serves under a
claim; a module says it for its own tools in its manifest, keyed by tool. Each entry is the command as
typed (`journalctl`, `systemctl restart`, `docker logs`), one short line; `edit /etc/hosts` and
`HOSTALIASES` name the two local work-arounds for a mesh name. The controller answers both — the
seats' `tools` verb and `module list --json` — and is the only source.
2. **The agent's instructions carry an "instead of" table generated from it**, on every render of the agent
module: one row per seat or module, each verb with the commands it replaces, the rows the guard on that
machine refused most first. Never written by hand: a verb that gains `replaces` is in the next render.
3. **Search finds a tool by the command it replaces.** `mesh_search` matches a query that is a command line
against every replaced command — its words in order — and ranks: a replaced command first (the most
specific wins), then words in a name or a replaced command, then words in a description, with a short
table of the words agents use for the mesh's (*logs* finds the journal). Seats before modules on a tie.
4. **A guard on the agent's shell refuses working round the mesh.** The agent module delivers, in its plugin
on every machine, a hook run before every shell command and file edit. It refuses `ssh` (and `scp`,
`sftp`, `rsync`, `mosh`, `autossh`) to a mesh machine — by name, by a name under its domains, by any name
in the mesh's internal domain, by address, read as ssh itself reads the destination, a jump through one
included — and writing the hosts or resolver file, and `HOSTALIASES`. The refusal names the tool that does
the job on that machine when one says it replaces the command, and otherwise says that **a missing tool is
created in the module that owns it, on its seat, never worked around.** An ssh login as the forge's git
account passes, **stated here rather than silently**: that account runs git and nothing else. Nothing else
is allowed by exception.
5. **The operator alone overrides the guard, and every override is recorded.** An override is a variable
with the reason as its value, set in the operator's own shell before a session starts, read from the
session's environment as it was started — so nothing a session does can set it, and a command naming it is
refused outright. Every override and every refusal is a line in the agent module's record on that machine,
readable through the module's tool; an override that cannot be recorded is not honoured.
## Consequences
- **How each rule is checked.** Rule 1: the controller refuses a manifest whose `replaces` names a tool it
does not declare, or an entry that is empty, longer than a line or repeated — the module check every pull
request's gate runs; the controller's tests hold that the worked-around verbs say what they replace.
Rule 2: the agent module's tests generate the table from records and hold its rows, order and cut; the
rendered file is the module's managed instruction file, never edited. Rule 3: the tool runner's tests hold
that `journalctl`, `logs`, `systemctl status` and `docker ps` find the right verb first, and that a
controller older than the field searches as before. Rules 4 and 5: the agent module's tests hold the
guard's refusals and what passes — git to the forge, ssh beyond the mesh, a command merely mentioning
ssh — the override read only from the session's start, and recorded; the module's guard tool shows the
rules, the data and the record, which is how a refusal that should not have happened, or a habit that
found a way round, is seen.
- **It is a guard against a habit, not a sandbox.** A command built to hide what it runs can hide it. The
record is the check on that, not the matcher.
- **A tool the mesh lacks now surfaces as a refusal** instead of disappearing into an ssh session. The
answer to one is a tool in the owning module, which is more work than an ssh line, and is the point.
- **A module's `replaces` waits for the controller that reads it.** The manifest is read strictly, so a
module may say what its tools replace only once that controller runs; until then its tools are found by
name and description as before, and the seats' verbs carry the mesh's own statement.
- The agent module now asks the controller for its tools and machines every few minutes, and renders when
the answer changes.
## References
- The controller: verbs say what they replace, the manifest field, both answers (`mesh-controller`).
- The mesh MCP server's search (`mesh-tools`, the tool runner's loopback mode).
- The agent module's table, guard and record (`mesh-catalog`, the claude-code module).
- [ADR 0195](0195-the-meshs-tools-are-found-by-address-not-announced-whole.md) — the mesh's tools are found by address.
- [ADR 0216](0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md) — the agent's plugin, which carries the guard.
+1
View File
@@ -342,6 +342,7 @@ python3 00-META/checks/index.py fail if stale
- **0241** — [A machine says how its network is, and an outside writer of a mesh file is a finding](0241-a-machine-says-how-its-network-is-and-an-outside-writer-of-a-mesh-file-is-a-finding.md)
- **0242** — [A recorded build moves only by a person's push, and a send says what it recreates](0242-a-recorded-build-moves-only-by-a-persons-push-and-a-send-says-what-it-recreates.md) *(proposed)*
- **0243** — [The agent module removes a home item it did not place only on the person's word, and keeps a copy](0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md)
- **0245** — [A verb says what it replaces, and the agent is guarded from working round the mesh](0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md)
### How it is built
@@ -5,6 +5,7 @@ code: [mesh-catalog modules/claude-code]
updated: 2026-10-07
decisions:
- 02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md
- 02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md
- 02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md
- 02-DECISIONS/0213-the-operator-sets-the-agents-managed-settings-through-the-agent-module.md
- 02-DECISIONS/0209-a-login-on-a-node-moves-that-node-to-its-account-and-an-api-key-is-added-from-any-node-sealed.md
@@ -128,6 +129,19 @@ listed here, because a table is a copy that drifts.
pull request and a human approval for every merge; test before pushing, because nodes update unattended;
the playbooks in the record.
**Instead of a shell command** ([ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md)).
A table generated on every render from what each seat verb and module tool says it replaces, as the
controller answers it: one row per seat or module, each verb beside the commands it replaces, the rows the
guard on this machine refused most first. Never written by hand, and short; what it leaves out, the mesh MCP
server's search finds by the command itself.
**The guard on the agent's shell** (ADR 0245). The module's plugin carries, first among its hooks, the
module's own program as a check before every shell command and file edit: ssh and its kin to a mesh
machine, and writing the hosts or resolver file or setting `HOSTALIASES`, are refused with the tool that
does the job, or with the rule that a missing tool is created in its owning module. It runs from the
managed directory, so a session cannot change it, judges with the machines and replaced commands the module
last asked of the controller, and records every refusal and every operator's override in the module's state.
## 4. The mesh MCP server
> **Revised 2026-10-03, building it.** The vendor's managed-settings key for tool servers refuses any
@@ -281,6 +295,7 @@ operator's word.
| the module's render test: a registered skill, subagent, command, hook and output style land in the `nox-mesh` plugin; a tool server, a setting and an instruction section in their managed files; for one machine of two, a mesh item on both, a node item on one, a home item only in that home; a setting naming the marketplace keys is overridden | ADR 0216 |
| the module's test: a home name the person already uses is refused, unregistering removes only the placed path, and an item above 256 KiB is refused | ADR 0216, ADR 0182 |
| live: a skill registered at the mesh scope is offered as `nox-mesh:<name>` in a new session on each machine | ADR 0216 |
| the module's test: the "instead of" table is generated from the controller's answers, ordered and cut; the guard refuses ssh to a mesh machine by name, domain, internal name, address, alias and jump, and the local work-arounds for a mesh name, naming the tool — and lets git to the forge, ssh beyond the mesh and a command merely mentioning ssh through; the override is honoured only from the session's start and only when recorded | ADR 0245 |
| the mesh MCP server's provision resolves by co-location; a machine without the mesh MCP server refuses the module by name | ADR 0027, ADR 0152 |
| a new session on the assigned workstation lists the mesh MCP server's five tools under `mesh` ([ADR 0195](../../02-DECISIONS/0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)) and answers "which node am I" from the instruction file | the exit of the build |