claude-code: the manifest, the managed directory and the tools (hq design 36, to-be 40 WP2)
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).
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# claude-code
|
||||
|
||||
The operator's agent on a machine (novox/hq design 36): its package, its machine-wide managed
|
||||
configuration, and the consumer side of the Anthropic licence manager (design 39, ADR 0183).
|
||||
|
||||
## What it writes
|
||||
|
||||
Under the agent's managed directory, `/etc/claude-code`, owned whole by this module and rewritten
|
||||
whenever the node's tool runtime collects the module's tools:
|
||||
|
||||
| file | holds |
|
||||
|---|---|
|
||||
| `managed-mcp.json` | the tool servers every session loads: the mesh's console as `mesh`, and the servers set in this module's `mcp_servers` setting. **Exclusive**: a server not listed here does not load — not one added with `claude mcp add`, not a project's `.mcp.json`, not a plugin's |
|
||||
| `managed-settings.json` | the repositories' attribution convention, the claude.ai connectors kept beside the managed servers, and the key-helper while the node holds an API-key licence |
|
||||
| `CLAUDE.md` | how a session on this mesh works, this node's name and role, the conventions |
|
||||
|
||||
Under the operator's home, only `~/.claude/.credentials.json`, and only when the licence manager hands
|
||||
this node a subscription token. Nothing else under the home is read or written.
|
||||
|
||||
## Settings
|
||||
|
||||
Per node or for the whole mesh, through `mesh-controller.settings module=claude-code`:
|
||||
|
||||
- `role` — what this node is, in a few words; shown to every session.
|
||||
- `mcp_servers` — extra tool servers, keyed by name, in the vendor's `.mcp.json` entry shape
|
||||
(`{"type":"http","url":…}` or `{"type":"stdio","command":…,"args":[…]}`). The name `mesh` is the
|
||||
module's own and cannot be set. Put a person's own servers here, or they stop loading.
|
||||
|
||||
## On a machine that carried the predecessor
|
||||
|
||||
Remove these by hand, once; the mesh removes nothing it did not make (ADR 0182):
|
||||
|
||||
- `~/.claude/CLAUDE.md`
|
||||
- `~/.claude/rules/00-hal-mesh.md`, `~/.claude/rules/conventions.md`
|
||||
- `~/.claude/skills/cleanup/`, `~/.claude/skills/hal-switch-license/`
|
||||
- the hand-made console entry in `~/.claude.json` under `mcpServers` — it is ignored now anyway
|
||||
|
||||
## Escalation
|
||||
|
||||
Writing `/etc/claude-code` needs root. The runtime runs as the operator account, and the module uses
|
||||
that account's passwordless `sudo`; on a machine without it, `claude_code_render` says so and nothing
|
||||
is written.
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"module": "claude-code",
|
||||
"version": "1",
|
||||
"slug": "agent",
|
||||
"capabilities": [
|
||||
"package-manager"
|
||||
],
|
||||
"requires": [
|
||||
"mcp-endpoint"
|
||||
],
|
||||
"binds": {
|
||||
"mcp-endpoint": "${dir:state}/mcp-endpoint.json"
|
||||
},
|
||||
"tools": [
|
||||
"claude_code_status",
|
||||
"claude_code_render",
|
||||
"claude_code_public_key",
|
||||
"claude_code_apply",
|
||||
"claude_code_pending_login"
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "package",
|
||||
"type": "package",
|
||||
"package": "claude-code"
|
||||
},
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"mode": "0700",
|
||||
"owner": "${machine:account}",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "facts",
|
||||
"type": "file",
|
||||
"path": "${dir:state}/facts.json",
|
||||
"mode": "0600",
|
||||
"owner": "${machine:account}",
|
||||
"content": "{\n \"node\": \"${machine:name}\",\n \"console\": \"http://127.0.0.1:${bound:mcp-endpoint:port}/mcp\"\n}\n"
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
"type": "file",
|
||||
"path": "${dir:state}/settings.json",
|
||||
"mode": "0600",
|
||||
"owner": "${machine:account}",
|
||||
"merge": "json",
|
||||
"content": "{\n \"role\": \"\",\n \"mcp_servers\": {}\n}\n"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "tools",
|
||||
"kind": "bundle",
|
||||
"language": "typescript",
|
||||
"entrypoints": [
|
||||
"tools/index.js"
|
||||
],
|
||||
"env": {
|
||||
"MESH_CLAUDE_CODE_STATE": "${dir:state}",
|
||||
"MESH_CLAUDE_CODE_FACTS": "${dir:state}/facts.json",
|
||||
"MESH_CLAUDE_CODE_SETTINGS": "${dir:state}/settings.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@novox/module-claude-code",
|
||||
"version": "0.1.0",
|
||||
"description": "claude-code — the operator's agent on a machine: its managed configuration, and the consumer side of the Anthropic licence manager (novox/hq design 36).",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc seal.ts grant.ts identity.ts render.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --rootDir . --outDir dist",
|
||||
"test": "npm run build && node --test --experimental-strip-types 'test/*.test.ts'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@novox/mesh-sdk": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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.
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { render } from "../dist/render.js";
|
||||
|
||||
const facts = { node: "workstation", console: "http://127.0.0.1:4270/mcp" };
|
||||
|
||||
test("the console is the `mesh` server, and an operator's servers are listed beside it", () => {
|
||||
const out = render(facts, { mcp_servers: { search: { type: "http", url: "https://s.example/mcp" } } }, null, "/h");
|
||||
const mcp = JSON.parse(out["managed-mcp.json"]);
|
||||
assert.deepEqual(Object.keys(mcp.mcpServers), ["mesh", "search"]);
|
||||
assert.deepEqual(mcp.mcpServers.mesh, { type: "http", url: facts.console });
|
||||
});
|
||||
|
||||
test("a setting cannot replace the mesh's own entry, and a name the vendor refuses is left out", () => {
|
||||
const out = render(facts, { mcp_servers: { mesh: { type: "http", url: "http://evil" }, "bad name": {} } }, null, "/h");
|
||||
const mcp = JSON.parse(out["managed-mcp.json"]);
|
||||
assert.equal(mcp.mcpServers.mesh.url, facts.console);
|
||||
assert.ok(!("bad name" in mcp.mcpServers));
|
||||
});
|
||||
|
||||
test("managed settings carry the mesh's keys only, and the key-helper only for an API-key licence", () => {
|
||||
const sub = JSON.parse(render(facts, {}, { licence: "personal", kind: "subscription" }, "/h")["managed-settings.json"]);
|
||||
assert.deepEqual(sub, { attribution: { commit: "", pr: "" }, allowAllClaudeAiMcps: true });
|
||||
const key = JSON.parse(render(facts, {}, { licence: "api", kind: "api-key" }, "/state/api-key-helper")["managed-settings.json"]);
|
||||
assert.equal(key.apiKeyHelper, "/state/api-key-helper");
|
||||
assert.ok(!("model" in key), "a preference is the person's");
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
test("rendering is deterministic, so an unchanged input writes nothing", () => {
|
||||
const s = { mcp_servers: { b: { type: "http", url: "https://b" }, a: { type: "http", url: "https://a" } } };
|
||||
assert.deepEqual(render(facts, s, null, "/h"), render(facts, s, null, "/h"));
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
// 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 = <T>(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<Facts | null>(p.facts, null);
|
||||
if (!facts?.console) throw new Error(`the mesh has not rendered ${p.facts} yet; nothing to write`);
|
||||
const settings = readJson<Settings>(p.settings, {});
|
||||
const binding = readJson<Binding | null>(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<string, unknown>): Record<string, unknown> {
|
||||
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<Binding | null>(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<string, unknown> {
|
||||
const binding = readJson<Binding | null>(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<Facts | null>(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<string, unknown>): Record<string, unknown> {
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["seal.ts", "grant.ts", "identity.ts", "render.ts", "tools/index.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user