From eab335b755949f245ee0ac65feb883e30b5e9571 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 23:41:01 +0200 Subject: [PATCH] claude-code: launched over stdio (ADR 0193), the console's five tools in its instructions (ADR 0195) Every bundle is now a child speaking MCP over stdio, so stdout is the channel: the module logs on stderr. The managed CLAUDE.md teaches mesh_search, mesh_describe, mesh_call, mesh_overview and mesh_machine with addresses (., /.) instead of flat tool names. A hand-over is applied whatever the trailing render says; a failed render is reported beside it. Proven over stdio as the runtime drives it: five tools listed, a key made on first use, a sealed switch writing an access-token-only 0600 credentials file that keeps unknown keys. --- modules/claude-code/render.ts | 17 +++++++++++------ modules/claude-code/test/render.test.ts | 3 ++- modules/claude-code/tools/index.ts | 21 ++++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/modules/claude-code/render.ts b/modules/claude-code/render.ts index 4545c88..f33aed0 100644 --- a/modules/claude-code/render.ts +++ b/modules/claude-code/render.ts @@ -82,14 +82,19 @@ it is rewritten whenever the module renders. ## How a session on this mesh works -The console is the only way to the mesh: the MCP server named \`mesh\`. Its tools are the vocabulary. +The console is the only way to the mesh: the MCP server named \`mesh\`. It offers five tools, and +everything else is an address you find and call through them: + +- \`mesh_search\` — words in, matching addresses out. \`mesh_describe\` — one address's arguments. +- \`mesh_call\` — call an address. A seat the mesh holds once is \`.\` (the mesh's own verbs + are \`mesh-controller.\`: \`status\`, \`plan\`, \`node\`, \`assign\`, \`push\`, \`settings\`); + a module on a machine is \`/.\`. +- \`mesh_overview\` and \`mesh_machine\` — the mesh's seats and machines, and what one machine runs. - **Symptom first.** For an error, a failing service or anything unexpected, search the record with the - literal text before forming a hypothesis: \`records.records_search\`. Read a document with - \`records.records_read\`. -- **Ask the mesh before changing it.** \`mesh-controller.status\`, \`.plan\`, \`.node\`, \`.modules\`. - Change it through the controller's verbs (\`assign\`, \`push\`, \`settings\`) or the catalogue. -- **The forge** through the forge module's tools. + literal text before forming a hypothesis: the records module's \`records_search\`, then + \`records_read\`. +- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue. - **A licence** through the \`anthropic-licence-manager\` seat's verbs. Never edit the agent's credentials file by hand, never print or ask for a token. diff --git a/modules/claude-code/test/render.test.ts b/modules/claude-code/test/render.test.ts index e8d90c8..2efeb59 100644 --- a/modules/claude-code/test/render.test.ts +++ b/modules/claude-code/test/render.test.ts @@ -30,7 +30,8 @@ test("the instruction file names the node and its role, and no other node", () = const md = render(facts, { role: "the laptop" }, null, "/h")["CLAUDE.md"]; assert.match(md, /\*\*Node:\*\* `workstation`/); assert.match(md, /\*\*Role:\*\* the laptop/); - assert.match(md, /records\.records_search/); + assert.match(md, /mesh_call/); + assert.match(md, /records_search/); }); test("rendering is deterministic, so an unchanged input writes nothing", () => { diff --git a/modules/claude-code/tools/index.ts b/modules/claude-code/tools/index.ts index a3f312e..daf93e2 100644 --- a/modules/claude-code/tools/index.ts +++ b/modules/claude-code/tools/index.ts @@ -1,6 +1,7 @@ -// claude-code's tools (novox/hq design 36, ADR 0183). Served by the node's tool runtime, which runs as -// the operator account; this bundle is given its state directory and two files the mesh renders into it -// (ADR 0192), and the runtime's own words — the operator's account and home among them. +// claude-code's tools (novox/hq design 36, ADR 0183). A bundle the node's runtime launches and speaks MCP +// to over stdio (ADR 0193), as the operator account; it is given its state directory and two files the +// mesh renders into it (ADR 0192), and the runtime's own words — the operator's account and home among +// them. **stdout is the MCP channel**: everything this module says, it says on stderr. // // Every time the runtime collects these tools, the managed directory is rendered: written only when its // content changed, through the account's escalation, because /etc is root's. The credentials file under @@ -122,8 +123,14 @@ function apply(p: Paths, args: Record): Record writeCredentials(credentialsPath(p), withGrant(local, grant)); } writeFileSync(bindingPath(p), JSON.stringify({ licence: handed.licence, kind: handed.kind }) + "\n", { mode: 0o600 }); - // An API-key binding adds the key-helper to the managed settings; a subscription takes it away. - const rendered = renderNow(p); + // An API-key binding adds the key-helper to the managed settings; a subscription takes it away. The + // licence is applied whatever the render says; a render that fails is reported beside it, not instead. + let rendered: string[] | { failed: string }; + try { + rendered = renderNow(p); + } catch (err) { + rendered = { failed: err instanceof Error ? err.message : String(err) }; + } return { applied: true, licence: handed.licence, kind: handed.kind, source, rendered }; } @@ -209,10 +216,10 @@ registerModuleTools("claude-code", (env) => { if (!p) return []; try { keypair(p); - for (const line of renderNow(p)) if (!line.endsWith("unchanged")) console.log(`[claude-code] ${line}`); + for (const line of renderNow(p)) if (!line.endsWith("unchanged")) console.error(`[claude-code] ${line}`); } catch (err) { // Said, and the tools still served: claude_code_status and claude_code_render say what is wrong. - console.log(`[claude-code] ${err instanceof Error ? err.message : String(err)}`); + console.error(`[claude-code] ${err instanceof Error ? err.message : String(err)}`); } return getClaudeCodeTools(p); });