claude-code: refuse registering what a session runs, so no agent can plant it in the operator's sessions
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

The tool runner is not told who called, so a hook, a managed setting, a
permission rule or a tool server registered through these tools could come
from an agent confined on another node and run as the operator there
(hq issue 340). They are set at the controller's terminal now; a skill,
subagent or command that runs something by itself is refused too, kept
hooks and settings are never rendered, and claude_code_registrations
lists what is registered and by which node.
This commit is contained in:
jochen
2026-10-09 02:30:11 +02:00
parent a1406e7d79
commit 736743fedc
11 changed files with 650 additions and 336 deletions
+8 -27
View File
@@ -329,42 +329,23 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
return map[string]any{"here": Registered(p), "everywhere": keys}, err
}},
{Name: "claude_code_mcp_register",
Description: "Register an MCP server with Claude Code on this node, every node, or a list — an http/sse server by url, or a stdio server by command. Kept on the bus, so a node that joins later takes it too. Never put a secret in env or headers: the mesh refuses one.",
Description: "Refused, and says why (novox/hq issue 340): a tool server is started by every session on the nodes it reaches, as whoever starts it, and this tool cannot tell an agent from the operator. Tool servers are the operator's, set at the controller's terminal in this module's mcp_servers setting; what was registered before stays, and claude_code_registrations lists it.",
Input: map[string]any{
"name": str("the server's name: letters, digits, - and _"),
"type": str("http, sse or stdio (default stdio when a command is given, http when a url is)"),
"url": str("an http or sse server's url"),
"command": str("a stdio server's program"),
"args": map[string]any{"type": "array", "description": "a stdio server's arguments"},
"env": map[string]any{"type": "object", "description": "a stdio server's environment"},
"headers": map[string]any{"type": "object", "description": "an http server's headers"},
"nodes": nodesArg,
"name": str("the server's name"),
"nodes": nodesArg,
},
Run: func(a map[string]any) (any, error) {
entry := map[string]any{}
if t, _ := a["type"].(string); t != "" {
entry["type"] = t
} else if _, hasURL := a["url"]; hasURL {
entry["type"] = "http"
} else {
entry["type"] = "stdio"
}
for _, k := range []string{"url", "command", "args", "env", "headers"} {
if v, ok := a[k]; ok {
entry[k] = v
}
}
name, _ := a["name"].(string)
return RegisterServer(p, Registration{Name: name, Entry: entry, Nodes: nodesOf(a["nodes"])}, servers, view, writeManaged, nodesRunningMe)
}},
Run: func(map[string]any) (any, error) { return nil, errors.New(ServerAtTheTerminal) }},
{Name: "claude_code_mcp_unregister",
Description: "Remove an MCP server registered through this module, on this node or more.",
Description: "Remove an MCP server registered through this module before registering was refused (novox/hq issue 340), on this node or more: it stops loading in the sessions there. Removing takes away what a session runs; adding is the operator's, at the controller's terminal.",
Input: map[string]any{"name": str("the server's name"), "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
name, _ := a["name"].(string)
return RegisterServer(p, Registration{Name: name, Nodes: nodesOf(a["nodes"])}, servers, view, writeManaged, nodesRunningMe)
}},
}
out = append(out, stdio.Tool{Name: "claude_code_registrations",
Description: "Read-only: everything registered through this module that a Claude Code session runs or obeys — the tool servers, the hooks, the settings and the skills, subagents, commands, output styles and instructions — by key, with who registered it, when, and whether it is still rendered (novox/hq issue 340), beside what the operator set for this node at the controller's terminal (managed_settings, mcp_servers). `by` is the node whose tool runner served the call, never the caller.",
Run: func(map[string]any) (any, error) { return Registrations(p, servers, config) }})
return append(out, configTools(p, config, configView)...)
}