// 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 function render(facts: Facts, settings: Settings, binding: Binding | null, helperPath: string): Rendered { const servers: Record = {}; for (const [name, entry] of Object.entries(settings.mcp_servers ?? {})) { if (name === MESH_ENTRY) continue; // the mesh's own entry is the mesh's; a setting cannot replace it if (!/^[A-Za-z0-9_-]+$/.test(name)) continue; 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\`. Its tools are the vocabulary. - **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. - **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. `; }