Merge pull request 'ADR 0245: a verb says what it replaces, and the agent is guarded from working round the mesh' (#173) from decision/0242-a-verb-says-what-it-replaces into main
This commit was merged in pull request #173.
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