The module owns /etc/claude-code: managed-mcp.json lists the console as `mesh` over HTTP on loopback plus the servers in its mcp_servers setting (exclusive, by the operator's choice — the https rule of managedMcpServers refuses a loopback console); managed-settings.json carries the attribution convention, keeps claude.ai connectors, and adds the key-helper only for an API-key licence; CLAUDE.md says how a session here works. Rendered whenever the runtime collects the tools, written only on change, through the operator account's sudo. Under the home, only the credentials file, only on a hand-over. Nothing declared under a home or /etc; the console's port comes from node-tools' mcp-endpoint (mesh-tools #34).
112 lines
4.8 KiB
TypeScript
112 lines
4.8 KiB
TypeScript
// 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<Record<string, Record<string, unknown>>>;
|
|
}
|
|
|
|
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<string, unknown> = {};
|
|
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<string, unknown> = {
|
|
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<string, unknown>): Record<string, unknown> {
|
|
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.
|
|
`;
|
|
}
|