#!/usr/bin/env node /** * pi-subagents MCP (stdio, zero deps). * Tools: pi_subagent_roles, pi_subagent_run, pi_subagent_result. * * - Trusted executor: INSTALL_ROOT/scripts/pi_subagents.py (this server install). * - Authoritative agentctl: discovered main worktree scripts/agentctl.py (governance SSOT). * - worktree must share git-common-dir with INSTALL_ROOT. * - Request JSON only under private OS tmp (no pre-lease repo writes). */ import { spawn, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import fs from 'node:fs'; import os from 'node:os'; import crypto from 'node:crypto'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const INSTALL_ROOT = path.resolve(__dirname, '../..'); const MAX_PAYLOAD = 2 * 1024 * 1024; const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; const active = new Map(); // key -> { child, digest, stdout, stderr, exitCode, rejection } function gitCommonDir(cwd) { const r = spawnSync('git', ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'], { encoding: 'utf8', }); if (r.status !== 0) return null; return r.stdout.trim(); } function gitMainWorktree(cwd) { const list = spawnSync('git', ['-C', cwd, 'worktree', 'list', '--porcelain'], { encoding: 'utf8' }); if (list.status !== 0) return null; const first = /^worktree (.+)$/m.exec(list.stdout || ''); return first ? first[1] : null; } function validateId(value, label) { if (typeof value !== 'string' || !ID_RE.test(value) || value === '.' || value === '..') { throw new Error(`invalid ${label}`); } } function resolveRepo(worktree) { if (!worktree || typeof worktree !== 'string') { throw new Error('worktree is required'); } const wt = path.resolve(worktree); const common = gitCommonDir(wt); if (!common) throw new Error(`not a git worktree: ${wt}`); const installCommon = gitCommonDir(INSTALL_ROOT); if (!installCommon) throw new Error('INSTALL_ROOT is not a git worktree'); if (common !== installCommon) { throw new Error('worktree does not share INSTALL_ROOT git-common-dir (cross-repo refused)'); } // Authoritative main = first worktree list entry; executor stays at INSTALL_ROOT. const discovered = gitMainWorktree(INSTALL_ROOT) || INSTALL_ROOT; return { main: discovered, discoveredMain: discovered, worktree: wt, common }; } function agentctlPath(main) { return path.join(main, 'scripts', 'agentctl.py'); } function executorPath() { return path.join(INSTALL_ROOT, 'scripts', 'pi_subagents.py'); } function safeConfigPath(worktree, config) { const defaultRel = '.doc/ops/2026-09-24-subagent-model-config.md'; const rel = config || defaultRel; if (typeof rel !== 'string' || !rel.trim()) throw new Error('invalid config path'); if (path.isAbsolute(rel)) throw new Error('config must be worktree-relative'); if (rel.includes('\0') || rel.split(path.sep).includes('..')) { throw new Error('config path traversal refused'); } const abs = path.resolve(worktree, rel); const normWt = worktree.endsWith(path.sep) ? worktree : worktree + path.sep; if (!(abs === worktree || abs.startsWith(normWt))) { throw new Error('config escapes worktree'); } // refuse symlink config try { if (fs.lstatSync(abs).isSymbolicLink()) throw new Error('config symlink refused'); } catch (e) { if (e.message === 'config symlink refused') throw e; // missing checked by executor } // only the registered config file name if (path.basename(abs) !== '2026-09-24-subagent-model-config.md') { throw new Error('config must be the registered model config file'); } return abs; } function payloadDigest(request) { return crypto.createHash('sha256').update(JSON.stringify(request)).digest('hex'); } function runSync(argv, { cwd, timeout = 120000, env } = {}) { const r = spawnSync(argv[0], argv.slice(1), { cwd, encoding: 'utf8', timeout, maxBuffer: 8 * 1024 * 1024, env: env || process.env, }); return { ok: r.status === 0, code: r.status ?? 1, stdout: r.stdout || '', stderr: r.stderr || '', error: r.error ? String(r.error) : undefined, }; } function textResult(payload) { const body = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2); return { content: [{ type: 'text', text: body }] }; } function errResult(msg) { const body = typeof msg === 'string' ? msg : JSON.stringify(msg); return { content: [{ type: 'text', text: body }], isError: true }; } function parseJsonLine(text) { const lines = String(text || '').split(/\n/).map((l) => l.trim()).filter(Boolean); for (let i = lines.length - 1; i >= 0; i--) { try { return JSON.parse(lines[i]); } catch { /* continue */ } } return null; } function sanitizeErr(text) { return String(text || '') .replace(/(api[_-]?key|token|secret|password|authorization)\s*[:=]\s*\S+/gi, '$1=[REDACTED]') .slice(0, 2000); } function killActiveTree(meta) { if (!meta?.child) return; const child = meta.child; try { if (child.pid) { try { process.kill(-child.pid, 'SIGTERM'); } catch { /* not a group */ } try { child.kill('SIGTERM'); } catch { /* */ } } } catch { /* */ } setTimeout(() => { try { if (child.exitCode === null && child.pid) { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* */ } try { child.kill('SIGKILL'); } catch { /* */ } } } catch { /* */ } }, 2000); } function reapAll() { for (const meta of active.values()) { killActiveTree(meta); } } const TOOLS = [ { name: 'pi_subagent_roles', description: 'Read-only: list PI subagent roles/models from markdown config (does not invoke PI)', inputSchema: { type: 'object', properties: { worktree: { type: 'string', description: 'Absolute path to registered task worktree' }, task_id: { type: 'string' }, config: { type: 'string', description: 'Optional config path relative to worktree' }, }, required: ['worktree', 'task_id'], additionalProperties: false, }, }, { name: 'pi_subagent_run', description: 'Start one PI subagent run under main-worktree agentctl lease (non-blocking)', inputSchema: { type: 'object', properties: { worktree: { type: 'string' }, task_id: { type: 'string' }, request: { type: 'object', description: 'PI subagent request (task_id optional; injected from outer task_id)', properties: { task_id: { type: 'string', description: 'Optional; must match outer task_id when set' }, role_id: { type: 'string' }, request_id: { type: 'string' }, goal: { type: 'string' }, authorization_ref: { type: 'string' }, source_sha: { type: 'string', description: 'Full commit hash' }, input_files: { type: 'array', items: { type: 'string' } }, timeout_seconds: { type: 'integer' }, approved_commands: { type: 'array', items: { type: 'array', items: { type: 'string' } }, description: 'test_run/acceptance_check only', }, plan_digest: { type: 'string', description: 'sha256 hex for test roles' }, model_override: { type: 'object' }, }, required: [ 'role_id', 'request_id', 'goal', 'authorization_ref', 'source_sha', 'input_files', 'timeout_seconds', ], additionalProperties: false, }, config: { type: 'string', description: 'Worktree-relative registered model config path' }, }, required: ['worktree', 'task_id', 'request'], additionalProperties: false, }, }, { name: 'pi_subagent_result', description: 'Read durable receipt for a request_id (does not invoke PI). running is not isError; terminal failure is.', inputSchema: { type: 'object', properties: { worktree: { type: 'string' }, task_id: { type: 'string' }, request_id: { type: 'string' }, }, required: ['worktree', 'task_id', 'request_id'], additionalProperties: false, }, }, ]; function callTool(name, args = {}) { try { if (name === 'pi_subagent_roles') { validateId(args.task_id, 'task_id'); const { worktree } = resolveRepo(args.worktree); const config = safeConfigPath(worktree, args.config); const r = runSync(['python3', '-B', executorPath(), 'roles', '--config', config], { cwd: worktree, }); const payload = parseJsonLine(r.stdout) || parseJsonLine(r.stderr); if (!r.ok) return errResult(payload || sanitizeErr(r.stderr || r.stdout || r.error)); return textResult(payload); } if (name === 'pi_subagent_result') { validateId(args.task_id, 'task_id'); validateId(args.request_id, 'request_id'); const { worktree } = resolveRepo(args.worktree); const key = `${args.task_id}/${args.request_id}`; const meta = active.get(key); const r = runSync([ 'python3', '-B', executorPath(), 'result', '--task', args.task_id, '--request-id', args.request_id, ], { cwd: worktree }); const payload = parseJsonLine(r.stdout) || parseJsonLine(r.stderr); const surfaceRejection = () => { if (meta?.rejection) return errResult(meta.rejection); if (meta && meta.exitCode !== null && meta.exitCode !== 0) { const errPayload = parseJsonLine(meta.stderr()) || parseJsonLine(meta.stdout()) || { ok: false, code: 'SUPERVISOR_FAILED', message: sanitizeErr(meta.stderr() || meta.stdout() || 'supervisor failed'), exit_code: meta.exitCode, }; return errResult(errPayload); } return null; }; if (!payload) { const rej = surfaceRejection(); if (rej) return rej; if (meta && meta.exitCode === null) { return textResult({ ok: true, run_status: 'running', task_id: args.task_id, request_id: args.request_id, }); } return errResult({ ok: false, code: 'NOT_FOUND', message: sanitizeErr(r.stderr || r.stdout || r.error || 'no receipt'), }); } // Settled supervisor rejection with no durable receipt (UNKNOWN_REQUEST). if (payload.code === 'UNKNOWN_REQUEST') { const rej = surfaceRejection(); if (rej) return rej; // Child still in flight: do not treat as terminal unknown. if (meta && meta.exitCode === null) { return textResult({ ok: true, run_status: 'running', task_id: args.task_id, request_id: args.request_id, }); } return errResult(payload); } if (!r.ok && payload.code && payload.code !== 'UNKNOWN_REQUEST') { return errResult(payload); } // In-flight: never isError if (payload.run_status === 'running') { return textResult(payload); } if (payload.ok === false || (payload.run_status && payload.run_status !== 'succeeded')) { return errResult(payload); } return textResult(payload); } if (name === 'pi_subagent_run') { validateId(args.task_id, 'task_id'); const { main, worktree } = resolveRepo(args.worktree); const request = args.request; if (!request || typeof request !== 'object' || Array.isArray(request)) { return errResult({ ok: false, code: 'INVALID_REQUEST', message: 'request object required' }); } if (request.task_id && request.task_id !== args.task_id) { return errResult({ ok: false, code: 'TASK_MISMATCH', message: 'request.task_id must match task_id' }); } if (Object.prototype.hasOwnProperty.call(request, 'config_path')) { return errResult({ ok: false, code: 'INVALID_REQUEST', message: 'request.config_path is not supported; use tool config argument', }); } request.task_id = args.task_id; const requestId = request.request_id; try { validateId(requestId, 'request_id'); } catch (e) { return errResult({ ok: false, code: 'INVALID_REQUEST', message: String(e.message || e) }); } const digest = payloadDigest(request); const key = `${args.task_id}/${requestId}`; const existing = active.get(key); if (existing && existing.exitCode === null) { if (existing.digest === digest) { return textResult({ ok: true, accepted: true, running: true, task_id: args.task_id, request_id: requestId, request_digest: digest, }); } return errResult({ ok: false, code: 'REQUEST_DIGEST_MISMATCH', message: 'active request_id with different payload', }); } let config; try { config = safeConfigPath(worktree, args.config); } catch (e) { return errResult({ ok: false, code: 'CONFIG_INVALID', message: String(e.message || e) }); } const resource = `path:.doc/test-reports/assets/pi-subagents/${args.task_id}/${requestId}`; // Private OS tmp only — never write into worktree before lease. let tmpDir; let reqPath; try { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-subagent-mcp-')); reqPath = path.join(tmpDir, `${requestId}.json`); fs.writeFileSync(reqPath, JSON.stringify(request), { mode: 0o600 }); } catch (e) { return errResult({ ok: false, code: 'TMP_WRITE_FAILED', message: sanitizeErr(String(e.message || e)), }); } let child; try { child = spawn( 'python3', [ '-B', agentctlPath(main), 'with', '--task', args.task_id, '--resource', resource, '--', 'python3', '-B', executorPath(), 'run', '--request', reqPath, '--config', config, ], { cwd: worktree, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], detached: true, // own process group for cleanup }, ); } catch (e) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ } return errResult({ ok: false, code: 'SPAWN_FAILED', message: sanitizeErr(String(e.message || e)), }); } let stdout = ''; let stderr = ''; child.stdout.on('data', (c) => { stdout += c.toString('utf8'); if (stdout.length > MAX_PAYLOAD) stdout = stdout.slice(-MAX_PAYLOAD); }); child.stderr.on('data', (c) => { stderr += c.toString('utf8'); if (stderr.length > MAX_PAYLOAD) stderr = stderr.slice(-MAX_PAYLOAD); }); const meta = { child, digest, stdout: () => stdout, stderr: () => stderr, exitCode: null, rejection: null, tmpDir, reqPath, }; child.on('error', (err) => { meta.exitCode = 1; meta.rejection = { ok: false, code: 'SPAWN_FAILED', message: sanitizeErr(String(err.message || err)), }; try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ } }); child.on('exit', (code) => { meta.exitCode = code; if (code && code !== 0) { const parsed = parseJsonLine(stderr) || parseJsonLine(stdout); if (parsed && parsed.code) { meta.rejection = { ok: false, code: parsed.code, message: sanitizeErr(parsed.message || parsed.code), }; } else { meta.rejection = { ok: false, code: 'SUPERVISOR_FAILED', message: sanitizeErr(stderr || stdout || `exit ${code}`), exit_code: code, }; } } try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ } }); active.set(key, meta); return textResult({ ok: true, accepted: true, running: true, task_id: args.task_id, request_id: requestId, request_digest: digest, resource, }); } return errResult(`unknown tool: ${name}`); } catch (e) { return errResult({ ok: false, code: 'MCP_ERROR', message: sanitizeErr(String(e?.message || e)) }); } } function send(msg) { const body = JSON.stringify(msg); if (Buffer.byteLength(body, 'utf8') > MAX_PAYLOAD) { const id = msg?.id; process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32600, message: 'response too large' }, }) + '\n'); return; } process.stdout.write(body + '\n'); } function handle(msg) { if (!msg || typeof msg !== 'object') return; if (msg.method && msg.id === undefined) { return; } const { id, method, params } = msg; try { if (method === 'initialize') { send({ jsonrpc: '2.0', id, result: { protocolVersion: params?.protocolVersion || '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'pi-subagents', version: '1.0.0' }, }, }); return; } if (method === 'tools/list') { send({ jsonrpc: '2.0', id, result: { tools: TOOLS } }); return; } if (method === 'tools/call') { const name = params?.name; const args = params?.arguments || {}; const raw = JSON.stringify(args); if (Buffer.byteLength(raw, 'utf8') > MAX_PAYLOAD) { send({ jsonrpc: '2.0', id, result: errResult('arguments exceed payload bound') }); return; } const result = callTool(name, args); send({ jsonrpc: '2.0', id, result }); return; } if (method === 'ping') { send({ jsonrpc: '2.0', id, result: {} }); return; } send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }); } catch (e) { send({ jsonrpc: '2.0', id, error: { code: -32000, message: String(e?.message || e) } }); } } let buf = Buffer.alloc(0); process.stdin.on('data', (chunk) => { if (buf.length + chunk.length > MAX_PAYLOAD * 4) { buf = Buffer.alloc(0); process.stderr.write('payload overflow; reset buffer\n'); return; } buf = Buffer.concat([buf, chunk]); while (true) { const headerEnd = buf.indexOf('\r\n\r\n'); if (headerEnd !== -1) { const header = buf.slice(0, headerEnd).toString('utf8'); const m = /Content-Length:\s*(\d+)/i.exec(header); if (!m) { buf = buf.slice(headerEnd + 4); continue; } const len = Number(m[1]); if (len > MAX_PAYLOAD) { buf = buf.slice(headerEnd + 4); continue; } const start = headerEnd + 4; if (buf.length < start + len) return; const body = buf.slice(start, start + len).toString('utf8'); buf = buf.slice(start + len); try { handle(JSON.parse(body)); } catch (e) { process.stderr.write(`parse error: ${e}\n`); } continue; } const nl = buf.indexOf('\n'); if (nl === -1) return; const line = buf.slice(0, nl).toString('utf8').trim(); buf = buf.slice(nl + 1); if (!line || line.startsWith('Content-Length')) continue; try { handle(JSON.parse(line)); } catch (e) { process.stderr.write(`ndjson parse error: ${e}\n`); } } }); function shutdown(signal) { process.stderr.write(`pi-subagents-mcp shutting down (${signal})\n`); reapAll(); setTimeout(() => process.exit(0), 500).unref?.(); } process.stdin.on('end', () => { reapAll(); process.exit(0); }); process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT'));