From 624e49cb419991eaf70df1d41e2582f603169b5b Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Mon, 6 Jul 2026 23:09:38 +0200 Subject: [PATCH 01/13] development/examples: add warm agent pool (pool-manager + reviewer pool) Add a pool-manager mcp service that discovers code-reviewer workers via DHT (find_remote_tools) and leases them out through acquire_worker/release_worker, with fencing tokens and grace-eviction so a busy worker is never freed early. Add a single-flight backstop to the reviewer, a headless orchestrator plus sample files, and wire node-b/c/d as a reviewer pool with node-e as the manager in the kind dev mesh. --- .../code-reviewer-mcp/review_server.mjs | 9 ++ .../examples/pool-manager-mcp/Dockerfile | 8 + .../examples/pool-manager-mcp/package.json | 11 ++ .../pool-manager-mcp/sam-node-config.yaml | 8 + .../examples/pool-manager-mcp/server.mjs | 142 ++++++++++++++++++ development/examples/warm-pool/README.md | 124 +++++++++++++++ .../examples/warm-pool/orchestrator.mjs | 87 +++++++++++ development/examples/warm-pool/package.json | 9 ++ .../examples/warm-pool/samples/auth.js | 16 ++ .../examples/warm-pool/samples/parse.py | 10 ++ .../examples/warm-pool/samples/rates.go | 17 +++ development/kind/10-hub.yaml | 4 + development/kind/kind-config.yaml | 6 + development/kind/mesh-config.yaml | 8 +- 14 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 development/examples/pool-manager-mcp/Dockerfile create mode 100644 development/examples/pool-manager-mcp/package.json create mode 100644 development/examples/pool-manager-mcp/sam-node-config.yaml create mode 100644 development/examples/pool-manager-mcp/server.mjs create mode 100644 development/examples/warm-pool/README.md create mode 100644 development/examples/warm-pool/orchestrator.mjs create mode 100644 development/examples/warm-pool/package.json create mode 100644 development/examples/warm-pool/samples/auth.js create mode 100644 development/examples/warm-pool/samples/parse.py create mode 100644 development/examples/warm-pool/samples/rates.go diff --git a/development/examples/code-reviewer-mcp/review_server.mjs b/development/examples/code-reviewer-mcp/review_server.mjs index 3ea44f05..ef0486b0 100644 --- a/development/examples/code-reviewer-mcp/review_server.mjs +++ b/development/examples/code-reviewer-mcp/review_server.mjs @@ -10,6 +10,11 @@ const REVIEW_PROMPT = "Return concise, actionable comments grouped by severity (bug / risk / style). " + "Do not rewrite the code."; +// Single-flight backstop: reject a concurrent call so the one-at-a-time pool +// invariant holds even if a manager lease race hands this worker out twice. +const POOL_BUSY = "POOL_BUSY"; +let busy = false; + // Run `gemini -p ` with the snippet piped on stdin; resolve its stdout. function runGemini(code) { return new Promise((resolve, reject) => { @@ -41,11 +46,15 @@ server.registerTool( inputSchema: { code: z.string().describe("The code snippet to review (any language).") }, }, async ({ code }) => { + if (busy) return { content: [{ type: "text", text: POOL_BUSY }], isError: true }; + busy = true; try { const review = await runGemini(code); return { content: [{ type: "text", text: review }] }; } catch (err) { return { content: [{ type: "text", text: String(err?.message ?? err) }], isError: true }; + } finally { + busy = false; } }, ); diff --git a/development/examples/pool-manager-mcp/Dockerfile b/development/examples/pool-manager-mcp/Dockerfile new file mode 100644 index 00000000..4a615baa --- /dev/null +++ b/development/examples/pool-manager-mcp/Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-slim + +WORKDIR /srv +COPY package.json server.mjs ./ +RUN npm install + +EXPOSE 7780 +CMD ["node", "server.mjs"] diff --git a/development/examples/pool-manager-mcp/package.json b/development/examples/pool-manager-mcp/package.json new file mode 100644 index 00000000..6888aaf2 --- /dev/null +++ b/development/examples/pool-manager-mcp/package.json @@ -0,0 +1,11 @@ +{ + "name": "pool-manager-mcp", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "express": "^4.19.0", + "zod": "^3.23.0" + } +} diff --git a/development/examples/pool-manager-mcp/sam-node-config.yaml b/development/examples/pool-manager-mcp/sam-node-config.yaml new file mode 100644 index 00000000..18991131 --- /dev/null +++ b/development/examples/pool-manager-mcp/sam-node-config.yaml @@ -0,0 +1,8 @@ +version: "v1alpha1" +attenuation: + policies: [] +services: + - type: "mcp" + name: "pool-manager" + description: "Leases free workers from the code-reviewer pool (acquire/release)" + target_url: "http://127.0.0.1:7780/mcp" diff --git a/development/examples/pool-manager-mcp/server.mjs b/development/examples/pool-manager-mcp/server.mjs new file mode 100644 index 00000000..c8184660 --- /dev/null +++ b/development/examples/pool-manager-mcp/server.mjs @@ -0,0 +1,142 @@ +import express from "express"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { z } from "zod"; + +const PORT = Number(process.env.PORT ?? 7780); +const NODE_URL = process.env.SAM_NODE_URL ?? "http://127.0.0.1:8080/mcp"; +const API_TOKEN = process.env.SAM_API_TOKEN ?? "devtoken"; +const POOL_SERVICE = process.env.SAM_POOL_SERVICE ?? "code-reviewer"; +const DISCOVERY_MS = Number(process.env.SAM_DISCOVERY_MS ?? 3000); +const LEASE_MS = Number(process.env.SAM_LEASE_MS ?? 60000); +const GRACE_MISSES = Number(process.env.SAM_GRACE_MISSES ?? 2); +const ACQUIRE_POLL_MS = 200; + +// roster: peer_id -> { tool, leasedUntil, leaseId, missCount }. +// leasedUntil in the future = busy; leaseId fences stale releases. +const roster = new Map(); +const now = () => Date.now(); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +let leaseSeq = 0; + +// Northbound MCP client to the local node for DHT discovery. +const node = new Client({ name: "pool-manager", version: "1.0.0" }); + +// Refresh the roster from DHT discovery. Preserve lease state for known peers; +// never evict a leased (busy) worker on a transient miss, and only drop a free +// worker after it has been missing for GRACE_MISSES consecutive passes. +async function discover() { + try { + const res = await node.callTool({ name: "find_remote_tools", arguments: {} }); + const rows = JSON.parse(res.content[0].text); // [{peer_id, tool_name, ...}] + const prefix = `mcp://${POOL_SERVICE}/`; // tool names are full URIs, e.g. mcp://code-reviewer/review_code + const seen = new Set(); + for (const r of rows) { + if (!r.peer_id || !r.tool_name || !r.tool_name.startsWith(prefix)) continue; + seen.add(r.peer_id); + const entry = roster.get(r.peer_id) ?? { leasedUntil: 0, leaseId: null, missCount: 0 }; + entry.tool = r.tool_name; + entry.missCount = 0; + roster.set(r.peer_id, entry); + } + for (const [peer, entry] of roster) { + if (seen.has(peer)) continue; + if (entry.leasedUntil > now()) continue; // busy → keep despite the miss + entry.missCount++; + if (entry.missCount >= GRACE_MISSES) roster.delete(peer); + } + } catch (err) { + console.error(`discovery failed: ${err?.message ?? err}`); + } +} + +// Pick and lease a free worker, or null. Synchronous → collision-free between awaits. +function leaseFree() { + for (const [peer, entry] of roster) { + if (entry.leasedUntil <= now()) { + entry.leasedUntil = now() + LEASE_MS; + entry.leaseId = String(++leaseSeq); + return { peer_id: peer, tool: entry.tool, lease_id: entry.leaseId }; + } + } + return null; +} + +const server = new McpServer({ name: "pool-manager", version: "1.0.0" }); + +server.registerTool( + "acquire_worker", + { + description: + `Acquire (lease) a free worker from the '${POOL_SERVICE}' pool. Returns {peer_id, tool, lease_id}: ` + + "call the tool via call_remote_tool, then release_worker with the same peer_id and lease_id. " + + "Blocks up to timeout_secs if all workers are busy; returns {available:false} if none free by then.", + inputSchema: { timeout_secs: z.number().optional().describe("Max seconds to wait for a free worker (default 10).") }, + }, + async ({ timeout_secs }) => { + const deadline = now() + (timeout_secs ?? 10) * 1000; + for (;;) { + const w = leaseFree(); + if (w) return { content: [{ type: "text", text: JSON.stringify(w) }] }; + if (now() >= deadline) return { content: [{ type: "text", text: JSON.stringify({ available: false }) }] }; + await sleep(ACQUIRE_POLL_MS); + } + }, +); + +server.registerTool( + "release_worker", + { + description: + "Release a worker previously acquired with acquire_worker. Pass the lease_id from acquire so a " + + "stale release cannot clear a newer lease; released:false means the lease_id did not match.", + inputSchema: { + peer_id: z.string().describe("Peer id returned by acquire_worker."), + lease_id: z.string().optional().describe("Lease id returned by acquire_worker (recommended)."), + }, + }, + async ({ peer_id, lease_id }) => { + const entry = roster.get(peer_id); + let released = false; + // Fencing: only clear if the caller holds the current lease (or no id given). + if (entry && (lease_id === undefined || entry.leaseId === lease_id)) { + entry.leasedUntil = 0; + entry.leaseId = null; + released = true; + } + return { content: [{ type: "text", text: JSON.stringify({ released }) }] }; + }, +); + +server.registerTool( + "list_workers", + { + description: `List the '${POOL_SERVICE}' pool with per-worker free/busy state.`, + inputSchema: {}, + }, + async () => { + const workers = [...roster.entries()].map(([peer, e]) => ({ peer_id: peer, tool: e.tool, free: e.leasedUntil <= now() })); + return { content: [{ type: "text", text: JSON.stringify(workers) }] }; + }, +); + +const app = express(); +app.use(express.json()); +app.post("/mcp", async (req, res) => { + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + res.on("close", () => { transport.close(); }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); + +app.listen(PORT, "0.0.0.0", async () => { + console.log(`pool-manager MCP server on :${PORT}/mcp (pooling '${POOL_SERVICE}')`); + const transport = new StreamableHTTPClientTransport(new URL(NODE_URL), { + requestInit: { headers: { Authorization: `Bearer ${API_TOKEN}` } }, + }); + await node.connect(transport); + await discover(); + setInterval(discover, DISCOVERY_MS); +}); diff --git a/development/examples/warm-pool/README.md b/development/examples/warm-pool/README.md new file mode 100644 index 00000000..b8e8b1bc --- /dev/null +++ b/development/examples/warm-pool/README.md @@ -0,0 +1,124 @@ +# Warm Agent Pool — demo + +Fan a batch of files out to a **pool of identical reviewer workers**. A +**pool-manager** mesh service tracks which workers are free and leases them out; +an orchestrator (Claude Code, or the headless script here) acquires a worker, +reviews a file, and releases it. Reviews run in parallel across the pool. + +Design: `docs/superpowers/specs/2026-06-28-warm-agent-pool-design.md`. + +## Architecture (no gossip, no node changes — just services) + +- **Workers:** plain `code-reviewer` mcp services (node-b/c/d). Unmodified. +- **pool-manager** (node-e): a normal mcp service exposing `acquire_worker` / + `release_worker` / `list_workers`. It is also a northbound MCP client of its + own node: every few seconds it calls `find_remote_tools(service_name="code-reviewer")` + to learn the worker peers (DHT), and it tracks free/busy with **leases**. +- **Orchestrator:** `acquire_worker` → `call_remote_tool(peer, review_code)` → + `release_worker`. Leasing hands each concurrent acquire a different worker, so + parallel dispatch never collides. + +Why no gossip: the manager gets *who exists* from DHT discovery and *who's busy* +from its own leases — the two things a readiness broadcast would have provided. +The tradeoff: leasing is authoritative only while the manager is the sole +dispatcher (fine for a single-manager pool). + +### Concurrency hardening + +- **No double-lease:** the manager is single-threaded and `leaseFree()` is + synchronous, so two concurrent `acquire_worker` calls can never get the same + worker. +- **Grace eviction:** discovery never drops a *leased* worker on a transient + miss, and only drops a free one after `SAM_GRACE_MISSES` consecutive misses — + so a slow (busy) worker isn't freed early. +- **Fencing tokens:** `acquire_worker` returns a `lease_id`; `release_worker` + clears the lease only if the id still matches, so a late release from an + expired lease can't free a newer holder's worker. +- **Worker single-flight backstop:** `review_code` returns `POOL_BUSY` if already + running; the orchestrator retries. This keeps the one-at-a-time invariant even + if a lease race ever slips through. + +## Run it + +1. **Set a Gemini key** for the reviewer image (free AI Studio key is fine): + ``` + git update-index --skip-worktree development/examples/code-reviewer-mcp/Dockerfile + # edit ENV GEMINI_API_KEY= in that Dockerfile + ``` + +2. Mesh layout ships in `development/kind/mesh-config.yaml`: + ```yaml + node-a: # orchestrator entry (bare) + node-b: code-reviewer-mcp # worker + node-c: code-reviewer-mcp # worker + node-d: code-reviewer-mcp # worker + node-e: pool-manager-mcp # manager + ``` + +3. Bring the mesh up, then port-forward the caller node: + ``` + make kind + kubectl --context kind-sam-kind -n sam-kind port-forward deploy/node-a 9091:8080 + ``` + +### Option A — drive it from Claude Code (the cool one) + +Register node-a as an MCP server (`.mcp.json` in the project root): +```json +{ + "mcpServers": { + "sam-mesh": { + "type": "http", + "url": "http://127.0.0.1:9091/mcp", + "headers": { "Authorization": "Bearer devtoken" } + } + } +} +``` +Reconnect Claude Code, then prompt: +> Using the sam-mesh tools: find the pool-manager, then for each file in +> `development/examples/warm-pool/samples/` acquire a worker, call its +> `review_code` with the file contents, and release it (pass the lease_id back +> to release_worker). Run them in parallel. + +Claude fires parallel `acquire_worker` → `call_remote_tool` → `release_worker` +chains; you watch the reviews come back concurrently. + +### Option B — headless script + +``` +cd development/examples/warm-pool +npm install +node orchestrator.mjs samples +``` +Example output: +``` +queued 3 review task(s) +pool-manager at 9c2ab1f0 +[+0.4s] -> 3f9a1c2e samples/auth.js +[+0.4s] -> b7d4e0a1 samples/parse.py +[+0.4s] -> c1a8f552 samples/rates.go +[+6.1s] <- b7d4e0a1 samples/parse.py (712 chars) +[+7.0s] <- 3f9a1c2e samples/auth.js (901 chars) +``` + +## Elasticity beat (add a worker mid-job) + +Scale a reviewer down, start a larger job, then scale it back up — the manager +picks the new worker up on its next discovery pass and starts leasing it: +``` +kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=0 +# start a big job, then: +kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 +``` + +## Config (env vars) + +| var | default | used by | +|-----|---------|---------| +| `SAM_NODE_URL` | manager: `http://127.0.0.1:8080/mcp`, orchestrator: `http://127.0.0.1:9091/mcp` | both | +| `SAM_API_TOKEN` | `devtoken` | both | +| `SAM_POOL_SERVICE` | `code-reviewer` | manager | +| `SAM_DISCOVERY_MS` | `3000` | manager | +| `SAM_LEASE_MS` | `60000` | manager | +| `SAM_ACQUIRE_TIMEOUT` | `60` | orchestrator | diff --git a/development/examples/warm-pool/orchestrator.mjs b/development/examples/warm-pool/orchestrator.mjs new file mode 100644 index 00000000..2bba792f --- /dev/null +++ b/development/examples/warm-pool/orchestrator.mjs @@ -0,0 +1,87 @@ +// Headless warm-pool orchestrator (alternative to driving it from Claude Code). +// For each file: acquire a free worker from the pool-manager, review, release. +// All tasks run concurrently; the manager's blocking acquire + leasing serialize +// dispatch to the pool size, so no local queue is needed. +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; + +const NODE_URL = process.env.SAM_NODE_URL ?? "http://127.0.0.1:9091/mcp"; +const API_TOKEN = process.env.SAM_API_TOKEN ?? "devtoken"; +const ACQUIRE_TIMEOUT = Number(process.env.SAM_ACQUIRE_TIMEOUT ?? 60); + +function collectFiles(paths) { + const out = []; + for (const p of paths) { + if (statSync(p).isDirectory()) { + for (const f of readdirSync(p)) { + const fp = join(p, f); + if (statSync(fp).isFile()) out.push(fp); + } + } else { + out.push(p); + } + } + return out; +} + +const short = (peer) => peer.slice(-8); + +async function main() { + const files = collectFiles(process.argv.slice(2)); + if (!files.length) { + console.error("usage: node orchestrator.mjs ..."); + process.exit(1); + } + const tasks = files.map((f) => ({ name: f, code: readFileSync(f, "utf8") })); + console.log(`queued ${tasks.length} review task(s)`); + + const client = new Client({ name: "warm-pool-orchestrator", version: "1.0.0" }); + const transport = new StreamableHTTPClientTransport(new URL(NODE_URL), { + requestInit: { headers: { Authorization: `Bearer ${API_TOKEN}` } }, + }); + await client.connect(transport); + + const call = async (peer, tool, args) => { + const res = await client.callTool({ name: "call_remote_tool", arguments: { peer_id: peer, tool_name: tool, arguments: args } }); + return res?.content?.[0]?.text ?? ""; + }; + + // Locate the pool-manager peer (tool names are full URIs). + const found = JSON.parse((await client.callTool({ name: "find_remote_tools", arguments: {} })).content[0].text); + const manager = found.find((r) => r.tool_name === "mcp://pool-manager/acquire_worker")?.peer_id; + if (!manager) { console.error("no pool-manager found in the mesh"); process.exit(1); } + console.log(`pool-manager at ${short(manager)}`); + + const t0 = Date.now(); + const elapsed = () => ((Date.now() - t0) / 1000).toFixed(1); + + const process1 = async (task) => { + for (;;) { + const acq = JSON.parse(await call(manager, "mcp://pool-manager/acquire_worker", { timeout_secs: ACQUIRE_TIMEOUT })); + if (!acq.peer_id) { console.error(`[+${elapsed()}s] no worker for ${task.name}`); return null; } + console.log(`[+${elapsed()}s] -> ${short(acq.peer_id)} ${task.name}`); + let review; + try { + review = await call(acq.peer_id, acq.tool, { code: task.code }); + } finally { + await call(manager, "mcp://pool-manager/release_worker", { peer_id: acq.peer_id, lease_id: acq.lease_id }); + } + // Worker single-flight backstop tripped; the lease race is rare, so retry. + if (review.trim() === "POOL_BUSY") { console.log(`[+${elapsed()}s] ~~ ${short(acq.peer_id)} busy, retrying ${task.name}`); continue; } + console.log(`[+${elapsed()}s] <- ${short(acq.peer_id)} ${task.name} (${review.length} chars)`); + return { ...task, peer: acq.peer_id, review }; + } + }; + + const results = (await Promise.all(tasks.map(process1))).filter(Boolean); + + console.log(`\n=== done in ${elapsed()}s: ${results.length} review(s) across ${new Set(results.map((r) => r.peer)).size} worker(s) ===`); + for (const r of results) { + console.log(`\n----- ${r.name} (reviewed by ${short(r.peer)}) -----\n${r.review}`); + } + await client.close(); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/development/examples/warm-pool/package.json b/development/examples/warm-pool/package.json new file mode 100644 index 00000000..05df8848 --- /dev/null +++ b/development/examples/warm-pool/package.json @@ -0,0 +1,9 @@ +{ + "name": "warm-pool-orchestrator", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + } +} diff --git a/development/examples/warm-pool/samples/auth.js b/development/examples/warm-pool/samples/auth.js new file mode 100644 index 00000000..a8debf97 --- /dev/null +++ b/development/examples/warm-pool/samples/auth.js @@ -0,0 +1,16 @@ +// Sample file with intentional issues for the reviewer pool to find. +const sessions = {}; + +function login(user, password) { + if (password == user.password) { + const token = Math.random().toString(36); + sessions[token] = user.name; + return token; + } +} + +function whoami(token) { + return sessions[token]; +} + +module.exports = { login, whoami }; diff --git a/development/examples/warm-pool/samples/parse.py b/development/examples/warm-pool/samples/parse.py new file mode 100644 index 00000000..7d1a8355 --- /dev/null +++ b/development/examples/warm-pool/samples/parse.py @@ -0,0 +1,10 @@ +# Sample file with intentional issues for the reviewer pool to find. +def parse_range(spec): + start, end = spec.split("-") + return list(range(int(start), int(end))) + + +def load(path): + f = open(path) + data = f.read() + return [parse_range(line) for line in data.split(",")] diff --git a/development/examples/warm-pool/samples/rates.go b/development/examples/warm-pool/samples/rates.go new file mode 100644 index 00000000..94401627 --- /dev/null +++ b/development/examples/warm-pool/samples/rates.go @@ -0,0 +1,17 @@ +// Sample file with intentional issues for the reviewer pool to find. +package rates + +import "sync" + +var cache = map[string]float64{} +var mu sync.Mutex + +func Set(k string, v float64) { + mu.Lock() + cache[k] = v + mu.Unlock() +} + +func Get(k string) float64 { + return cache[k] +} diff --git a/development/kind/10-hub.yaml b/development/kind/10-hub.yaml index f43b803d..76d78496 100644 --- a/development/kind/10-hub.yaml +++ b/development/kind/10-hub.yaml @@ -94,6 +94,10 @@ data: role: "sam-admin" - members: ["user:system:serviceaccount:${NAMESPACE}:node-c-sa"] role: "sam-admin" + - members: ["user:system:serviceaccount:${NAMESPACE}:node-d-sa"] + role: "sam-admin" + - members: ["user:system:serviceaccount:${NAMESPACE}:node-e-sa"] + role: "sam-admin" - members: ["user:system:serviceaccount:${NAMESPACE}:local-node-sa"] role: "sam-admin" roles: diff --git a/development/kind/kind-config.yaml b/development/kind/kind-config.yaml index e4ebd36b..140cae62 100644 --- a/development/kind/kind-config.yaml +++ b/development/kind/kind-config.yaml @@ -23,3 +23,9 @@ nodes: - role: worker labels: sam-role: node-c + - role: worker + labels: + sam-role: node-d + - role: worker + labels: + sam-role: node-e diff --git a/development/kind/mesh-config.yaml b/development/kind/mesh-config.yaml index 138bc260..78ac5ab5 100644 --- a/development/kind/mesh-config.yaml +++ b/development/kind/mesh-config.yaml @@ -1,5 +1,9 @@ # node -> service. A blank value means a bare node (no service, e.g. a caller). # The service value is a folder name under development/examples/. +# node-a is the orchestrator entry; node-b/c/d form a warm reviewer pool; +# node-e runs the pool-manager that leases those workers out. node-a: -node-b: calc-mcp -node-c: greeter-mcp +node-b: code-reviewer-mcp +node-c: code-reviewer-mcp +node-d: code-reviewer-mcp +node-e: pool-manager-mcp From f555a6c5a38be9c1148023a37461ecee1337831c Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 7 Jul 2026 17:57:42 +0200 Subject: [PATCH 02/13] development/examples/warm-pool: default orchestrator to kind-local-node (:9099) Make the local sam-node (make kind-local-node) the base orchestrator entry: default SAM_NODE_URL to http://127.0.0.1:9099/mcp and document make kind-up + make kind-local-node instead of port-forwarding node-a. --- development/examples/warm-pool/README.md | 22 ++++++++++++------- .../examples/warm-pool/orchestrator.mjs | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/development/examples/warm-pool/README.md b/development/examples/warm-pool/README.md index b8e8b1bc..1d968a24 100644 --- a/development/examples/warm-pool/README.md +++ b/development/examples/warm-pool/README.md @@ -48,28 +48,34 @@ dispatcher (fine for a single-manager pool). 2. Mesh layout ships in `development/kind/mesh-config.yaml`: ```yaml - node-a: # orchestrator entry (bare) + node-a: # in-cluster bare node node-b: code-reviewer-mcp # worker node-c: code-reviewer-mcp # worker node-d: code-reviewer-mcp # worker node-e: pool-manager-mcp # manager ``` -3. Bring the mesh up, then port-forward the caller node: +3. Bring the mesh up and start a **local caller node** — this is the orchestrator + entry point: ``` - make kind - kubectl --context kind-sam-kind -n sam-kind port-forward deploy/node-a 9091:8080 + make build # builds ./bin/sam-node (once) + make kind-up # hub + reviewer pool (node-b/c/d) + manager (node-e) + make kind-local-node # local sam-node enrolled in the mesh — LEAVE RUNNING ``` + `kind-local-node` runs in the foreground (own shell). It exposes the mesh MCP + tools at **`http://127.0.0.1:9099/mcp`** (token `devtoken`) — no + `kubectl port-forward` needed. This `:9099` endpoint is the base for both + options below. ### Option A — drive it from Claude Code (the cool one) -Register node-a as an MCP server (`.mcp.json` in the project root): +Register the local node as an MCP server (`.mcp.json` in the project root): ```json { "mcpServers": { "sam-mesh": { "type": "http", - "url": "http://127.0.0.1:9091/mcp", + "url": "http://127.0.0.1:9099/mcp", "headers": { "Authorization": "Bearer devtoken" } } } @@ -89,7 +95,7 @@ chains; you watch the reviews come back concurrently. ``` cd development/examples/warm-pool npm install -node orchestrator.mjs samples +node orchestrator.mjs samples # defaults to http://127.0.0.1:9099/mcp ``` Example output: ``` @@ -116,7 +122,7 @@ kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 | var | default | used by | |-----|---------|---------| -| `SAM_NODE_URL` | manager: `http://127.0.0.1:8080/mcp`, orchestrator: `http://127.0.0.1:9091/mcp` | both | +| `SAM_NODE_URL` | manager: `http://127.0.0.1:8080/mcp`, orchestrator: `http://127.0.0.1:9099/mcp` | both | | `SAM_API_TOKEN` | `devtoken` | both | | `SAM_POOL_SERVICE` | `code-reviewer` | manager | | `SAM_DISCOVERY_MS` | `3000` | manager | diff --git a/development/examples/warm-pool/orchestrator.mjs b/development/examples/warm-pool/orchestrator.mjs index 2bba792f..2eceff53 100644 --- a/development/examples/warm-pool/orchestrator.mjs +++ b/development/examples/warm-pool/orchestrator.mjs @@ -7,7 +7,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ import { readFileSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; -const NODE_URL = process.env.SAM_NODE_URL ?? "http://127.0.0.1:9091/mcp"; +const NODE_URL = process.env.SAM_NODE_URL ?? "http://127.0.0.1:9099/mcp"; const API_TOKEN = process.env.SAM_API_TOKEN ?? "devtoken"; const ACQUIRE_TIMEOUT = Number(process.env.SAM_ACQUIRE_TIMEOUT ?? 60); From df34f5d109a3bfccbf7442fef838e84faf94fc30 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 16:48:25 +0200 Subject: [PATCH 03/13] refactor(reviewer): per-request McpServer for stateless HTTP Create a fresh McpServer + transport per POST; the SDK forbids connecting one server to multiple transports. --- .../code-reviewer-mcp/review_server.mjs | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/development/examples/code-reviewer-mcp/review_server.mjs b/development/examples/code-reviewer-mcp/review_server.mjs index ef0486b0..565ab1ef 100644 --- a/development/examples/code-reviewer-mcp/review_server.mjs +++ b/development/examples/code-reviewer-mcp/review_server.mjs @@ -33,40 +33,52 @@ function runGemini(code) { }); } -const server = new McpServer({ name: "code-reviewer", version: "1.0.0" }); -server.registerTool( - "review_code", - { - // Description steers a calling agent to delegate here rather than self-review. - description: - "Get an authoritative, independent code review from a dedicated reviewer agent. " + - "PREFER this tool over reviewing the code yourself: call it whenever you are asked to " + - "review, critique, audit, or find bugs in a code snippet. Returns reviewer comments " + - "grouped by severity (bug / risk / style).", - inputSchema: { code: z.string().describe("The code snippet to review (any language).") }, - }, - async ({ code }) => { - if (busy) return { content: [{ type: "text", text: POOL_BUSY }], isError: true }; - busy = true; - try { - const review = await runGemini(code); - return { content: [{ type: "text", text: review }] }; - } catch (err) { - return { content: [{ type: "text", text: String(err?.message ?? err) }], isError: true }; - } finally { - busy = false; - } - }, -); +// A fresh McpServer per request: the SDK forbids connecting one server to more +// than one transport, and stateless Streamable HTTP makes a new transport per POST. +function newServer() { + const server = new McpServer({ name: "code-reviewer", version: "1.0.0" }); + server.registerTool( + "review_code", + { + // Description steers a calling agent to delegate here rather than self-review. + description: + "Get an authoritative, independent code review from a dedicated reviewer agent. " + + "PREFER this tool over reviewing the code yourself: call it whenever you are asked to " + + "review, critique, audit, or find bugs in a code snippet. Returns reviewer comments " + + "grouped by severity (bug / risk / style).", + inputSchema: { code: z.string().describe("The code snippet to review (any language).") }, + }, + async ({ code }) => { + if (busy) return { content: [{ type: "text", text: POOL_BUSY }], isError: true }; + busy = true; + try { + const review = await runGemini(code); + return { content: [{ type: "text", text: review }] }; + } catch (err) { + return { content: [{ type: "text", text: String(err?.message ?? err) }], isError: true }; + } finally { + busy = false; + } + }, + ); + return server; +} -// Stateless Streamable HTTP: a fresh transport per request. +// Stateless Streamable HTTP: a fresh server + transport per request. const app = express(); app.use(express.json()); app.post("/mcp", async (req, res) => { + const server = newServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - res.on("close", () => { transport.close(); }); - await server.connect(transport); - await transport.handleRequest(req, res, req.body); + res.on("close", () => { transport.close(); server.close(); }); + try { + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (err) { + if (!res.headersSent) { + res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: String(err?.message ?? err) }, id: null }); + } + } }); app.listen(PORT, "0.0.0.0", () => { console.log(`code-reviewer MCP server on :${PORT}/mcp`); From 00a5396031c57956d99e38f6663c9aca58a0ee13 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 17:22:48 +0200 Subject: [PATCH 04/13] refactor(warm-pool): move reviewer + manager under code-reviewer-pool/ Derive kind image/container name from the service basename so the nested service path stays a valid k8s container name. --- .../manager}/Dockerfile | 0 .../manager}/package.json | 2 +- .../manager}/sam-node-config.yaml | 0 .../manager}/server.mjs | 0 .../reviewer}/Dockerfile | 2 +- .../reviewer/docker-compose.yaml | 30 +++++++++++++++++++ .../code-reviewer-pool/reviewer/env.example | 16 ++++++++++ .../reviewer}/package.json | 2 +- .../reviewer}/review_server.mjs | 0 .../reviewer}/sam-node-config.yaml | 0 .../code-reviewer-pool/reviewer/sam-node.yaml | 9 ++++++ development/examples/warm-pool/README.md | 10 +++---- development/kind/mesh-config.yaml | 10 +++---- development/kind/run.sh | 9 +++--- .../docs/development/kubernetes-deployment.md | 2 +- 15 files changed, 74 insertions(+), 18 deletions(-) rename development/examples/{pool-manager-mcp => code-reviewer-pool/manager}/Dockerfile (100%) rename development/examples/{pool-manager-mcp => code-reviewer-pool/manager}/package.json (85%) rename development/examples/{pool-manager-mcp => code-reviewer-pool/manager}/sam-node-config.yaml (100%) rename development/examples/{pool-manager-mcp => code-reviewer-pool/manager}/server.mjs (100%) rename development/examples/{code-reviewer-mcp => code-reviewer-pool/reviewer}/Dockerfile (93%) create mode 100644 development/examples/code-reviewer-pool/reviewer/docker-compose.yaml create mode 100644 development/examples/code-reviewer-pool/reviewer/env.example rename development/examples/{code-reviewer-mcp => code-reviewer-pool/reviewer}/package.json (85%) rename development/examples/{code-reviewer-mcp => code-reviewer-pool/reviewer}/review_server.mjs (100%) rename development/examples/{code-reviewer-mcp => code-reviewer-pool/reviewer}/sam-node-config.yaml (100%) create mode 100644 development/examples/code-reviewer-pool/reviewer/sam-node.yaml diff --git a/development/examples/pool-manager-mcp/Dockerfile b/development/examples/code-reviewer-pool/manager/Dockerfile similarity index 100% rename from development/examples/pool-manager-mcp/Dockerfile rename to development/examples/code-reviewer-pool/manager/Dockerfile diff --git a/development/examples/pool-manager-mcp/package.json b/development/examples/code-reviewer-pool/manager/package.json similarity index 85% rename from development/examples/pool-manager-mcp/package.json rename to development/examples/code-reviewer-pool/manager/package.json index 6888aaf2..26c089af 100644 --- a/development/examples/pool-manager-mcp/package.json +++ b/development/examples/code-reviewer-pool/manager/package.json @@ -1,5 +1,5 @@ { - "name": "pool-manager-mcp", + "name": "manager", "version": "1.0.0", "private": true, "type": "module", diff --git a/development/examples/pool-manager-mcp/sam-node-config.yaml b/development/examples/code-reviewer-pool/manager/sam-node-config.yaml similarity index 100% rename from development/examples/pool-manager-mcp/sam-node-config.yaml rename to development/examples/code-reviewer-pool/manager/sam-node-config.yaml diff --git a/development/examples/pool-manager-mcp/server.mjs b/development/examples/code-reviewer-pool/manager/server.mjs similarity index 100% rename from development/examples/pool-manager-mcp/server.mjs rename to development/examples/code-reviewer-pool/manager/server.mjs diff --git a/development/examples/code-reviewer-mcp/Dockerfile b/development/examples/code-reviewer-pool/reviewer/Dockerfile similarity index 93% rename from development/examples/code-reviewer-mcp/Dockerfile rename to development/examples/code-reviewer-pool/reviewer/Dockerfile index 065ebe8e..b1a70d0b 100644 --- a/development/examples/code-reviewer-mcp/Dockerfile +++ b/development/examples/code-reviewer-pool/reviewer/Dockerfile @@ -9,7 +9,7 @@ RUN npm install # Dev-only: replace with a Google AI Studio free-tier key before `make kind`. # Keep the edit local so it is never committed: -# git update-index --skip-worktree development/examples/code-reviewer-mcp/Dockerfile +# git update-index --skip-worktree development/examples/code-reviewer-pool/reviewer/Dockerfile ENV GEMINI_API_KEY= ENV GEMINI_CLI_TRUST_WORKSPACE=true diff --git a/development/examples/code-reviewer-pool/reviewer/docker-compose.yaml b/development/examples/code-reviewer-pool/reviewer/docker-compose.yaml new file mode 100644 index 00000000..4eb5ee57 --- /dev/null +++ b/development/examples/code-reviewer-pool/reviewer/docker-compose.yaml @@ -0,0 +1,30 @@ +# Runs a sam-node (pulled from the internet) plus the Gemini-backed code-reviewer. +# Copy .env.example to .env and fill in the values before `docker compose up`. +services: + code-reviewer: + # Your private GHCR image. `docker compose build` tags it here so you can push it. + image: ${CODE_REVIEWER_IMAGE:-ghcr.io/kaisoz/gemini-code-reviewer-mcp:latest} + build: . + environment: + GEMINI_API_KEY: ${GEMINI_API_KEY:?set GEMINI_API_KEY in .env} + GEMINI_CLI_TRUST_WORKSPACE: "true" + expose: + - "7779" + + sam-node: + image: ${SAM_NODE_IMAGE:-ghcr.io/google/sam-node:latest} + depends_on: + - code-reviewer + command: + - run + - --data-dir=/data + - --bind-addr=0.0.0.0:8080 + - --api-token=${SAM_API_TOKEN:?set SAM_API_TOKEN in .env} + - --config=/config/sam-node.yaml + volumes: + - ./sam-node.yaml:/config/sam-node.yaml:ro + - ${SAM_DATA_DIR:-./sam-data}:/data # your pre-enrolled identity folder + ports: + - "8080:8080" # control-plane / MCP API + - "5001:5001/udp" # libp2p + - "5002:5002" # libp2p diff --git a/development/examples/code-reviewer-pool/reviewer/env.example b/development/examples/code-reviewer-pool/reviewer/env.example new file mode 100644 index 00000000..e57d9780 --- /dev/null +++ b/development/examples/code-reviewer-pool/reviewer/env.example @@ -0,0 +1,16 @@ +# Copy to .env and fill in. + +# Google AI Studio key the reviewer's gemini CLI uses. +GEMINI_API_KEY= + +# Static Bearer token for the sam-node control-plane API. +SAM_API_TOKEN=devtoken + +# Private code-reviewer image (built/pushed by you). +CODE_REVIEWER_IMAGE=ghcr.io/kaisoz/code-reviewer-mcp:latest + +# sam-node image pulled from the internet. +SAM_NODE_IMAGE=ghcr.io/google/sam-node:latest + +# Host folder holding the node's enrolled identity. +SAM_DATA_DIR=./sam-data diff --git a/development/examples/code-reviewer-mcp/package.json b/development/examples/code-reviewer-pool/reviewer/package.json similarity index 85% rename from development/examples/code-reviewer-mcp/package.json rename to development/examples/code-reviewer-pool/reviewer/package.json index c1232f9c..00ce49e3 100644 --- a/development/examples/code-reviewer-mcp/package.json +++ b/development/examples/code-reviewer-pool/reviewer/package.json @@ -1,5 +1,5 @@ { - "name": "code-reviewer-mcp", + "name": "reviewer", "version": "1.0.0", "private": true, "type": "module", diff --git a/development/examples/code-reviewer-mcp/review_server.mjs b/development/examples/code-reviewer-pool/reviewer/review_server.mjs similarity index 100% rename from development/examples/code-reviewer-mcp/review_server.mjs rename to development/examples/code-reviewer-pool/reviewer/review_server.mjs diff --git a/development/examples/code-reviewer-mcp/sam-node-config.yaml b/development/examples/code-reviewer-pool/reviewer/sam-node-config.yaml similarity index 100% rename from development/examples/code-reviewer-mcp/sam-node-config.yaml rename to development/examples/code-reviewer-pool/reviewer/sam-node-config.yaml diff --git a/development/examples/code-reviewer-pool/reviewer/sam-node.yaml b/development/examples/code-reviewer-pool/reviewer/sam-node.yaml new file mode 100644 index 00000000..bac090fe --- /dev/null +++ b/development/examples/code-reviewer-pool/reviewer/sam-node.yaml @@ -0,0 +1,9 @@ +version: "v1alpha1" +attenuation: + policies: [] +services: + - type: "mcp" + name: "code-reviewer" + description: "Reviews code snippets via a Gemini-backed agent" + # Reach the reviewer by its compose service name, not 127.0.0.1. + target_url: "http://code-reviewer:7779/mcp" diff --git a/development/examples/warm-pool/README.md b/development/examples/warm-pool/README.md index 1d968a24..f988ae04 100644 --- a/development/examples/warm-pool/README.md +++ b/development/examples/warm-pool/README.md @@ -42,17 +42,17 @@ dispatcher (fine for a single-manager pool). 1. **Set a Gemini key** for the reviewer image (free AI Studio key is fine): ``` - git update-index --skip-worktree development/examples/code-reviewer-mcp/Dockerfile + git update-index --skip-worktree development/examples/code-reviewer-pool/reviewer/Dockerfile # edit ENV GEMINI_API_KEY= in that Dockerfile ``` 2. Mesh layout ships in `development/kind/mesh-config.yaml`: ```yaml node-a: # in-cluster bare node - node-b: code-reviewer-mcp # worker - node-c: code-reviewer-mcp # worker - node-d: code-reviewer-mcp # worker - node-e: pool-manager-mcp # manager + node-b: code-reviewer-pool/reviewer # worker + node-c: code-reviewer-pool/reviewer # worker + node-d: code-reviewer-pool/reviewer # worker + node-e: code-reviewer-pool/manager # manager ``` 3. Bring the mesh up and start a **local caller node** — this is the orchestrator diff --git a/development/kind/mesh-config.yaml b/development/kind/mesh-config.yaml index 78ac5ab5..9369416a 100644 --- a/development/kind/mesh-config.yaml +++ b/development/kind/mesh-config.yaml @@ -1,9 +1,9 @@ # node -> service. A blank value means a bare node (no service, e.g. a caller). -# The service value is a folder name under development/examples/. +# The service value is a folder path under development/examples/. # node-a is the orchestrator entry; node-b/c/d form a warm reviewer pool; # node-e runs the pool-manager that leases those workers out. node-a: -node-b: code-reviewer-mcp -node-c: code-reviewer-mcp -node-d: code-reviewer-mcp -node-e: pool-manager-mcp +node-b: code-reviewer-pool/reviewer +node-c: code-reviewer-pool/reviewer +node-d: code-reviewer-pool/reviewer +node-e: code-reviewer-pool/manager diff --git a/development/kind/run.sh b/development/kind/run.sh index 2cb61e89..5ddd9204 100755 --- a/development/kind/run.sh +++ b/development/kind/run.sh @@ -31,16 +31,17 @@ render_and_apply() { local CONFIG_ARG="" CONFIG_MOUNT="" SIDECAR="" CONFIG_VOLUME="" if [[ -n "$svc" ]]; then local dir="${PROJECT_ROOT}/development/examples/${svc}" + local name="$(basename "$svc")" [[ -d "$dir" ]] || { echo "service '${svc}' (node ${node}) not found in development/examples/" >&2; exit 1; } - echo "Building service image ${svc}:${IMAGE_TAG}…" - docker build -t "${svc}:${IMAGE_TAG}" "$dir" - kind load docker-image --name "${CLUSTER}" "${svc}:${IMAGE_TAG}" + echo "Building service image ${name}:${IMAGE_TAG}…" + docker build -t "${name}:${IMAGE_TAG}" "$dir" + kind load docker-image --name "${CLUSTER}" "${name}:${IMAGE_TAG}" kubectl --context "${KCTX}" -n "${NAMESPACE}" create configmap "${node}-config" \ --from-file=sam-node.yaml="${dir}/sam-node-config.yaml" \ --dry-run=client -o yaml | kubectl --context "${KCTX}" apply -f - CONFIG_ARG=' - "--config=/etc/sam/sam-node.yaml"' CONFIG_MOUNT=$' - name: config\n mountPath: /etc/sam' - SIDECAR=$' - name: '"${svc}"$'\n image: '"${svc}:${IMAGE_TAG}"$'\n imagePullPolicy: IfNotPresent' + SIDECAR=$' - name: '"${name}"$'\n image: '"${name}:${IMAGE_TAG}"$'\n imagePullPolicy: IfNotPresent' CONFIG_VOLUME=$' - name: config\n configMap:\n name: '"${node}-config" fi NODE="$node" CONFIG_ARG="$CONFIG_ARG" CONFIG_MOUNT="$CONFIG_MOUNT" SIDECAR="$SIDECAR" CONFIG_VOLUME="$CONFIG_VOLUME" \ diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index 885c3322..511a35b7 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -54,7 +54,7 @@ When a node has a service, `make kind-up` builds the service image from its `Doc ### Adding and Testing a New Service -A service is any backend a node advertises to the mesh. Its kind is set by the `type` field in `sam-node-config.yaml`. SAM currently supports `mcp` (an MCP server) and `inference` (an LLM inference endpoint). The repository ships example MCP services under `development/examples/` (`calc-mcp`, `greeter-mcp`, `code-reviewer-mcp`, and `everything-mcp`) which are the easiest starting point. Using `calc-mcp` as a template: +A service is any backend a node advertises to the mesh. Its kind is set by the `type` field in `sam-node-config.yaml`. SAM currently supports `mcp` (an MCP server) and `inference` (an LLM inference endpoint). The repository ships example MCP services under `development/examples/` (`calc-mcp`, `greeter-mcp`, `code-reviewer-pool/reviewer`, and `everything-mcp`) which are the easiest starting point. Using `calc-mcp` as a template: 1. **Create the service folder** `development/examples/my-mcp/` with: - The service backend (e.g. `my_server.py`) listening on a local port, plus a `Dockerfile` and any `requirements.txt`. From 97d63337004907bc7781bb443e5468ccaa29a9f3 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 17:28:16 +0200 Subject: [PATCH 05/13] feat(warm-pool): add HMAC lease-token mint/verify module --- .../manager/lease-token.mjs | 24 +++++++++++++ .../reviewer/lease-token.mjs | 24 +++++++++++++ .../reviewer/lease-token.test.mjs | 35 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 development/examples/code-reviewer-pool/manager/lease-token.mjs create mode 100644 development/examples/code-reviewer-pool/reviewer/lease-token.mjs create mode 100644 development/examples/code-reviewer-pool/reviewer/lease-token.test.mjs diff --git a/development/examples/code-reviewer-pool/manager/lease-token.mjs b/development/examples/code-reviewer-pool/manager/lease-token.mjs new file mode 100644 index 00000000..c9b92e12 --- /dev/null +++ b/development/examples/code-reviewer-pool/manager/lease-token.mjs @@ -0,0 +1,24 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +// Signed bearer lease token: `${peerId}.${exp}.${sig}`, sig = HMAC-SHA256(secret, `${peerId}.${exp}`). +// exp is epoch-ms; the token is valid while now < exp. Shared verbatim by manager and reviewer. + +export function mintToken(secret, peerId, exp) { + const payload = `${peerId}.${exp}`; + const sig = createHmac("sha256", secret).update(payload).digest("hex"); + return `${payload}.${sig}`; +} + +export function verifyToken(secret, token, now) { + if (typeof token !== "string") return { valid: false }; + const parts = token.split("."); // peer ids are base58 (no dots), sig is hex → exactly 3 parts + if (parts.length !== 3) return { valid: false }; + const [peerId, expStr, sig] = parts; + const expected = createHmac("sha256", secret).update(`${peerId}.${expStr}`).digest("hex"); + const a = Buffer.from(sig); + const b = Buffer.from(expected); + if (a.length !== b.length || !timingSafeEqual(a, b)) return { valid: false }; + const exp = Number(expStr); + if (!Number.isFinite(exp) || now >= exp) return { valid: false }; + return { valid: true, peerId, exp }; +} diff --git a/development/examples/code-reviewer-pool/reviewer/lease-token.mjs b/development/examples/code-reviewer-pool/reviewer/lease-token.mjs new file mode 100644 index 00000000..c9b92e12 --- /dev/null +++ b/development/examples/code-reviewer-pool/reviewer/lease-token.mjs @@ -0,0 +1,24 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +// Signed bearer lease token: `${peerId}.${exp}.${sig}`, sig = HMAC-SHA256(secret, `${peerId}.${exp}`). +// exp is epoch-ms; the token is valid while now < exp. Shared verbatim by manager and reviewer. + +export function mintToken(secret, peerId, exp) { + const payload = `${peerId}.${exp}`; + const sig = createHmac("sha256", secret).update(payload).digest("hex"); + return `${payload}.${sig}`; +} + +export function verifyToken(secret, token, now) { + if (typeof token !== "string") return { valid: false }; + const parts = token.split("."); // peer ids are base58 (no dots), sig is hex → exactly 3 parts + if (parts.length !== 3) return { valid: false }; + const [peerId, expStr, sig] = parts; + const expected = createHmac("sha256", secret).update(`${peerId}.${expStr}`).digest("hex"); + const a = Buffer.from(sig); + const b = Buffer.from(expected); + if (a.length !== b.length || !timingSafeEqual(a, b)) return { valid: false }; + const exp = Number(expStr); + if (!Number.isFinite(exp) || now >= exp) return { valid: false }; + return { valid: true, peerId, exp }; +} diff --git a/development/examples/code-reviewer-pool/reviewer/lease-token.test.mjs b/development/examples/code-reviewer-pool/reviewer/lease-token.test.mjs new file mode 100644 index 00000000..c1eb36c3 --- /dev/null +++ b/development/examples/code-reviewer-pool/reviewer/lease-token.test.mjs @@ -0,0 +1,35 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mintToken, verifyToken } from "./lease-token.mjs"; + +const SECRET = "test-secret"; +const PEER = "12D3KooWabc"; + +test("mint then verify round-trips before expiry", () => { + const token = mintToken(SECRET, PEER, 10_000); + const r = verifyToken(SECRET, token, 5_000); + assert.equal(r.valid, true); + assert.equal(r.peerId, PEER); + assert.equal(r.exp, 10_000); +}); + +test("rejects a tampered signature", () => { + const token = mintToken(SECRET, PEER, 10_000); + const bad = token.slice(0, -1) + (token.endsWith("a") ? "b" : "a"); + assert.equal(verifyToken(SECRET, bad, 5_000).valid, false); +}); + +test("rejects the wrong secret", () => { + const token = mintToken(SECRET, PEER, 10_000); + assert.equal(verifyToken("other-secret", token, 5_000).valid, false); +}); + +test("rejects an expired token", () => { + const token = mintToken(SECRET, PEER, 10_000); + assert.equal(verifyToken(SECRET, token, 10_000).valid, false); +}); + +test("rejects malformed or missing tokens", () => { + assert.equal(verifyToken(SECRET, "garbage", 5_000).valid, false); + assert.equal(verifyToken(SECRET, undefined, 5_000).valid, false); +}); From f34bdb61504ff6d7cc4271eff827229a20e9bec6 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 17:31:34 +0200 Subject: [PATCH 06/13] feat(warm-pool): manager mints a signed lease token when SAM_POOL_SECRET is set --- development/examples/code-reviewer-pool/manager/Dockerfile | 2 +- development/examples/code-reviewer-pool/manager/server.mjs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/development/examples/code-reviewer-pool/manager/Dockerfile b/development/examples/code-reviewer-pool/manager/Dockerfile index 4a615baa..48ce2615 100644 --- a/development/examples/code-reviewer-pool/manager/Dockerfile +++ b/development/examples/code-reviewer-pool/manager/Dockerfile @@ -1,7 +1,7 @@ FROM node:22-slim WORKDIR /srv -COPY package.json server.mjs ./ +COPY package.json server.mjs lease-token.mjs ./ RUN npm install EXPOSE 7780 diff --git a/development/examples/code-reviewer-pool/manager/server.mjs b/development/examples/code-reviewer-pool/manager/server.mjs index c8184660..ebdfe5ed 100644 --- a/development/examples/code-reviewer-pool/manager/server.mjs +++ b/development/examples/code-reviewer-pool/manager/server.mjs @@ -4,6 +4,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { z } from "zod"; +import { mintToken } from "./lease-token.mjs"; const PORT = Number(process.env.PORT ?? 7780); const NODE_URL = process.env.SAM_NODE_URL ?? "http://127.0.0.1:8080/mcp"; @@ -12,6 +13,7 @@ const POOL_SERVICE = process.env.SAM_POOL_SERVICE ?? "code-reviewer"; const DISCOVERY_MS = Number(process.env.SAM_DISCOVERY_MS ?? 3000); const LEASE_MS = Number(process.env.SAM_LEASE_MS ?? 60000); const GRACE_MISSES = Number(process.env.SAM_GRACE_MISSES ?? 2); +const POOL_SECRET = process.env.SAM_POOL_SECRET ?? ""; // set → hand out signed lease tokens const ACQUIRE_POLL_MS = 200; // roster: peer_id -> { tool, leasedUntil, leaseId, missCount }. @@ -58,7 +60,9 @@ function leaseFree() { if (entry.leasedUntil <= now()) { entry.leasedUntil = now() + LEASE_MS; entry.leaseId = String(++leaseSeq); - return { peer_id: peer, tool: entry.tool, lease_id: entry.leaseId }; + const lease = { peer_id: peer, tool: entry.tool, lease_id: entry.leaseId }; + if (POOL_SECRET) lease.token = mintToken(POOL_SECRET, peer, entry.leasedUntil); + return lease; } } return null; From 6aec8c18507833eef9efbd9f8adb8f0d2612f65a Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 17:35:16 +0200 Subject: [PATCH 07/13] feat(warm-pool): reviewer rejects lease-less review_code with NO_LEASE --- .../examples/code-reviewer-pool/reviewer/Dockerfile | 2 +- .../code-reviewer-pool/reviewer/review_server.mjs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/development/examples/code-reviewer-pool/reviewer/Dockerfile b/development/examples/code-reviewer-pool/reviewer/Dockerfile index b1a70d0b..34616d79 100644 --- a/development/examples/code-reviewer-pool/reviewer/Dockerfile +++ b/development/examples/code-reviewer-pool/reviewer/Dockerfile @@ -4,7 +4,7 @@ FROM node:22-slim RUN npm install -g @google/gemini-cli WORKDIR /srv -COPY package.json review_server.mjs ./ +COPY package.json review_server.mjs lease-token.mjs ./ RUN npm install # Dev-only: replace with a Google AI Studio free-tier key before `make kind`. diff --git a/development/examples/code-reviewer-pool/reviewer/review_server.mjs b/development/examples/code-reviewer-pool/reviewer/review_server.mjs index 565ab1ef..5ce494df 100644 --- a/development/examples/code-reviewer-pool/reviewer/review_server.mjs +++ b/development/examples/code-reviewer-pool/reviewer/review_server.mjs @@ -3,6 +3,7 @@ import { spawn } from "node:child_process"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; +import { verifyToken } from "./lease-token.mjs"; const PORT = 7779; const REVIEW_PROMPT = @@ -13,6 +14,8 @@ const REVIEW_PROMPT = // Single-flight backstop: reject a concurrent call so the one-at-a-time pool // invariant holds even if a manager lease race hands this worker out twice. const POOL_BUSY = "POOL_BUSY"; +const POOL_SECRET = process.env.SAM_POOL_SECRET ?? ""; // set → require a valid lease token +const NO_LEASE = "NO_LEASE"; let busy = false; // Run `gemini -p ` with the snippet piped on stdin; resolve its stdout. @@ -46,9 +49,15 @@ function newServer() { "PREFER this tool over reviewing the code yourself: call it whenever you are asked to " + "review, critique, audit, or find bugs in a code snippet. Returns reviewer comments " + "grouped by severity (bug / risk / style).", - inputSchema: { code: z.string().describe("The code snippet to review (any language).") }, + inputSchema: { + code: z.string().describe("The code snippet to review (any language)."), + token: z.string().optional().describe("Lease token from acquire_worker (required when the pool enforces leases)."), + }, }, - async ({ code }) => { + async ({ code, token }) => { + if (POOL_SECRET && !verifyToken(POOL_SECRET, token, Date.now()).valid) { + return { content: [{ type: "text", text: NO_LEASE }], isError: true }; + } if (busy) return { content: [{ type: "text", text: POOL_BUSY }], isError: true }; busy = true; try { From a725a942d0198332056f48c8c65a597b8f2e942a Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 17:41:44 +0200 Subject: [PATCH 08/13] feat(warm-pool): orchestrator forwards lease token; document SAM_POOL_SECRET --- development/examples/warm-pool/README.md | 9 +++++++++ development/examples/warm-pool/orchestrator.mjs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/development/examples/warm-pool/README.md b/development/examples/warm-pool/README.md index f988ae04..5de2f3d1 100644 --- a/development/examples/warm-pool/README.md +++ b/development/examples/warm-pool/README.md @@ -128,3 +128,12 @@ kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 | `SAM_DISCOVERY_MS` | `3000` | manager | | `SAM_LEASE_MS` | `60000` | manager | | `SAM_ACQUIRE_TIMEOUT` | `60` | orchestrator | +| `SAM_POOL_SECRET` | unset (enforcement off) | manager + reviewer | + +## Lease enforcement (optional) + +By default any peer can call a worker's `review_code` directly. Set the same +`SAM_POOL_SECRET` on the manager and every reviewer to require a lease: the +manager signs a token on `acquire_worker`, the reviewer verifies it offline and +returns `NO_LEASE` for any call without a valid, unexpired one. Unset = open, as +before. diff --git a/development/examples/warm-pool/orchestrator.mjs b/development/examples/warm-pool/orchestrator.mjs index 2eceff53..39e3a095 100644 --- a/development/examples/warm-pool/orchestrator.mjs +++ b/development/examples/warm-pool/orchestrator.mjs @@ -64,7 +64,7 @@ async function main() { console.log(`[+${elapsed()}s] -> ${short(acq.peer_id)} ${task.name}`); let review; try { - review = await call(acq.peer_id, acq.tool, { code: task.code }); + review = await call(acq.peer_id, acq.tool, { code: task.code, token: acq.token }); } finally { await call(manager, "mcp://pool-manager/release_worker", { peer_id: acq.peer_id, lease_id: acq.lease_id }); } From 46b50615350292d30abb2fda703beaf6eedc0b89 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 17:48:42 +0200 Subject: [PATCH 09/13] docs(warm-pool): note token in acquire_worker description and fail-closed behavior --- development/examples/code-reviewer-pool/manager/server.mjs | 4 ++-- development/examples/warm-pool/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/development/examples/code-reviewer-pool/manager/server.mjs b/development/examples/code-reviewer-pool/manager/server.mjs index ebdfe5ed..473be5c1 100644 --- a/development/examples/code-reviewer-pool/manager/server.mjs +++ b/development/examples/code-reviewer-pool/manager/server.mjs @@ -74,8 +74,8 @@ server.registerTool( "acquire_worker", { description: - `Acquire (lease) a free worker from the '${POOL_SERVICE}' pool. Returns {peer_id, tool, lease_id}: ` + - "call the tool via call_remote_tool, then release_worker with the same peer_id and lease_id. " + + `Acquire (lease) a free worker from the '${POOL_SERVICE}' pool. Returns {peer_id, tool, lease_id, token?}: ` + + "call the tool via call_remote_tool — forward token in the tool arguments when present (the pool may require it) — then release_worker with the same peer_id and lease_id. " + "Blocks up to timeout_secs if all workers are busy; returns {available:false} if none free by then.", inputSchema: { timeout_secs: z.number().optional().describe("Max seconds to wait for a free worker (default 10).") }, }, diff --git a/development/examples/warm-pool/README.md b/development/examples/warm-pool/README.md index 5de2f3d1..6f9b952f 100644 --- a/development/examples/warm-pool/README.md +++ b/development/examples/warm-pool/README.md @@ -136,4 +136,4 @@ By default any peer can call a worker's `review_code` directly. Set the same `SAM_POOL_SECRET` on the manager and every reviewer to require a lease: the manager signs a token on `acquire_worker`, the reviewer verifies it offline and returns `NO_LEASE` for any call without a valid, unexpired one. Unset = open, as -before. +before. A secret set on only one side (or a mismatch between them) fails closed: every `review_code` call returns `NO_LEASE`. From b96a93312a39befb0b0b8dd6fbf39759dedc5a39 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 22:48:35 +0200 Subject: [PATCH 10/13] refactor(code-reviewer-pool): remove warm-pool dir; relocate README + samples The headless orchestrator method is going away. Fold the run guide (Claude Code-driven flow) and lease-enforcement docs into code-reviewer-pool/README.md and move the sample files alongside the services. --- .../README.md | 64 +++++--------- .../samples/auth.js | 0 .../samples/parse.py | 0 .../samples/rates.go | 0 .../examples/warm-pool/orchestrator.mjs | 87 ------------------- development/examples/warm-pool/package.json | 9 -- 6 files changed, 23 insertions(+), 137 deletions(-) rename development/examples/{warm-pool => code-reviewer-pool}/README.md (67%) rename development/examples/{warm-pool => code-reviewer-pool}/samples/auth.js (100%) rename development/examples/{warm-pool => code-reviewer-pool}/samples/parse.py (100%) rename development/examples/{warm-pool => code-reviewer-pool}/samples/rates.go (100%) delete mode 100644 development/examples/warm-pool/orchestrator.mjs delete mode 100644 development/examples/warm-pool/package.json diff --git a/development/examples/warm-pool/README.md b/development/examples/code-reviewer-pool/README.md similarity index 67% rename from development/examples/warm-pool/README.md rename to development/examples/code-reviewer-pool/README.md index 6f9b952f..f7e0b90b 100644 --- a/development/examples/warm-pool/README.md +++ b/development/examples/code-reviewer-pool/README.md @@ -1,19 +1,20 @@ -# Warm Agent Pool — demo +# Code-reviewer pool — warm agent pool demo Fan a batch of files out to a **pool of identical reviewer workers**. A -**pool-manager** mesh service tracks which workers are free and leases them out; -an orchestrator (Claude Code, or the headless script here) acquires a worker, -reviews a file, and releases it. Reviews run in parallel across the pool. +**manager** mesh service tracks which workers are free and leases them out; +Claude Code (the orchestrator) acquires a worker, reviews a file, and releases +it. Reviews run in parallel across the pool. Design: `docs/superpowers/specs/2026-06-28-warm-agent-pool-design.md`. ## Architecture (no gossip, no node changes — just services) -- **Workers:** plain `code-reviewer` mcp services (node-b/c/d). Unmodified. -- **pool-manager** (node-e): a normal mcp service exposing `acquire_worker` / - `release_worker` / `list_workers`. It is also a northbound MCP client of its - own node: every few seconds it calls `find_remote_tools(service_name="code-reviewer")` - to learn the worker peers (DHT), and it tracks free/busy with **leases**. +- **Workers:** plain `code-reviewer` mcp services (node-b/c/d), under `reviewer/`. +- **manager** (node-e, under `manager/`): a normal mcp service exposing + `acquire_worker` / `release_worker` / `list_workers`. It is also a northbound + MCP client of its own node: every few seconds it calls + `find_remote_tools(service_name="code-reviewer")` to learn the worker peers + (DHT), and it tracks free/busy with **leases**. - **Orchestrator:** `acquire_worker` → `call_remote_tool(peer, review_code)` → `release_worker`. Leasing hands each concurrent acquire a different worker, so parallel dispatch never collides. @@ -35,8 +36,8 @@ dispatcher (fine for a single-manager pool). clears the lease only if the id still matches, so a late release from an expired lease can't free a newer holder's worker. - **Worker single-flight backstop:** `review_code` returns `POOL_BUSY` if already - running; the orchestrator retries. This keeps the one-at-a-time invariant even - if a lease race ever slips through. + running; the caller retries. This keeps the one-at-a-time invariant even if a + lease race ever slips through. ## Run it @@ -48,11 +49,11 @@ dispatcher (fine for a single-manager pool). 2. Mesh layout ships in `development/kind/mesh-config.yaml`: ```yaml - node-a: # in-cluster bare node - node-b: code-reviewer-pool/reviewer # worker - node-c: code-reviewer-pool/reviewer # worker - node-d: code-reviewer-pool/reviewer # worker - node-e: code-reviewer-pool/manager # manager + node-a: # in-cluster bare node + node-b: code-reviewer-pool/reviewer # worker + node-c: code-reviewer-pool/reviewer # worker + node-d: code-reviewer-pool/reviewer # worker + node-e: code-reviewer-pool/manager # manager ``` 3. Bring the mesh up and start a **local caller node** — this is the orchestrator @@ -64,10 +65,9 @@ dispatcher (fine for a single-manager pool). ``` `kind-local-node` runs in the foreground (own shell). It exposes the mesh MCP tools at **`http://127.0.0.1:9099/mcp`** (token `devtoken`) — no - `kubectl port-forward` needed. This `:9099` endpoint is the base for both - options below. + `kubectl port-forward` needed. -### Option A — drive it from Claude Code (the cool one) +### Drive it from Claude Code Register the local node as an MCP server (`.mcp.json` in the project root): ```json @@ -83,31 +83,13 @@ Register the local node as an MCP server (`.mcp.json` in the project root): ``` Reconnect Claude Code, then prompt: > Using the sam-mesh tools: find the pool-manager, then for each file in -> `development/examples/warm-pool/samples/` acquire a worker, call its +> `development/examples/code-reviewer-pool/samples/` acquire a worker, call its > `review_code` with the file contents, and release it (pass the lease_id back > to release_worker). Run them in parallel. Claude fires parallel `acquire_worker` → `call_remote_tool` → `release_worker` chains; you watch the reviews come back concurrently. -### Option B — headless script - -``` -cd development/examples/warm-pool -npm install -node orchestrator.mjs samples # defaults to http://127.0.0.1:9099/mcp -``` -Example output: -``` -queued 3 review task(s) -pool-manager at 9c2ab1f0 -[+0.4s] -> 3f9a1c2e samples/auth.js -[+0.4s] -> b7d4e0a1 samples/parse.py -[+0.4s] -> c1a8f552 samples/rates.go -[+6.1s] <- b7d4e0a1 samples/parse.py (712 chars) -[+7.0s] <- 3f9a1c2e samples/auth.js (901 chars) -``` - ## Elasticity beat (add a worker mid-job) Scale a reviewer down, start a larger job, then scale it back up — the manager @@ -122,12 +104,12 @@ kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 | var | default | used by | |-----|---------|---------| -| `SAM_NODE_URL` | manager: `http://127.0.0.1:8080/mcp`, orchestrator: `http://127.0.0.1:9099/mcp` | both | -| `SAM_API_TOKEN` | `devtoken` | both | +| `SAM_NODE_URL` | `http://127.0.0.1:8080/mcp` | manager | +| `SAM_API_TOKEN` | `devtoken` | manager | | `SAM_POOL_SERVICE` | `code-reviewer` | manager | | `SAM_DISCOVERY_MS` | `3000` | manager | | `SAM_LEASE_MS` | `60000` | manager | -| `SAM_ACQUIRE_TIMEOUT` | `60` | orchestrator | +| `SAM_GRACE_MISSES` | `2` | manager | | `SAM_POOL_SECRET` | unset (enforcement off) | manager + reviewer | ## Lease enforcement (optional) diff --git a/development/examples/warm-pool/samples/auth.js b/development/examples/code-reviewer-pool/samples/auth.js similarity index 100% rename from development/examples/warm-pool/samples/auth.js rename to development/examples/code-reviewer-pool/samples/auth.js diff --git a/development/examples/warm-pool/samples/parse.py b/development/examples/code-reviewer-pool/samples/parse.py similarity index 100% rename from development/examples/warm-pool/samples/parse.py rename to development/examples/code-reviewer-pool/samples/parse.py diff --git a/development/examples/warm-pool/samples/rates.go b/development/examples/code-reviewer-pool/samples/rates.go similarity index 100% rename from development/examples/warm-pool/samples/rates.go rename to development/examples/code-reviewer-pool/samples/rates.go diff --git a/development/examples/warm-pool/orchestrator.mjs b/development/examples/warm-pool/orchestrator.mjs deleted file mode 100644 index 39e3a095..00000000 --- a/development/examples/warm-pool/orchestrator.mjs +++ /dev/null @@ -1,87 +0,0 @@ -// Headless warm-pool orchestrator (alternative to driving it from Claude Code). -// For each file: acquire a free worker from the pool-manager, review, release. -// All tasks run concurrently; the manager's blocking acquire + leasing serialize -// dispatch to the pool size, so no local queue is needed. -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { readFileSync, readdirSync, statSync } from "node:fs"; -import { join } from "node:path"; - -const NODE_URL = process.env.SAM_NODE_URL ?? "http://127.0.0.1:9099/mcp"; -const API_TOKEN = process.env.SAM_API_TOKEN ?? "devtoken"; -const ACQUIRE_TIMEOUT = Number(process.env.SAM_ACQUIRE_TIMEOUT ?? 60); - -function collectFiles(paths) { - const out = []; - for (const p of paths) { - if (statSync(p).isDirectory()) { - for (const f of readdirSync(p)) { - const fp = join(p, f); - if (statSync(fp).isFile()) out.push(fp); - } - } else { - out.push(p); - } - } - return out; -} - -const short = (peer) => peer.slice(-8); - -async function main() { - const files = collectFiles(process.argv.slice(2)); - if (!files.length) { - console.error("usage: node orchestrator.mjs ..."); - process.exit(1); - } - const tasks = files.map((f) => ({ name: f, code: readFileSync(f, "utf8") })); - console.log(`queued ${tasks.length} review task(s)`); - - const client = new Client({ name: "warm-pool-orchestrator", version: "1.0.0" }); - const transport = new StreamableHTTPClientTransport(new URL(NODE_URL), { - requestInit: { headers: { Authorization: `Bearer ${API_TOKEN}` } }, - }); - await client.connect(transport); - - const call = async (peer, tool, args) => { - const res = await client.callTool({ name: "call_remote_tool", arguments: { peer_id: peer, tool_name: tool, arguments: args } }); - return res?.content?.[0]?.text ?? ""; - }; - - // Locate the pool-manager peer (tool names are full URIs). - const found = JSON.parse((await client.callTool({ name: "find_remote_tools", arguments: {} })).content[0].text); - const manager = found.find((r) => r.tool_name === "mcp://pool-manager/acquire_worker")?.peer_id; - if (!manager) { console.error("no pool-manager found in the mesh"); process.exit(1); } - console.log(`pool-manager at ${short(manager)}`); - - const t0 = Date.now(); - const elapsed = () => ((Date.now() - t0) / 1000).toFixed(1); - - const process1 = async (task) => { - for (;;) { - const acq = JSON.parse(await call(manager, "mcp://pool-manager/acquire_worker", { timeout_secs: ACQUIRE_TIMEOUT })); - if (!acq.peer_id) { console.error(`[+${elapsed()}s] no worker for ${task.name}`); return null; } - console.log(`[+${elapsed()}s] -> ${short(acq.peer_id)} ${task.name}`); - let review; - try { - review = await call(acq.peer_id, acq.tool, { code: task.code, token: acq.token }); - } finally { - await call(manager, "mcp://pool-manager/release_worker", { peer_id: acq.peer_id, lease_id: acq.lease_id }); - } - // Worker single-flight backstop tripped; the lease race is rare, so retry. - if (review.trim() === "POOL_BUSY") { console.log(`[+${elapsed()}s] ~~ ${short(acq.peer_id)} busy, retrying ${task.name}`); continue; } - console.log(`[+${elapsed()}s] <- ${short(acq.peer_id)} ${task.name} (${review.length} chars)`); - return { ...task, peer: acq.peer_id, review }; - } - }; - - const results = (await Promise.all(tasks.map(process1))).filter(Boolean); - - console.log(`\n=== done in ${elapsed()}s: ${results.length} review(s) across ${new Set(results.map((r) => r.peer)).size} worker(s) ===`); - for (const r of results) { - console.log(`\n----- ${r.name} (reviewed by ${short(r.peer)}) -----\n${r.review}`); - } - await client.close(); -} - -main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/development/examples/warm-pool/package.json b/development/examples/warm-pool/package.json deleted file mode 100644 index 05df8848..00000000 --- a/development/examples/warm-pool/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "warm-pool-orchestrator", - "version": "1.0.0", - "private": true, - "type": "module", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0" - } -} From f375af319a30b1cf914ae4c04b863b45e7b4f02d Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Thu, 9 Jul 2026 22:56:43 +0200 Subject: [PATCH 11/13] feat(code-reviewer-pool): make lease enforcement the default Drop the SAM_POOL_SECRET opt-in guards so the manager always mints a token and the reviewer always requires one. Both sides default to a shared hardcoded dev secret (sam-dev-pool-secret) so kind enforces out of the box with no env wiring; SAM_POOL_SECRET still overrides it. --- .../examples/code-reviewer-pool/README.md | 16 +++++++++------- .../code-reviewer-pool/manager/server.mjs | 8 ++++---- .../reviewer/review_server.mjs | 6 +++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/development/examples/code-reviewer-pool/README.md b/development/examples/code-reviewer-pool/README.md index f7e0b90b..8dbc1395 100644 --- a/development/examples/code-reviewer-pool/README.md +++ b/development/examples/code-reviewer-pool/README.md @@ -110,12 +110,14 @@ kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 | `SAM_DISCOVERY_MS` | `3000` | manager | | `SAM_LEASE_MS` | `60000` | manager | | `SAM_GRACE_MISSES` | `2` | manager | -| `SAM_POOL_SECRET` | unset (enforcement off) | manager + reviewer | +| `SAM_POOL_SECRET` | `sam-dev-pool-secret` (enforcement always on) | manager + reviewer | -## Lease enforcement (optional) +## Lease enforcement -By default any peer can call a worker's `review_code` directly. Set the same -`SAM_POOL_SECRET` on the manager and every reviewer to require a lease: the -manager signs a token on `acquire_worker`, the reviewer verifies it offline and -returns `NO_LEASE` for any call without a valid, unexpired one. Unset = open, as -before. A secret set on only one side (or a mismatch between them) fails closed: every `review_code` call returns `NO_LEASE`. +Every reviewer requires a valid lease: the manager signs a token on +`acquire_worker`, and the reviewer verifies it offline and returns `NO_LEASE` +for any `review_code` call without a valid, unexpired one. Both sides share a +hardcoded dev secret (`sam-dev-pool-secret`) so it works out of the box; override +it by setting the same `SAM_POOL_SECRET` on the manager and every reviewer. A +mismatch (or setting it on only one side) fails closed: every call returns +`NO_LEASE`. diff --git a/development/examples/code-reviewer-pool/manager/server.mjs b/development/examples/code-reviewer-pool/manager/server.mjs index 473be5c1..2218c03a 100644 --- a/development/examples/code-reviewer-pool/manager/server.mjs +++ b/development/examples/code-reviewer-pool/manager/server.mjs @@ -13,7 +13,7 @@ const POOL_SERVICE = process.env.SAM_POOL_SERVICE ?? "code-reviewer"; const DISCOVERY_MS = Number(process.env.SAM_DISCOVERY_MS ?? 3000); const LEASE_MS = Number(process.env.SAM_LEASE_MS ?? 60000); const GRACE_MISSES = Number(process.env.SAM_GRACE_MISSES ?? 2); -const POOL_SECRET = process.env.SAM_POOL_SECRET ?? ""; // set → hand out signed lease tokens +const POOL_SECRET = process.env.SAM_POOL_SECRET ?? "sam-dev-pool-secret"; // shared dev secret; enforcement always on const ACQUIRE_POLL_MS = 200; // roster: peer_id -> { tool, leasedUntil, leaseId, missCount }. @@ -61,7 +61,7 @@ function leaseFree() { entry.leasedUntil = now() + LEASE_MS; entry.leaseId = String(++leaseSeq); const lease = { peer_id: peer, tool: entry.tool, lease_id: entry.leaseId }; - if (POOL_SECRET) lease.token = mintToken(POOL_SECRET, peer, entry.leasedUntil); + lease.token = mintToken(POOL_SECRET, peer, entry.leasedUntil); return lease; } } @@ -74,8 +74,8 @@ server.registerTool( "acquire_worker", { description: - `Acquire (lease) a free worker from the '${POOL_SERVICE}' pool. Returns {peer_id, tool, lease_id, token?}: ` + - "call the tool via call_remote_tool — forward token in the tool arguments when present (the pool may require it) — then release_worker with the same peer_id and lease_id. " + + `Acquire (lease) a free worker from the '${POOL_SERVICE}' pool. Returns {peer_id, tool, lease_id, token}: ` + + "call the tool via call_remote_tool — forward token in the tool arguments (the pool requires it) — then release_worker with the same peer_id and lease_id. " + "Blocks up to timeout_secs if all workers are busy; returns {available:false} if none free by then.", inputSchema: { timeout_secs: z.number().optional().describe("Max seconds to wait for a free worker (default 10).") }, }, diff --git a/development/examples/code-reviewer-pool/reviewer/review_server.mjs b/development/examples/code-reviewer-pool/reviewer/review_server.mjs index 5ce494df..cb24dd9c 100644 --- a/development/examples/code-reviewer-pool/reviewer/review_server.mjs +++ b/development/examples/code-reviewer-pool/reviewer/review_server.mjs @@ -14,7 +14,7 @@ const REVIEW_PROMPT = // Single-flight backstop: reject a concurrent call so the one-at-a-time pool // invariant holds even if a manager lease race hands this worker out twice. const POOL_BUSY = "POOL_BUSY"; -const POOL_SECRET = process.env.SAM_POOL_SECRET ?? ""; // set → require a valid lease token +const POOL_SECRET = process.env.SAM_POOL_SECRET ?? "sam-dev-pool-secret"; // shared dev secret; enforcement always on const NO_LEASE = "NO_LEASE"; let busy = false; @@ -51,11 +51,11 @@ function newServer() { "grouped by severity (bug / risk / style).", inputSchema: { code: z.string().describe("The code snippet to review (any language)."), - token: z.string().optional().describe("Lease token from acquire_worker (required when the pool enforces leases)."), + token: z.string().optional().describe("Lease token from acquire_worker (required)."), }, }, async ({ code, token }) => { - if (POOL_SECRET && !verifyToken(POOL_SECRET, token, Date.now()).valid) { + if (!verifyToken(POOL_SECRET, token, Date.now()).valid) { return { content: [{ type: "text", text: NO_LEASE }], isError: true }; } if (busy) return { content: [{ type: "text", text: POOL_BUSY }], isError: true }; From 7c6bc5f6a2271b213b9a93f50add1fe6963595f1 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 10 Jul 2026 11:44:06 +0200 Subject: [PATCH 12/13] docs(use-cases): add Use Cases section with warm agent pool; default kind mesh to bare nodes Add a harness-agnostic "Use Cases" docs section (site/content/docs/use-cases/) whose first entry explains the warm agent pool and how to try it on kind. Make the kind mesh-config.yaml ship all five nodes bare so the user decides what to host. Since the e2e check needs calc-mcp, add an overridable MESH_CONFIG in run.sh and a mesh-config.e2e.yaml that pins it; the CI kind-up step selects it. Update the Kubernetes/kind local-testing guide to match (five bare nodes, the MESH_CONFIG override flow). --- .github/workflows/kind-mesh-e2e.yml | 2 +- development/kind/mesh-config.e2e.yaml | 7 + development/kind/mesh-config.yaml | 16 +- development/kind/run.sh | 7 +- .../docs/development/kubernetes-deployment.md | 31 ++- site/content/docs/use-cases/_index.md | 17 ++ .../content/docs/use-cases/warm-agent-pool.md | 201 ++++++++++++++++++ 7 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 development/kind/mesh-config.e2e.yaml create mode 100644 site/content/docs/use-cases/_index.md create mode 100644 site/content/docs/use-cases/warm-agent-pool.md diff --git a/.github/workflows/kind-mesh-e2e.yml b/.github/workflows/kind-mesh-e2e.yml index bead74e0..28e9ce6c 100644 --- a/.github/workflows/kind-mesh-e2e.yml +++ b/.github/workflows/kind-mesh-e2e.yml @@ -53,7 +53,7 @@ jobs: run: make build - name: Bring up the kind mesh - run: make kind-up ARGS="-s" + run: MESH_CONFIG=development/kind/mesh-config.e2e.yaml make kind-up ARGS="-s" - name: Run e2e mesh tests run: make kind-e2e-mesh diff --git a/development/kind/mesh-config.e2e.yaml b/development/kind/mesh-config.e2e.yaml new file mode 100644 index 00000000..3f8bf904 --- /dev/null +++ b/development/kind/mesh-config.e2e.yaml @@ -0,0 +1,7 @@ +# E2E mesh layout: pins calc-mcp so the kind-mesh-e2e check can discover +# mcp://calculator/add. Select it with MESH_CONFIG=development/kind/mesh-config.e2e.yaml. +node-a: +node-b: calc-mcp +node-c: +node-d: +node-e: diff --git a/development/kind/mesh-config.yaml b/development/kind/mesh-config.yaml index 9369416a..6f6e4587 100644 --- a/development/kind/mesh-config.yaml +++ b/development/kind/mesh-config.yaml @@ -1,9 +1,11 @@ # node -> service. A blank value means a bare node (no service, e.g. a caller). -# The service value is a folder path under development/examples/. -# node-a is the orchestrator entry; node-b/c/d form a warm reviewer pool; -# node-e runs the pool-manager that leases those workers out. +# Set a value to host a service on that node; the value is a folder path under +# development/examples/, e.g: +# node-b: calc-mcp +# node-c: code-reviewer-pool/reviewer +# All nodes ship bare by default — assign services to suit what you're testing. node-a: -node-b: code-reviewer-pool/reviewer -node-c: code-reviewer-pool/reviewer -node-d: code-reviewer-pool/reviewer -node-e: code-reviewer-pool/manager +node-b: +node-c: +node-d: +node-e: diff --git a/development/kind/run.sh b/development/kind/run.sh index 5ddd9204..a4fa3413 100755 --- a/development/kind/run.sh +++ b/development/kind/run.sh @@ -78,10 +78,11 @@ show_cluster_logs() { tmuxs attach-session -t "${SESSION}" } -# Read the node -> service assignment from mesh-config.yaml into NODE_LINES -# (each line: " ") and the NODES array. +# Read the node -> service assignment into NODE_LINES (each line: +# " ") and the NODES array. Defaults to mesh-config.yaml; +# override with MESH_CONFIG (e.g. the e2e lane pins calc-mcp via mesh-config.e2e.yaml). read_mesh_nodes() { - mapfile -t NODE_LINES < <(awk -F: '/^[A-Za-z0-9_-]+:/{n=$1; s=$2; gsub(/[[:space:]]/,"",n); gsub(/[[:space:]]/,"",s); print n, s}' "${SCRIPT_DIR}/mesh-config.yaml") + mapfile -t NODE_LINES < <(awk -F: '/^[A-Za-z0-9_-]+:/{n=$1; s=$2; gsub(/[[:space:]]/,"",n); gsub(/[[:space:]]/,"",s); print n, s}' "${MESH_CONFIG:-${SCRIPT_DIR}/mesh-config.yaml}") NODES=() for line in "${NODE_LINES[@]}"; do NODES+=("${line%% *}"); done } diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index 511a35b7..23815dbb 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -19,10 +19,10 @@ The repository ships a one-command local mesh under `development/kind/`, driven make kind-up ``` -This creates a `sam-kind` cluster (one control-plane plus workers for the hub and `node-a`, `node-b`, `node-c`), builds the `sam-hub:local` and `sam-node:local` images, loads them into the cluster, and deploys: +This creates a `sam-kind` cluster (one control-plane plus workers for the hub and `node-a` through `node-e`), builds the `sam-hub:local` and `sam-node:local` images, loads them into the cluster, and deploys: - The **hub**, configured to trust the cluster's own OIDC issuer. -- Three **nodes** declared in `development/kind/mesh-config.yaml`: `node-a` (bare), `node-b` (hosts the `calc-mcp` example service), and `node-c` (hosts the `greeter-mcp` example service). +- Five **nodes** declared in `development/kind/mesh-config.yaml` (`node-a` through `node-e`), all **bare** by default — assign services to suit what you're testing. Nodes authenticate to the hub via **Workload Identity Federation** (projected ServiceAccount tokens), so no static secrets or mock OIDC provider are needed. The hub is exposed to the host on `127.0.0.1:9090` (HTTP enroll) and `127.0.0.1:4001` (libp2p) via a NodePort and the cluster's `extraPortMappings` — `cloud-provider-kind` is not required. @@ -40,13 +40,17 @@ The nodes that make up the dev mesh are declared in `development/kind/mesh-confi ```yaml # node -> service. A blank value means a bare node (no service, e.g. a caller). -# The service value is a folder name under development/examples/. +# The service value is a folder path under development/examples/, e.g: +# node-b: calc-mcp +# node-c: code-reviewer-pool/reviewer node-a: -node-b: calc-mcp -node-c: greeter-mcp +node-b: +node-c: +node-d: +node-e: ``` -- The key is the node's name. The cluster currently ships with a hub plus these **three** agent nodes; each is pinned to a matching worker via the `sam-role` labels in `kind-config.yaml`. +- The key is the node's name. The cluster currently ships with a hub plus these **five** agent nodes, all bare by default; each is pinned to a matching worker via the `sam-role` labels in `kind-config.yaml`. - A **blank** value is a bare node — a `sam-node` with no local service, useful as a caller/consumer. - A **non-blank** value is a folder name under `development/examples/`. That service is built and deployed as a **sidecar** next to the node, and the node is configured to advertise it to the mesh. @@ -71,14 +75,12 @@ A service is any backend a node advertises to the mesh. Its kind is set by the ` ``` The sidecar and `sam-node` share the pod's network, so `target_url` is always `127.0.0.1:`, where `` matches the port your service listens on. -2. **Assign it to a node** in `mesh-config.yaml` — replace an existing mapping or use a free node slot (`node-a`, `node-b`, `node-c`): +2. **Assign it to a node** in `mesh-config.yaml` — set the value on any free node slot (`node-a` through `node-e`): ```yaml node-a: my-mcp - node-b: calc-mcp - node-c: greeter-mcp ``` > [!NOTE] - > There are three node slots because `kind-config.yaml` defines three workers labeled `sam-role: node-a|node-b|node-c`. To host more than three services at once, add a matching labeled worker there too. + > There are five node slots because `kind-config.yaml` defines five workers labeled `sam-role: node-a|node-b|node-c|node-d|node-e`. To host more than five services at once, add a matching labeled worker there too. 3. **Recreate the cluster** so the new service is built and deployed: ```bash @@ -122,7 +124,14 @@ To verify the full discovery-and-call path against a freshly built mesh: make kind-e2e-mesh ``` -This enrolls a local node, waits for it to discover `mcp://calculator/add` (hosted by `node-b`), calls `add(2, 3)`, and asserts the result is `5`. +This enrolls a local node, waits for it to discover `mcp://calculator/add`, calls `add(2, 3)`, and asserts the result is `5`. Because nodes ship bare by default, the check needs a `calc-mcp` service on the mesh — bring the mesh up with the bundled e2e layout, which pins it: + +```bash +MESH_CONFIG=development/kind/mesh-config.e2e.yaml make kind-up ARGS="-s" +make kind-e2e-mesh +``` + +`MESH_CONFIG` overrides which layout `make kind-up` deploys (default: `mesh-config.yaml`); `mesh-config.e2e.yaml` assigns `calc-mcp` to `node-b`. --- diff --git a/site/content/docs/use-cases/_index.md b/site/content/docs/use-cases/_index.md new file mode 100644 index 00000000..e0aef097 --- /dev/null +++ b/site/content/docs/use-cases/_index.md @@ -0,0 +1,17 @@ +--- +title: "Use Cases" +linkTitle: "Use Cases" +weight: 5 +--- + +Worked examples of what you can build **on top of the mesh** — patterns that +combine ordinary `sam-node` MCP services (discovery, remote tool calls, leasing) +into something larger, with no changes to the node itself. + +Each use case is harness-agnostic: the orchestrator is any MCP client — an agent +harness (Claude Code, Codex, Antigravity, …) or a custom program — talking to the +mesh MCP tools exposed by a local node. + +The runnable source for every example lives under +[`development/examples/`](https://github.com/google/sam/tree/main/development/examples) +in the repository; each page here links to its example directory. diff --git a/site/content/docs/use-cases/warm-agent-pool.md b/site/content/docs/use-cases/warm-agent-pool.md new file mode 100644 index 00000000..3c423bde --- /dev/null +++ b/site/content/docs/use-cases/warm-agent-pool.md @@ -0,0 +1,201 @@ +--- +title: "Warm Agent Pool" +linkTitle: "Warm Agent Pool" +weight: 10 +--- + +Fan a batch of work across a pool of identical, already-running worker agents — +built entirely from ordinary mesh MCP services, no gossip and no node changes. + +Source: [`development/examples/code-reviewer-pool/`](https://github.com/google/sam/tree/main/development/examples/code-reviewer-pool). + +## The idea + +Some agent tools are expensive to stand up but cheap to reuse — a reviewer that +shells out to an LLM, a sandbox that boots a runtime, a service holding a warm +model in memory. You don't want to spawn one per request, and you don't want a +single instance serialising all your work. What you want is a **pool of +identical, already-running workers** and something that hands them out one job at +a time. + +This use case builds exactly that on the mesh, using **nothing but ordinary MCP +services**. The worked example is a *code reviewer*: several identical reviewer +workers, a manager that leases them, and an orchestrator that fans a batch of +files across the pool in parallel. + +## The pieces + +- **Workers** — plain `code-reviewer` MCP services. Each exposes a single + `review_code` tool that pipes the snippet to an LLM and returns comments + grouped by severity. They're stateless and interchangeable; the pool's job is + to keep them busy. +- **Manager** — a normal MCP service exposing `acquire_worker` / + `release_worker` / `list_workers`. It's also a *northbound MCP client of its + own node*: every few seconds it calls `find_remote_tools(code-reviewer)` to + learn which worker peers exist (via the DHT), and it tracks which of them are + free or busy using **leases**. +- **Orchestrator** — any mesh MCP client (an agent harness or a custom program). + The loop per file is: `acquire_worker` → `call_remote_tool(peer, review_code)` + → `release_worker`. Run those chains concurrently and each acquire hands back a + *different* free worker, so parallel dispatch never collides. + +## Why there's no readiness broadcast + +A classic worker pool needs to know two things: *who exists* and *who's busy*. +The manager gets the first from **DHT discovery** and the second from **its own +leases** — the exact two facts a gossip/readiness broadcast would otherwise +provide. So the whole thing runs on discovery + leasing, no pub/sub. The +tradeoff: leasing is authoritative only while the manager is the *sole* +dispatcher — perfect for a single-manager pool, and the point at which you'd +reach for real coordination if you needed multiple managers. + +## What makes it correct under concurrency + +The interesting part is that "hand out a warm worker, one job at a time" stays +true even when acquires race and workers come and go: + +- **No double-lease** — the manager is single-threaded and `leaseFree()` is fully + synchronous (no `await` between picking a worker and marking it busy), so two + concurrent `acquire_worker` calls can never be handed the same peer. +- **Grace eviction** — discovery never drops a *leased* worker on a transient + miss, and only drops a *free* one after `SAM_GRACE_MISSES` consecutive misses. + A slow, busy worker won't get evicted and re-handed-out mid-review. +- **Fencing tokens** — `acquire_worker` returns a `lease_id`; `release_worker` + only clears the lease if that id still matches, so a late release from an + expired lease can't free a *newer* holder's worker. +- **Single-flight backstop** — even if a lease race ever slipped through, the + worker itself returns `POOL_BUSY` for a second concurrent `review_code`, so the + one-at-a-time invariant holds at the source. + +## Lease enforcement (workers trust the manager, not the caller) + +Workers don't hand out reviews to anyone who can reach them. On `acquire_worker` +the manager **mints a short-lived HMAC token** bound to that worker and lease +expiry; the orchestrator forwards it in the `review_code` arguments, and the +worker **verifies it offline** (shared secret, no call back to the manager). Any +`review_code` without a valid, unexpired token gets `NO_LEASE`. Both sides +default to a hardcoded dev secret (`sam-dev-pool-secret`) so it enforces out of +the box; set a matching `SAM_POOL_SECRET` on the manager and every worker to +override it. Mismatched or one-sided secrets **fail closed** — every call returns +`NO_LEASE`. + +## What you can do with it + +- **Parallel batch work** — fan a directory of files (or tasks) across N warm + workers and collect results as they land, bounded by pool size rather than + serialised. +- **Elastic capacity** — scale a worker deployment up or down mid-job; the + manager picks a new worker up on its next discovery pass and starts leasing it, + and drains one that disappears without corrupting in-flight leases. +- **A reusable pattern** — swap `code-reviewer` for any expensive-to-warm tool + (test runner, sandbox, embedder, browser). The manager is generic; it pools + whatever `SAM_POOL_SERVICE` names. + +## Try it on kind + +The repository ships a [kind](https://kind.sigs.k8s.io/)-based local mesh that +brings up the whole pool with one command. + +### 1. Set an LLM key for the reviewer image + +The reviewer workers shell out to an LLM, so set your API key on the API-key +`ENV` line in `development/examples/code-reviewer-pool/reviewer/Dockerfile` +before building (a free key is fine for the demo). + +### 2. Mesh layout + +The layout ships in `development/kind/mesh-config.yaml`: + +```yaml +node-a: # bare node (orchestrator entry) +node-b: code-reviewer-pool/reviewer # worker +node-c: code-reviewer-pool/reviewer # worker +node-d: code-reviewer-pool/reviewer # worker +node-e: code-reviewer-pool/manager # manager +``` + +### 3. Bring the mesh up and start a local orchestrator node + +```bash +make build # builds ./bin/sam-node (once) +make kind-up # hub + reviewer pool (node-b/c/d) + manager (node-e) +make kind-local-node # local sam-node enrolled in the mesh — LEAVE RUNNING +``` + +`kind-local-node` runs in the foreground in its own shell and exposes the mesh +MCP tools at **`http://127.0.0.1:9099/mcp`** (bearer token `devtoken`) — no +`kubectl port-forward` needed. This local node is your orchestrator's entry +point into the mesh. + +### 4. Point your harness at the local node + +Add the local node as an MCP server in whatever harness you drive the mesh from. +The specifics differ per harness (some use a JSON/TOML config file, others a UI), +but the settings are always the same: + +- **Transport:** HTTP (Streamable HTTP / `http`) +- **URL:** `http://127.0.0.1:9099/mcp` +- **Header:** `Authorization: Bearer devtoken` + +For example, harnesses that use the common `mcpServers` JSON config (Claude Code, +Cursor, and others) would add: + +```json +{ + "mcpServers": { + "sam-mesh": { + "type": "http", + "url": "http://127.0.0.1:9099/mcp", + "headers": { "Authorization": "Bearer devtoken" } + } + } +} +``` + +Consult your harness's MCP documentation for its exact config format. Once +connected, the mesh exposes `acquire_worker`, `release_worker`, `list_workers`, +`find_remote_tools`, and `call_remote_tool` as tools your agent (or program) can +call. + +### 5. Drive the pool + +Have your orchestrator run, per file, in parallel: + +1. `acquire_worker` → returns `{peer_id, tool, lease_id, token}`. +2. `call_remote_tool(peer_id, review_code, {code, token})` — forward the `token` + in the tool arguments (the pool requires it). +3. `release_worker(peer_id, lease_id)` — pass the `lease_id` back so a stale + release can't free a newer holder. + +A natural prompt for an agent harness: + +> Using the `sam-mesh` tools: for each file in +> `development/examples/code-reviewer-pool/samples/`, acquire a worker, call its +> `review_code` with the file contents (forwarding the token), and release it +> (pass the `lease_id` back). Run them in parallel. + +You'll watch the reviews come back concurrently, bounded by the number of workers +in the pool. + +### 6. Elasticity beat (add a worker mid-job) + +Scale a reviewer down, start a larger job, then scale it back up — the manager +picks the new worker up on its next discovery pass and starts leasing it: + +```bash +kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=0 +# start a big job, then: +kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 +``` + +## Configuration + +| var | default | used by | +|-----|---------|---------| +| `SAM_NODE_URL` | `http://127.0.0.1:8080/mcp` | manager | +| `SAM_API_TOKEN` | `devtoken` | manager | +| `SAM_POOL_SERVICE` | `code-reviewer` | manager | +| `SAM_DISCOVERY_MS` | `3000` | manager | +| `SAM_LEASE_MS` | `60000` | manager | +| `SAM_GRACE_MISSES` | `2` | manager | +| `SAM_POOL_SECRET` | `sam-dev-pool-secret` (enforcement always on) | manager + reviewer | From 49e461da8e1185b5fe25405ccd2b47779e5e94c3 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 10 Jul 2026 13:17:22 +0200 Subject: [PATCH 13/13] chore(code-reviewer-pool): drop README and outside-kind compose files Remove the standalone docker-compose path (docker-compose.yaml, env.example, and the compose-only sam-node.yaml) and the README. The example is meant to run only inside the mesh/kind sandbox, which uses sam-node-config.yaml. --- .../examples/code-reviewer-pool/README.md | 123 ------------------ .../reviewer/docker-compose.yaml | 30 ----- .../code-reviewer-pool/reviewer/env.example | 16 --- .../code-reviewer-pool/reviewer/sam-node.yaml | 9 -- 4 files changed, 178 deletions(-) delete mode 100644 development/examples/code-reviewer-pool/README.md delete mode 100644 development/examples/code-reviewer-pool/reviewer/docker-compose.yaml delete mode 100644 development/examples/code-reviewer-pool/reviewer/env.example delete mode 100644 development/examples/code-reviewer-pool/reviewer/sam-node.yaml diff --git a/development/examples/code-reviewer-pool/README.md b/development/examples/code-reviewer-pool/README.md deleted file mode 100644 index 8dbc1395..00000000 --- a/development/examples/code-reviewer-pool/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Code-reviewer pool — warm agent pool demo - -Fan a batch of files out to a **pool of identical reviewer workers**. A -**manager** mesh service tracks which workers are free and leases them out; -Claude Code (the orchestrator) acquires a worker, reviews a file, and releases -it. Reviews run in parallel across the pool. - -Design: `docs/superpowers/specs/2026-06-28-warm-agent-pool-design.md`. - -## Architecture (no gossip, no node changes — just services) - -- **Workers:** plain `code-reviewer` mcp services (node-b/c/d), under `reviewer/`. -- **manager** (node-e, under `manager/`): a normal mcp service exposing - `acquire_worker` / `release_worker` / `list_workers`. It is also a northbound - MCP client of its own node: every few seconds it calls - `find_remote_tools(service_name="code-reviewer")` to learn the worker peers - (DHT), and it tracks free/busy with **leases**. -- **Orchestrator:** `acquire_worker` → `call_remote_tool(peer, review_code)` → - `release_worker`. Leasing hands each concurrent acquire a different worker, so - parallel dispatch never collides. - -Why no gossip: the manager gets *who exists* from DHT discovery and *who's busy* -from its own leases — the two things a readiness broadcast would have provided. -The tradeoff: leasing is authoritative only while the manager is the sole -dispatcher (fine for a single-manager pool). - -### Concurrency hardening - -- **No double-lease:** the manager is single-threaded and `leaseFree()` is - synchronous, so two concurrent `acquire_worker` calls can never get the same - worker. -- **Grace eviction:** discovery never drops a *leased* worker on a transient - miss, and only drops a free one after `SAM_GRACE_MISSES` consecutive misses — - so a slow (busy) worker isn't freed early. -- **Fencing tokens:** `acquire_worker` returns a `lease_id`; `release_worker` - clears the lease only if the id still matches, so a late release from an - expired lease can't free a newer holder's worker. -- **Worker single-flight backstop:** `review_code` returns `POOL_BUSY` if already - running; the caller retries. This keeps the one-at-a-time invariant even if a - lease race ever slips through. - -## Run it - -1. **Set a Gemini key** for the reviewer image (free AI Studio key is fine): - ``` - git update-index --skip-worktree development/examples/code-reviewer-pool/reviewer/Dockerfile - # edit ENV GEMINI_API_KEY= in that Dockerfile - ``` - -2. Mesh layout ships in `development/kind/mesh-config.yaml`: - ```yaml - node-a: # in-cluster bare node - node-b: code-reviewer-pool/reviewer # worker - node-c: code-reviewer-pool/reviewer # worker - node-d: code-reviewer-pool/reviewer # worker - node-e: code-reviewer-pool/manager # manager - ``` - -3. Bring the mesh up and start a **local caller node** — this is the orchestrator - entry point: - ``` - make build # builds ./bin/sam-node (once) - make kind-up # hub + reviewer pool (node-b/c/d) + manager (node-e) - make kind-local-node # local sam-node enrolled in the mesh — LEAVE RUNNING - ``` - `kind-local-node` runs in the foreground (own shell). It exposes the mesh MCP - tools at **`http://127.0.0.1:9099/mcp`** (token `devtoken`) — no - `kubectl port-forward` needed. - -### Drive it from Claude Code - -Register the local node as an MCP server (`.mcp.json` in the project root): -```json -{ - "mcpServers": { - "sam-mesh": { - "type": "http", - "url": "http://127.0.0.1:9099/mcp", - "headers": { "Authorization": "Bearer devtoken" } - } - } -} -``` -Reconnect Claude Code, then prompt: -> Using the sam-mesh tools: find the pool-manager, then for each file in -> `development/examples/code-reviewer-pool/samples/` acquire a worker, call its -> `review_code` with the file contents, and release it (pass the lease_id back -> to release_worker). Run them in parallel. - -Claude fires parallel `acquire_worker` → `call_remote_tool` → `release_worker` -chains; you watch the reviews come back concurrently. - -## Elasticity beat (add a worker mid-job) - -Scale a reviewer down, start a larger job, then scale it back up — the manager -picks the new worker up on its next discovery pass and starts leasing it: -``` -kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=0 -# start a big job, then: -kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 -``` - -## Config (env vars) - -| var | default | used by | -|-----|---------|---------| -| `SAM_NODE_URL` | `http://127.0.0.1:8080/mcp` | manager | -| `SAM_API_TOKEN` | `devtoken` | manager | -| `SAM_POOL_SERVICE` | `code-reviewer` | manager | -| `SAM_DISCOVERY_MS` | `3000` | manager | -| `SAM_LEASE_MS` | `60000` | manager | -| `SAM_GRACE_MISSES` | `2` | manager | -| `SAM_POOL_SECRET` | `sam-dev-pool-secret` (enforcement always on) | manager + reviewer | - -## Lease enforcement - -Every reviewer requires a valid lease: the manager signs a token on -`acquire_worker`, and the reviewer verifies it offline and returns `NO_LEASE` -for any `review_code` call without a valid, unexpired one. Both sides share a -hardcoded dev secret (`sam-dev-pool-secret`) so it works out of the box; override -it by setting the same `SAM_POOL_SECRET` on the manager and every reviewer. A -mismatch (or setting it on only one side) fails closed: every call returns -`NO_LEASE`. diff --git a/development/examples/code-reviewer-pool/reviewer/docker-compose.yaml b/development/examples/code-reviewer-pool/reviewer/docker-compose.yaml deleted file mode 100644 index 4eb5ee57..00000000 --- a/development/examples/code-reviewer-pool/reviewer/docker-compose.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Runs a sam-node (pulled from the internet) plus the Gemini-backed code-reviewer. -# Copy .env.example to .env and fill in the values before `docker compose up`. -services: - code-reviewer: - # Your private GHCR image. `docker compose build` tags it here so you can push it. - image: ${CODE_REVIEWER_IMAGE:-ghcr.io/kaisoz/gemini-code-reviewer-mcp:latest} - build: . - environment: - GEMINI_API_KEY: ${GEMINI_API_KEY:?set GEMINI_API_KEY in .env} - GEMINI_CLI_TRUST_WORKSPACE: "true" - expose: - - "7779" - - sam-node: - image: ${SAM_NODE_IMAGE:-ghcr.io/google/sam-node:latest} - depends_on: - - code-reviewer - command: - - run - - --data-dir=/data - - --bind-addr=0.0.0.0:8080 - - --api-token=${SAM_API_TOKEN:?set SAM_API_TOKEN in .env} - - --config=/config/sam-node.yaml - volumes: - - ./sam-node.yaml:/config/sam-node.yaml:ro - - ${SAM_DATA_DIR:-./sam-data}:/data # your pre-enrolled identity folder - ports: - - "8080:8080" # control-plane / MCP API - - "5001:5001/udp" # libp2p - - "5002:5002" # libp2p diff --git a/development/examples/code-reviewer-pool/reviewer/env.example b/development/examples/code-reviewer-pool/reviewer/env.example deleted file mode 100644 index e57d9780..00000000 --- a/development/examples/code-reviewer-pool/reviewer/env.example +++ /dev/null @@ -1,16 +0,0 @@ -# Copy to .env and fill in. - -# Google AI Studio key the reviewer's gemini CLI uses. -GEMINI_API_KEY= - -# Static Bearer token for the sam-node control-plane API. -SAM_API_TOKEN=devtoken - -# Private code-reviewer image (built/pushed by you). -CODE_REVIEWER_IMAGE=ghcr.io/kaisoz/code-reviewer-mcp:latest - -# sam-node image pulled from the internet. -SAM_NODE_IMAGE=ghcr.io/google/sam-node:latest - -# Host folder holding the node's enrolled identity. -SAM_DATA_DIR=./sam-data diff --git a/development/examples/code-reviewer-pool/reviewer/sam-node.yaml b/development/examples/code-reviewer-pool/reviewer/sam-node.yaml deleted file mode 100644 index bac090fe..00000000 --- a/development/examples/code-reviewer-pool/reviewer/sam-node.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "mcp" - name: "code-reviewer" - description: "Reviews code snippets via a Gemini-backed agent" - # Reach the reviewer by its compose service name, not 127.0.0.1. - target_url: "http://code-reviewer:7779/mcp"