// What the module writes into the agent's machine-wide managed directory (novox/hq design 36 §1–§4). // Pure: composed from the facts the mesh rendered, the settings the operator set and the licence the // node holds, so what lands under /etc is tested without a machine. // // Three files, owned whole by this module: // managed-mcp.json the tool servers every session loads: the mesh's console as `mesh`, and the // servers the operator declared for the mesh or this node. Exclusive by the // vendor's rule — a server not listed here does not load — which is why the // list is the module's settings and nothing else (operator's choice, 2026-10-03). // managed-settings.json the mesh's keys only: the repositories' attribution convention, the // claude.ai connectors kept beside the managed servers, and — for an API-key // licence only — the key-helper. A person's preferences are theirs. // CLAUDE.md how a session on this mesh works, who this node is, the conventions. export const MANAGED_DIR = "/etc/claude-code"; export interface Facts { readonly node: string; readonly console: string; } export interface Settings { readonly role?: string; /** Extra tool servers, in the vendor's `.mcp.json` entry shape, keyed by name. */ readonly mcp_servers?: Readonly>>; } export interface Binding { readonly licence: string; readonly kind: "subscription" | "api-key"; } export interface Rendered { readonly [file: string]: string; } const MESH_ENTRY = "mesh"; export type Servers = Readonly>>; /** Whether an entry is one the vendor's managed file takes: a name of letters, digits, `-` and `_`, and * an http/sse server with a url or a stdio server with a command. Returns why not, or null. */ export function entryProblem(name: string, entry: Record): string | null { if (!/^[A-Za-z0-9_-]+$/.test(name)) return `"${name}" is not a name the agent takes: letters, digits, - and _`; if (name === MESH_ENTRY) return `"${MESH_ENTRY}" is the mesh's own entry`; const type = entry?.type ?? "stdio"; if (type === "http" || type === "sse" || type === "streamable-http") { return typeof entry.url === "string" && entry.url ? null : `an ${type} server needs a url`; } if (type === "stdio") return typeof entry.command === "string" && entry.command ? null : "a stdio server needs a command"; return `"${String(type)}" is not a server type the agent knows (http, sse, stdio)`; } /** * Compose the three files. `registered` is the module's own list on this node — what was registered * through its tools — laid over the servers the operator set in its settings. */ export function render(facts: Facts, settings: Settings, binding: Binding | null, helperPath: string, registered: Servers = {}): Rendered { const servers: Record = {}; for (const [name, entry] of Object.entries({ ...(settings.mcp_servers ?? {}), ...registered })) { if (entryProblem(name, entry) !== null) continue; // the mesh's own entry, or one the agent would refuse servers[name] = entry; } servers[MESH_ENTRY] = { type: "http", url: facts.console }; const managed: Record = { attribution: { commit: "", pr: "" }, allowAllClaudeAiMcps: true, }; if (binding?.kind === "api-key") managed.apiKeyHelper = helperPath; return { "managed-mcp.json": json({ mcpServers: sortKeys(servers) }), "managed-settings.json": json(managed), "CLAUDE.md": instructions(facts, settings), }; } function json(v: unknown): string { return JSON.stringify(v, null, 2) + "\n"; } function sortKeys(o: Record): Record { return Object.fromEntries(Object.keys(o).sort().map((k) => [k, o[k]])); } export function instructions(facts: Facts, settings: Settings): string { const role = settings.role?.trim() ? settings.role.trim() : "not stated — set it in this module's settings for the node"; return `# This machine is a node of a Novox mesh Written by the mesh's \`claude-code\` module. Edit the module's settings or the catalogue, never this file: it is rewritten whenever the module renders. ## Who this node is - **Node:** \`${facts.node}\` - **Role:** ${role} - The other nodes, their roles and what runs where: ask the controller (\`mesh-controller.nodes\`, \`mesh-controller.node\`). Nothing here lists them, because a copy drifts. ## How a session on this mesh works 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: 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. ## Hard rules - A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If unsure, \`mesh-controller.plan\` for the node says what the mesh writes there. - Never write to a store's database by hand; schema changes are numbered migrations. - Never push to a main branch: a branch, a pull request, and a human approval for every merge. - The mesh creates no symlinks, and nobody else does either. - A package is declared in a module, never installed by hand. ## Conventions - Commit messages are concise, in the imperative, about why. - Test before pushing: nodes update unattended. - The playbooks in the record say how research, decisions, designs, issues and hand-offs are done. `; }