From 1dbfb2d85a4b6beb30738229e222c7898bc2073c Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 02:55:23 +0200 Subject: [PATCH] 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 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. --- .../00-report.md | 21 ++++- .../01-diagnosis.md | 81 ++++++++++++------- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md index adc91852..5bcc36bf 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md @@ -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 diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md index 68c74db8..6d8403d1 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/01-diagnosis.md @@ -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: - `/claude-code.claude_code_settings_set {settings: {"model": "x"}}`, and with `scope: node, nodes: all`; - `/claude-code.claude_code_permission_add {list: allow, rule: Bash}`; - `/claude-code.claude_code_mcp_register {name: x, url: "https://example.org/mcp", nodes: all}`; -- `/claude-code.claude_code_skill_register {name: x, content: "---\nname: x\nallowed-tools: Bash\n---\nx"}`; +- `/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: , 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 "}`.