Compare commits

..
5 Commits
Author SHA1 Message Date
jochen eab335b755 claude-code: launched over stdio (ADR 0193), the console's five tools in its instructions (ADR 0195)
Every bundle is now a child speaking MCP over stdio, so stdout is the channel: the module logs on
stderr. The managed CLAUDE.md teaches mesh_search, mesh_describe, mesh_call, mesh_overview and
mesh_machine with addresses (<seat>.<verb>, <node>/<module>.<tool>) instead of flat tool names.
A hand-over is applied whatever the trailing render says; a failed render is reported beside it.
Proven over stdio as the runtime drives it: five tools listed, a key made on first use, a sealed
switch writing an access-token-only 0600 credentials file that keeps unknown keys.
2026-10-03 23:41:01 +02:00
jochen f42b58f789 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).
2026-10-03 23:40:21 +02:00
jochen 5737752744 claude-code: the sealed hand-over, the credentials write with the lineage rule, the identity read (hq to-be 40 WP2, in progress)
The parts of the agent module that hold whichever way the console is registered: X25519 +
HKDF + AES-GCM from Node's own library so the bundle carries no dependency; the predecessor's
lineage rule (rotation only if newer, a re-issue adopted, a switch regardless) with its
incidents as tests; an atomic 0600 write that strips any refresh token and keeps keys it does not
know; the account read from the agent's own state file. Manifest and renderer follow.
2026-10-03 23:40:21 +02:00
mesh-admin efff54157b Merge pull request 'The seven say what the runtime loads from their tools bundle (hq ADR 0192)' (#243) from fix/0192-the-seven-say-what-the-runtime-loads into main 2026-10-03 13:58:27 +00:00
jochen 2000ec3f48 The seven say what the runtime loads from their tools bundle (hq ADR 0192)
None declares a tools list, so the composer had nothing saying the runtime loads from the bundle,
and delivered it nowhere: built, recorded, never sent. loads names tools/index.js.
2026-10-03 15:58:19 +02:00
10 changed files with 48 additions and 14 deletions
+3
View File
@@ -102,6 +102,9 @@
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_BASEROW_URL": "http://127.0.0.1:${port:80}",
"MESH_BASEROW_CONFIG_FILE": "${dir:mesh-state}/config.json"
+11 -6
View File
@@ -82,14 +82,19 @@ it is rewritten whenever the module renders.
## 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.
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 \`<seat>.<verb>\` (the mesh's own verbs
are \`mesh-controller.<verb>\`: \`status\`, \`plan\`, \`node\`, \`assign\`, \`push\`, \`settings\`);
a module on a machine is \`<node>/<module>.<tool>\`.
- \`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: \`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.
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.
+2 -1
View File
@@ -30,7 +30,8 @@ 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/);
assert.match(md, /mesh_call/);
assert.match(md, /records_search/);
});
test("rendering is deterministic, so an unchanged input writes nothing", () => {
+14 -7
View File
@@ -1,6 +1,7 @@
// 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.
// claude-code's tools (novox/hq design 36, ADR 0183). A bundle the node's runtime launches and speaks MCP
// to over stdio (ADR 0193), as the operator account; it 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. **stdout is the MCP channel**: everything this module says, it says on stderr.
//
// 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
@@ -122,8 +123,14 @@ function apply(p: Paths, args: Record<string, unknown>): Record<string, unknown>
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);
// An API-key binding adds the key-helper to the managed settings; a subscription takes it away. The
// licence is applied whatever the render says; a render that fails is reported beside it, not instead.
let rendered: string[] | { failed: string };
try {
rendered = renderNow(p);
} catch (err) {
rendered = { failed: err instanceof Error ? err.message : String(err) };
}
return { applied: true, licence: handed.licence, kind: handed.kind, source, rendered };
}
@@ -209,10 +216,10 @@ registerModuleTools("claude-code", (env) => {
if (!p) return [];
try {
keypair(p);
for (const line of renderNow(p)) if (!line.endsWith("unchanged")) console.log(`[claude-code] ${line}`);
for (const line of renderNow(p)) if (!line.endsWith("unchanged")) console.error(`[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)}`);
console.error(`[claude-code] ${err instanceof Error ? err.message : String(err)}`);
}
return getClaudeCodeTools(p);
});
+3
View File
@@ -33,6 +33,9 @@
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_CONFLUENCE_TOKEN_FILE": "${dir:state}/token",
"MESH_CONFLUENCE_CONFIG_FILE": "${dir:state}/config.json"
+3
View File
@@ -32,6 +32,9 @@
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_GITLAB_TOKEN_FILE": "${dir:state}/token",
"MESH_GITLAB_CONFIG_FILE": "${dir:state}/config.json"
+3
View File
@@ -32,6 +32,9 @@
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_JIRA_TOKEN_FILE": "${dir:state}/token",
"MESH_JIRA_CONFIG_FILE": "${dir:state}/config.json"
+3
View File
@@ -94,6 +94,9 @@
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_LETTA_URL": "http://127.0.0.1:${port:8283}",
"MESH_LETTA_CONFIG_FILE": "${dir:mesh-state}/config.json"
+3
View File
@@ -113,6 +113,9 @@
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_SEARXNG_URL": "http://127.0.0.1:${port:8080}",
"MESH_SEARXNG_CONFIG_FILE": "${dir:mesh-state}/config.json"
+3
View File
@@ -150,6 +150,9 @@
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_UNIFI_URL": "https://127.0.0.1:${port:8443}",
"MESH_UNIFI_CONFIG_FILE": "${dir:mesh-state}/config.json"