// 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. // // 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 // the home is written only when the licence manager hands this node a token (`claude_code_apply`); this // module calls nothing, the manager starts every exchange (ADR 0183's dated note). import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs"; import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { join } from "node:path"; import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { MANAGED_DIR, render, type Binding, type Facts, type Settings } from "../render.js"; import { generateKeyPair, open, seal, type SealedBox } from "../seal.js"; import { decideApply, grantOf, holdsLogin, readCredentials, withGrant, writeCredentials, type Grant } from "../grant.js"; import { readIdentity } from "../identity.js"; interface Paths { state: string; facts: string; settings: string; home: string; account: string; } function pathsFrom(env: NodeJS.ProcessEnv): Paths | null { const state = env.MESH_CLAUDE_CODE_STATE; const facts = env.MESH_CLAUDE_CODE_FACTS; const settings = env.MESH_CLAUDE_CODE_SETTINGS; const home = env.MESH_OPERATOR_HOME; if (!state || !facts || !settings || !home) return null; return { state, facts, settings, home, account: env.MESH_OPERATOR_ACCOUNT ?? "" }; } const readJson = (p: string, fallback: T): T => { try { return JSON.parse(readFileSync(p, "utf8")) as T; } catch { return fallback; } }; const credentialsPath = (p: Paths) => join(p.home, ".claude", ".credentials.json"); const identityPath = (p: Paths) => join(p.home, ".claude.json"); const bindingPath = (p: Paths) => join(p.state, "binding.json"); const apiKeyPath = (p: Paths) => join(p.state, "api-key"); const helperPath = (p: Paths) => join(p.state, "api-key-helper"); const keyPath = (p: Paths) => join(p.state, "key.pem"); const pubPath = (p: Paths) => join(p.state, "key.pub.pem"); const fingerprint = (s: string) => "sha256:" + createHash("sha256").update(s).digest("hex").slice(0, 16); function keypair(p: Paths): { publicKey: string; privateKey: string } { if (!existsSync(keyPath(p))) { const k = generateKeyPair(); writeFileSync(keyPath(p), k.privateKey, { mode: 0o600 }); writeFileSync(pubPath(p), k.publicKey, { mode: 0o644 }); } return { privateKey: readFileSync(keyPath(p), "utf8"), publicKey: readFileSync(pubPath(p), "utf8") }; } /** Write one managed file as root when its content changed. Returns what happened, in words. */ function writeManaged(name: string, content: string, asRoot: boolean): string { const path = join(MANAGED_DIR, name); let current: string | null = null; try { current = readFileSync(path, "utf8"); } catch { /* absent */ } if (current === content) return `${name}: unchanged`; const cmd = asRoot ? ["install", "-D", "-m", "0644", "/dev/stdin", path] : ["sudo", "-n", "install", "-D", "-m", "0644", "/dev/stdin", path]; const r = spawnSync(cmd[0], cmd.slice(1), { input: content, encoding: "utf8" }); if (r.status !== 0) { throw new Error( `${name}: could not be written to ${MANAGED_DIR} (${(r.stderr || r.error?.message || "").trim()}). ` + `The module writes there through the operator account's passwordless sudo; this machine does not give it.`, ); } return `${name}: written`; } function renderNow(p: Paths): string[] { const facts = readJson(p.facts, null); if (!facts?.console) throw new Error(`the mesh has not rendered ${p.facts} yet; nothing to write`); const settings = readJson(p.settings, {}); const binding = readJson(bindingPath(p), null); const files = render(facts, settings, binding, helperPath(p)); const asRoot = process.getuid?.() === 0; return Object.entries(files).map(([name, content]) => writeManaged(name, content, asRoot)); } interface Handed { licence: string; kind: "subscription" | "api-key"; source: "rotation" | "switch"; sealed: SealedBox; } function apply(p: Paths, args: Record): Record { const handed = args as unknown as Handed; if (!handed?.licence || !handed.sealed || (handed.kind !== "subscription" && handed.kind !== "api-key")) { return { applied: false, reason: "a hand-over names a licence, its kind and a sealed token" }; } const plain = open(handed.sealed, keypair(p).privateKey); const previous = readJson(bindingPath(p), null); const source = previous?.licence === handed.licence ? (handed.source ?? "rotation") : "switch"; if (handed.kind === "api-key") { writeFileSync(apiKeyPath(p), plain.trim() + "\n", { mode: 0o600 }); writeFileSync(helperPath(p), `#!/bin/sh\nexec cat '${apiKeyPath(p)}'\n`, { mode: 0o700 }); chmodSync(helperPath(p), 0o700); } else { const grant = JSON.parse(plain) as Grant; const local = readCredentials(credentialsPath(p)); const d = decideApply(grantOf(local), grant, source); if (!d.apply) { writeFileSync(bindingPath(p), JSON.stringify({ licence: handed.licence, kind: handed.kind }) + "\n", { mode: 0o600 }); return { applied: false, licence: handed.licence, reason: d.reason }; } 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); return { applied: true, licence: handed.licence, kind: handed.kind, source, rendered }; } function status(p: Paths): Record { const binding = readJson(bindingPath(p), null); const creds = readCredentials(credentialsPath(p)); const grant = grantOf(creds); const managed = ["managed-mcp.json", "managed-settings.json", "CLAUDE.md"].map((f) => { try { return { file: join(MANAGED_DIR, f), fingerprint: fingerprint(readFileSync(join(MANAGED_DIR, f), "utf8")) }; } catch { return { file: join(MANAGED_DIR, f), fingerprint: null }; } }); return { node: readJson(p.facts, null)?.node ?? null, licence: binding, token: grant ? { fingerprint: fingerprint(grant.accessToken), expiresAt: new Date(grant.expiresAt).toISOString(), refreshTokenOnDisk: holdsLogin(creds) } : null, managed, publicKey: existsSync(pubPath(p)) ? fingerprint(readFileSync(pubPath(p), "utf8")) : null, }; } function pendingLogin(p: Paths, args: Record): Record { const managerKey = typeof args.public_key === "string" ? args.public_key : ""; if (!managerKey) return { waiting: false, reason: "the caller names the public key to seal a login to" }; const creds = readCredentials(credentialsPath(p)); if (!holdsLogin(creds)) return { waiting: false }; const identity = readIdentity(identityPath(p)); return { waiting: true, identity, sealed: seal(JSON.stringify(creds!.claudeAiOauth), managerKey) }; } export function getClaudeCodeTools(p: Paths): ToolDefinition[] { return [ { name: "claude_code_status", description: "This machine's agent as the mesh configured it: the node, the licence it holds and when its token expires, " + "and the managed files it rendered. Fingerprints only — never a token.", input: {}, run: async () => status(p), }, { name: "claude_code_render", description: "Write the agent's managed directory now from the mesh's facts and this module's settings; says which files changed.", input: {}, run: async () => ({ rendered: renderNow(p) }), }, { name: "claude_code_public_key", description: "The public half of this node's key, which the licence manager seals a token to.", input: {}, run: async () => ({ public_key: keypair(p).publicKey }), }, { name: "claude_code_apply", description: "The licence manager's hand-over: a token sealed to this node's key, with its licence and kind. Applied by the " + "lineage rule; the answer says applied or refused and why, never the token.", input: { licence: { type: "string", description: "the licence's name" }, kind: { type: "string", description: "subscription or api-key" }, source: { type: "string", description: "rotation or switch" }, sealed: { type: "object", description: "the sealed box" }, }, run: async (args) => apply(p, args), }, { name: "claude_code_pending_login", description: "A login a person made on this machine, waiting to be adopted: the grant sealed to the key the caller gives, and " + "the account it belongs to. Nothing when no login is waiting.", input: { public_key: { type: "string", description: "the caller's public key, PEM" } }, run: async (args) => pendingLogin(p, args), }, ]; } registerModuleTools("claude-code", (env) => { const p = pathsFrom(env); if (!p) return []; try { keypair(p); for (const line of renderNow(p)) if (!line.endsWith("unchanged")) console.log(`[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)}`); } return getClaudeCodeTools(p); });