Issue 340: the whole layer of a module with a mergeable file is the terminal's, and the text still registrable is a risk said
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 review found that a mergeable file takes any key, not only those it
names, and that the text a session reads stays registrable through a verb;
say both, and the follow-ups the module took.
This commit is contained in:
jochen
2026-10-09 02:55:23 +02:00
parent 2953b8a8f4
commit 1dbfb2d85a
2 changed files with 69 additions and 33 deletions
@@ -34,8 +34,10 @@ Two more routes write the same files:
- **The controller's settings verb.** The module's `managed_settings` and `mcp_servers` settings land in a
mergeable file. [Issue 339](../339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md) made a
setting the terminal's alone when a trusted file asks for it with a placeholder. A mergeable file takes
its keys without one, so `mesh-controller.settings {module: claude-code, values: {"managed_settings":
{"hooks": …}}}` was taken from any caller.
any key without one, so `mesh-controller.settings {module: claude-code, values: {"managed_settings":
{"hooks": …}}}` was taken from any caller. The review found the same for every module with a mergeable
file: an identity provider's empty runtime configuration took a `url` of the caller's, and its provisioner
would send its admin login there.
- **Items whose text runs something.** A skill or a slash command may hold shell lines that the agent runs
when it loads the item, before any permission is asked. A skill's front matter may hold hooks and tools it
pre-approves. A subagent placed in a home may start a tool server or change the permission mode from its
@@ -64,6 +66,13 @@ own settings file. It does not see a tool call that changes the managed files of
- no node's layer of the module's settings set `managed_settings` or `mcp_servers`, and no layer had ever
been replaced.
## What stays open
Text a session reads (instruction sections, output styles, and skills, commands and subagents that run
nothing themselves) is still registered through a verb at the mesh and node scopes. It can ask the operator's
agent on every node to run something, which the session then asks about. The diagnosis says what that risk is.
The operator is deciding whether it becomes terminal-only too.
## Ordering
**This must land before ADR 0266's agent account is switched on** (hq pull request 225). Until it lands, the
@@ -81,7 +90,9 @@ at the controller's terminal. An agent's attempt through any mesh tool is refuse
runtime speaks to them and calls each one: a hook, settings with a hook, a status line or a permission
mode, a permission rule added or removed, a tool server by command or by address, and a skill, command or
subagent with a shell line or with `hooks`, `allowed-tools`, `mcpServers` or `permissionMode` in its front
matter, at the mesh, node and home scopes. Each is refused with an error that names this issue, and
matter, at the mesh, node and home scopes. Front matter hidden behind a byte-order mark, written as a flow
mapping or with merged keys, and front matter that does not parse, are refused, and so is an `@` reference
to a file outside the item. Each is refused with an error that names this issue, and
nothing is put on the bus or placed in a home. On the commit before the fix every one of them was taken.
- **In the module's render tests.** A hook, settings or a skill with a shell line already on the bus, or
kept in the module's own file by an older build, is never rendered. The tool server and the instruction
@@ -90,7 +101,9 @@ at the controller's terminal. An agent's attempt through any mesh tool is refuse
settings file holds.
- **In the controller's tests, through the real verb dispatch.** Setting, changing, dropping or clearing
the module's `managed_settings`, `mcp_servers` or `role` through the `settings` verb, for one machine or
the mesh, and through the `command` verb, is refused with an error naming this issue. The layer is
the mesh, and through the `command` verb, is refused with an error naming this issue. So is a new `url` on a
module whose mergeable runtime configuration starts empty, while a file marked `"trusted": false` stays the
verb's. The layer is
unchanged after every refusal. At the terminal the same is taken. These tests fail on the commit before
the fix.
- **Live, after both are taken:** the calls in the diagnosis's last section are each refused, and
@@ -38,21 +38,36 @@ documentation of skills, subagents, plugins, output styles and memory, read 2026
| output style, instructions, rule file | the plugin, the managed instruction file, a home | text the model reads; no field runs anything | still registered |
| a script beside a skill | the plugin, or a home | only when the agent runs it, through the session's own rules and the guard | still registered |
**Text is a lower class, and stays registrable.** Instructions, output styles and skills steer the agent, as
any text in its context does. A session still asks before it runs what the text suggests, under the
operator's permission rules and the mesh's guard. A skill is also invoked by the model on its own, which is
why its shell lines and pre-approved tools are refused. What the text alone asks for is judged by the
session. Blocking all registered text would end the shared instructions the mesh relies on, and would not
stop text reaching an agent by other routes (a file in a repository, a tool's answer). This is a judgement,
and it is said: a session in a mode that runs everything without asking would run what a registered text
asks for. That mode is the operator's own choice for their own session.
**Text is a lower class, and stays registrable through a verb. This is a risk, and it is the operator's to
take or close.** At the mesh and node scopes, an instruction section, an output style, and a skill, command or
subagent without the fields above are still registered through the module's tools, so any caller of the mesh
MCP server can register them, an agent confined on the control node included. They reach every session on the
nodes they name, as text the model reads:
- the instruction sections land in the managed instruction file, which every session reads first and treats
as the mesh's own word;
- a skill is invoked by the model on its own when its description matches what it is doing.
So a registered text can ask the operator's agent, on every node, to run a command. Nothing in it runs by
itself: the session still asks before it runs what the text suggests, under the operator's permission rules and
the mesh's guard. But a session in a mode that runs without asking, or a person who approves what looks like the
mesh's own instruction, would run it as the operator. That is the same sideways path as a hook, made one
approval longer.
What closing it costs: the mesh's shared instructions and skills, which agents keep up today, would be
registered at the controller's terminal alone. It also does not stop text reaching an agent by other routes
(a file in a repository, a tool's answer). On 2026-10-09 the operator is deciding whether this text becomes
terminal-only too. This issue records the answer when it is given.
### The routes that write the same files
- **The module's tools**: refused (below).
- **The controller's settings verb**: `TerminalKeys` (`internal/catalogue/terminal_keys.go`) counted the
settings a file asks for by placeholder alone. A mergeable file asks for every key it holds without one.
The module's settings file is mergeable, so `managed_settings`, `mcp_servers` and `role` were the verb's.
settings a file asks for by placeholder alone. A mergeable file takes every key a layer sets, named in its
content or not. The module's settings file is mergeable, so `managed_settings`, `mcp_servers` and `role` were
the verb's. The review found the same for every other mergeable file: 13 runtime configurations start empty,
and a provider reads what lands in them. A `url` set through a verb on the identity provider's would have
had its provisioner send the admin login to the caller's listener.
- **The catalogue's defaults**: the module's settings file holds empty values only. No layer sets them in
the catalogue.
- **The bus**: the module's `config` and `servers` state. Only a holder of the module's grants writes them,
@@ -65,22 +80,22 @@ asks for. That mode is the operator's own choice for their own session.
**In the controller** (pull request in `mesh-controller`):
1. A mergeable file asks for every key its own content names. Unless the file says `"trusted": false`, each
is a terminal key, refused through every verb like the keys issue 339 named. For the agent's module,
`managed_settings`, `mcp_servers` and `role` are the terminal's.
2. The refusal names this issue beside 339.
1. A module with a mergeable file not marked `"trusted": false` has its **whole layer** set at the terminal.
Through any verb, any change to it is refused, whatever the key, a key no file names included. Reading it
through a verb still answers. For the agent's module that covers `managed_settings`, `mcp_servers` and
`role`.
2. A mergeable file's own keys are also counted as keys it asks for, so `module check` names the file when it
does not say whether it is trusted.
3. The refusal names this issue beside 339.
This reaches every mergeable file whose content names keys, not only the agent's. On 2026-10-09 that was 12
more files in the catalogue: the database web app, the agent's licence manager, the dashboards, the agent
memory store, the chat client's look, the watcher's and the messenger's chat ids, the flow editor, the route
adapter, the search engine, the database pooler and the network controller. In the media catalogue it was 5:
the library sorter and four download clients' settings. Through a verb these keys were the caller's before.
Many decide what a consumer trusts or reaches: a route, a chat id that verifies a sender, a password.
`module check`'s count of files that do not say whether they are trusted rose from 9 to 22 over the catalogue. An author opts a
harmless one out with `"trusted": false`. That waits until the node-engine accepts the field, as issue 339
says.
A key the file does not name still lands through a verb. Nothing reading the agent's file reads such a key.
This reaches every module with a mergeable file, not only the agent's. On 2026-10-09 that was 25 modules in the
catalogue and 11 in the media catalogue, every one with a file that says nothing about trust. Through a verb
their settings were any caller's before. Many decide what a consumer trusts or reaches: a provider's address, a
route, a chat id that verifies a sender, a password. From now on the operator sets them at the terminal. An
author gives a verb back a file nothing trusts with `"trusted": false`. That waits until the node-engine accepts
the field, as issue 339 says. `module check`'s count of files that do not say whether they are trusted rose from
9 to 22 over the catalogue. It counts the files whose content names keys. A file that starts empty is not
counted, but is refused all the same.
**In the module** (pull request in `mesh-catalog`):
@@ -92,9 +107,15 @@ A key the file does not name still lands through a verb. Nothing reading the age
operator's `managed_settings` under the mesh's own keys.
3. A skill, subagent or command holding a shell line, or `hooks`, `mcpServers`, `permissionMode` or
`allowed-tools` in its front matter, is refused at every scope, and the import tool refuses one too.
4. What the module kept in its own file before is judged again when it renders.
5. Removing stays: unregistering a tool server, a hook or settings takes away what runs.
6. A new read-only tool, `claude_code_registrations`, lists every registration with its `by`, `at`, and
Front matter is read as YAML, after a byte-order mark, in flow style and with merged keys. Front matter
that does not close, does not parse, holds a key twice or is not a mapping is refused rather than guessed
at.
4. A skill or command that refers with `@` to a file that is not one of its own files is refused. The agent
reads such a file into its prompt when the item loads, so the item would carry another file's words, or a
secret, into every session it reaches.
5. What the module kept in its own file before is judged again when it renders.
6. Removing stays: unregistering a tool server, a hook or settings takes away what runs.
7. A new read-only tool, `claude_code_registrations`, lists every registration with its `by`, `at`, and
whether it is still rendered and why not, beside what the terminal set for the node. `by` is the node
whose tool runner served the call. The caller was never recorded, because the runner is not told. A tool
server's registration kept no `by`.
@@ -130,7 +151,9 @@ Each of these must be refused, saying why, and change nothing:
- `<node>/claude-code.claude_code_settings_set {settings: {"model": "x"}}`, and with `scope: node, nodes: all`;
- `<node>/claude-code.claude_code_permission_add {list: allow, rule: Bash}`;
- `<node>/claude-code.claude_code_mcp_register {name: x, url: "https://example.org/mcp", nodes: all}`;
- `<node>/claude-code.claude_code_skill_register {name: x, content: "---\nname: x\nallowed-tools: Bash\n---\nx"}`;
- `<node>/claude-code.claude_code_skill_register {name: x, content: "---\nname: x\nallowed-tools: Bash\n---\nx"}`, and
with the front matter as a flow mapping, and with `@~/.ssh/id_ed25519` in its body;
- `mesh-controller.settings {module: keycloak, node: <its node>, values: "{\"url\": \"http://192.0.2.1\"}"}`;
- `mesh-controller.settings {module: claude-code, values: "{\"managed_settings\": {\"model\": \"x\"}}"}`, and
with `node`, and `clear: "true"`;
- `mesh-controller.command {command: "settings set claude-code '{\"mcp_servers\":{}}' --node <a node>"}`.