Files
Agent-System/scripts/mcp/pi-subagents-mcp.mjs
T

587 lines
20 KiB
JavaScript

#!/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'));