ADR 0245: a verb says what it replaces, and the agent is guarded from working round the mesh
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:
+98
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user