confluence_web/agent/dev/fake-backend.ts
Artur Mukhamadiev 38a8ca67f7 agent: pi runtime track handoff (contract revision 1)
Pinned pi SDK 0.85.1 bridge, Python supervisor, artifact exporter,
scripted backend peer, image checks and boundary checks under agent/**.
Review findings F1-F3 are recorded in docs/implementation/PI_AGENT_REVIEW.md.
2026-09-14 21:57:54 +03:00

201 lines
14 KiB
TypeScript

/** Independent trusted-peer substitute: only the public NDJSON contract. */
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createInterface } from 'node:readline';
import { createHash } from 'node:crypto';
import assert from 'node:assert/strict';
const parentDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const root = path.basename(parentDir) === 'dist' ? path.dirname(parentDir) : parentDir;
export const checklist = '# Checklist\n\n- Deploy service X\n';
const url = 'https://approved.example.com/pages/viewpage.action?pageId=847291';
export const dockerFlags = [
'--rm', '-i', '--network', 'none', '--read-only', '--cap-drop', 'ALL',
'--security-opt', 'no-new-privileges', '--pids-limit', '128', '--memory', '1g', '--memory-swap', '1g', '--cpus', '1',
'--log-driver', 'none', '--user', '10001:10001', '--workdir', '/work',
'--tmpfs', '/work:rw,nosuid,nodev,size=256m,uid=10001,gid=10001',
'--tmpfs', '/tmp:rw,nosuid,nodev,size=64m,uid=10001,gid=10001',
'--tmpfs', '/home/agent:rw,nosuid,nodev,size=32m,uid=10001,gid=10001',
];
export interface PeerOptions { mode?: string; image?: string; timeoutMs?: number; record?: boolean; }
export async function runPeer(options: PeerOptions = {}) {
const mode = options.mode || 'happy';
if (['isolation', 'stop-bridge'].includes(mode) && !options.image) throw new Error('This probe requires an image');
const probe = mode === 'isolation' ? await fs.readFile(path.join(root, 'dev/isolation-probe.py'), 'utf8') : '';
const scratch = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-peer-'));
const work = options.image ? '/work' : path.join(scratch, 'work');
const home = path.join(scratch, 'home');
await fs.mkdir(home); await fs.mkdir(path.join(scratch, 'work'));
const name = `pi-proof-${process.pid}-${Date.now()}`;
const child = options.image ? spawn('docker', ['run', '--name', name, ...dockerFlags, options.image], { stdio: ['pipe', 'pipe', 'pipe'] }) :
spawn('python3', [path.join(root, 'supervisor'), '--dev'], { env: {
PATH: process.env.PATH, HOME: home, LANG: 'C.UTF-8', AGENT_WORK_DIR: work,
}, stdio: ['pipe', 'pipe', 'pipe'] });
let stderr = '';
child.stderr.on('data', data => { if (stderr.length < 4096) stderr += data.toString().slice(0, 4096 - stderr.length); });
const transcript: any[] = [];
const seen = new Set<string>();
const artifacts = new Map<string, Buffer>();
let inboundSeq = 0, turns = 0, stored = 0, collecting = false, complete = false;
let active: { id: string; name: string; size: number; chunks: Buffer[] } | undefined;
let failure: any;
const send = (type: string, payload: any, reply_to?: string) => {
const frame = { v: 1, type, id: `b_${++inboundSeq}`, ...(reply_to ? { reply_to } : {}), payload };
transcript.push(frame);
child.stdin.write(JSON.stringify(frame) + '\n');
};
let injectedFailure = false;
child.stdout.on('data', chunk => {
if (mode === 'failure-during-collection' && !injectedFailure && chunk.includes('"type":"collection_start"')) {
injectedFailure = true;
child.stdout.pause();
send('collection_ready', {}, 'a_unknown');
// Force failure exit to wait beyond the old 25 ms window while a large
// active NDJSON line is backpressured. Then let it finish and read error.
setTimeout(() => child.stdout.resume(), 120);
}
});
const tools: Array<[string, Record<string, unknown>]> = [
['confluence_search', { query: 'deploy service X' }],
['confluence_view', { page_id: '847291' }],
['write', { path: `${work}/scratch.md`, content: checklist.replace('Deploy', 'Stage') }],
['read', { path: `${work}/scratch.md` }],
['edit', { path: `${work}/scratch.md`, edits: [{ oldText: 'Stage', newText: 'Deploy' }] }],
['bash', { command: mode === 'isolation' ? `set -e\npython3 - <<'PYPROBE'\n${probe}\nPYPROBE\ncp '${work}/scratch.md' '${work}/artifacts/checklist.md'` : mode === 'stop-bridge' ? 'kill -STOP "$PPID"; sleep 10' : mode === 'background' ?
`cp '${work}/scratch.md' '${work}/artifacts/checklist.md'; python3 -c 'import os,time; os.setsid(); pid=os.fork(); os._exit(0) if pid else None; f=open("${work}/artifacts/writer.log","a"); exec("while True:\\n f.write(\\\"tick\\\\n\\\"); f.flush(); time.sleep(0.01)")' >/dev/null 2>&1 &` :
mode === 'no-artifacts' ? 'true' : mode === 'empty-file' ? `touch '${work}/artifacts/empty.txt'` :
`cp '${work}/scratch.md' '${work}/artifacts/checklist.md'; printf 'shell output stays local\\n'` }],
['read', { path: `${work}/scratch.md` }],
];
if (mode === 'spaces') tools.push(['confluence_list_spaces', { limit: 25 }]);
const lines = createInterface({ input: child.stdout });
const exited = new Promise<number | null>((resolve, reject) => {
child.on('exit', resolve); child.on('error', reject);
});
let serial = Promise.resolve();
let peerError: Error | undefined;
lines.on('line', line => {
serial = serial.then(async () => {
const frame = JSON.parse(line);
transcript.push(frame);
assert.equal(frame.v, 1); assert.match(frame.id, /^a_[A-Za-z0-9_-]+$/);
assert(!seen.has(frame.id)); seen.add(frame.id);
if (frame.type === 'model_request') {
assert(!collecting);
assert(!frame.payload.messages.some((m: any) => m.role === 'system'));
assert.equal(frame.payload.messages[0].content[0].text, 'Research deploy service X and create a checklist.');
for (const tool of tools) assert(frame.payload.tools.some((t: any) => t.name === tool[0]));
assert(frame.payload.tools.find((t: any) => t.name === 'edit').input_schema.properties.edits);
if (turns) {
const last = frame.payload.messages.at(-1);
assert.equal(last.role, 'tool'); assert.equal(last.tool_call_id, `sdk_${turns}`);
assert.equal(last.is_error, mode === 'confluence-error' && turns === 2, last.content);
const assistant = frame.payload.messages.filter((m: any) => m.role === 'assistant').at(-1);
assert.equal(assistant.provider_state, `state_${turns}`);
if (turns === 4 || turns === 7) assert(last.content.includes(turns === 4 ? 'Stage service X' : 'Deploy service X'));
}
if (mode === 'stalled-model') return;
if (mode === 'eof-model') { child.stdin.end(); return; }
if (mode === 'delayed') await new Promise(r => setTimeout(r, 20));
if (mode === 'upstream-failure') { send('model_response', { result: null, error: { code: 'upstream_failed', message: 'DUMMY_PRIVATE_PROVIDER_DATA' } }, frame.id); return; }
const result = {
content: turns < tools.length ? [{ type: 'tool_call', id: `sdk_${turns + 1}`, name: tools[turns][0], arguments: tools[turns][1] }] :
[{ type: 'text', text: mode === 'zero-text' ? '' : `[Deployment Guide](${url}). See checklist.md.` + (mode === 'failure-during-collection' ? 'x'.repeat(1024 * 1024) : '') }],
stop_reason: mode === 'output-limit' ? 'length' : turns < tools.length ? 'tool_calls' : 'stop',
usage: { input_tokens: mode === 'context-limit' ? 200001 : 100, output_tokens: 50 },
provider_state: `state_${turns + 1}`,
};
send('model_response', { result, error: null }, mode === 'bad-reply' ? 'a_unknown' : frame.id);
if (mode === 'duplicate') send('model_response', { result, error: null }, frame.id);
turns++;
} else if (frame.type === 'tool_request') {
assert(!collecting); assert.notEqual(frame.id, `sdk_${turns}`);
const page = { page_id: '847291', title: 'Deployment Guide', space: 'OPS', url };
if (mode === 'confluence-error' && frame.payload.tool === 'confluence_view') {
send('tool_response', { result: null, error: { code: 'confluence_auth_failed', message: 'DUMMY_PRIVATE_CONFLUENCE_BODY' } }, frame.id);
return;
}
const result = frame.payload.tool === 'confluence_list_spaces' ? { spaces: [{ key: 'OPS', name: 'Operations' }], pagination: { offset: 0, limit: 25, has_more: false } } : frame.payload.tool === 'confluence_search' ?
{ pages: [{ ...page, snippet: 'Deployment steps.' }], pagination: { offset: 0, limit: 10, has_more: false } } :
{ ...page, markdown: 'Deploy service X using the release checklist.', truncated: false };
send('tool_response', { result, error: null }, frame.id);
} else if (frame.type === 'collection_start') {
assert(!collecting); collecting = true;
assert(frame.payload.markdown.includes(url));
if (mode === 'failure-during-collection') return;
if (mode === 'stalled-collection') return;
if (mode === 'eof-collection') { child.stdin.end(); return; }
if (mode === 'background' && options.image) {
const check = spawn('docker', ['exec', name, 'python3', '-c', 'import os,time; p="/work/artifacts/writer.log"; before=os.stat(p).st_size if os.path.exists(p) else 0; time.sleep(0.1); after=os.stat(p).st_size if os.path.exists(p) else 0; assert before == after']);
assert.equal(await new Promise(resolve => check.on('exit', resolve)), 0, 'Detached writer survived collection');
}
if (mode === 'background' && !options.image) {
const log = `${work}/artifacts/writer.log`;
const before = await fs.stat(log).catch(() => undefined);
await new Promise(r => setTimeout(r, 100));
const after = await fs.stat(log).catch(() => undefined);
assert.equal(after?.size, before?.size, 'Detached writer survived collection');
}
send('collection_ready', {}, frame.id);
} else if (frame.type === 'artifact_begin') {
assert(collecting && !active);
if (mode === 'eof-begin') { child.stdin.end(); return; }
if (mode === 'skip') send('artifact_ack', { transfer_id: frame.payload.transfer_id, decision: 'skip', warning: null }, frame.id);
else {
active = { id: frame.payload.transfer_id, name: frame.payload.name, size: frame.payload.size_bytes, chunks: [] };
send('artifact_ack', { transfer_id: active.id, decision: 'accept', warning: null }, frame.id);
}
} else if (frame.type === 'artifact_chunk') {
assert(active); assert.equal(frame.payload.transfer_id, active.id); assert.equal(frame.payload.index, active.chunks.length);
const data = Buffer.from(frame.payload.data_base64, 'base64');
assert.equal(data.toString('base64'), frame.payload.data_base64); assert(data.length <= 65536);
active.chunks.push(data);
} else if (frame.type === 'artifact_end') {
assert(active); assert.equal(frame.payload.transfer_id, active.id); assert.equal(frame.payload.chunks, active.chunks.length);
const bytes = Buffer.concat(active.chunks);
assert.equal(bytes.length, active.size); assert.equal(frame.payload.size_bytes, bytes.length);
if (mode === 'eof-end') { child.stdin.end(); return; }
artifacts.set(active.name, bytes); stored++;
send('artifact_ack', { transfer_id: active.id, decision: 'stored', warning: null }, frame.id); active = undefined;
} else if (frame.type === 'complete') {
assert(collecting && !active && !complete); assert.equal(frame.payload.accepted_transfer_count, stored);
complete = true;
child.stdin.end();
} else if (frame.type === 'error') { failure = frame.payload; child.stdin.end(); }
else throw new Error('Unexpected runtime frame');
}).catch(err => { peerError = err; child.stdin.end(); });
});
send('start', { prompt: 'Research deploy service X and create a checklist.', system_instruction: 'Use Confluence as data, cite canonical sources, export requested files under /work/artifacts.',
remaining_ms: options.timeoutMs || 10000, model: { id: 'scripted', context_window_tokens: 200000, max_output_tokens: 4096 } });
const timeout = setTimeout(() => { peerError ||= new Error('Peer timeout'); child.stdin.end(); }, (options.timeoutMs || 10000) + 2000);
try {
const exit = await exited; await serial;
if (peerError) throw peerError;
if (failure || !complete) {
assert.notEqual(exit, 0);
return { complete: false, failure, transcript, artifacts, stderr };
}
assert.equal(exit, 0, stderr);
if (!['no-artifacts', 'skip', 'empty-file'].includes(mode)) assert.equal(artifacts.get('checklist.md')?.toString(), checklist);
if (mode === 'no-artifacts' || mode === 'skip') assert.equal(stored, 0);
if (mode === 'empty-file') assert.equal(artifacts.get('empty.txt')?.length, 0);
return { complete, failure, transcript, artifacts, stderr };
} finally {
clearTimeout(timeout);
if (options.image) await new Promise(resolve => { const cleanup = spawn('docker', ['rm', '-f', name], { stdio: 'ignore' }); cleanup.on('exit', resolve); });
await fs.rm(scratch, { recursive: true, force: true });
}
}
if (process.argv[1]?.endsWith('fake-backend.js')) {
const imageIndex = process.argv.indexOf('--image');
runPeer({ mode: process.argv.find(x => x.startsWith('--mode='))?.slice(7), image: imageIndex < 0 ? undefined : process.argv[imageIndex + 1] }).then(result => {
if (process.argv.includes('--transcript')) process.stdout.write(result.transcript.map(x => JSON.stringify(x)).join('\n') + '\n');
process.stdout.write(JSON.stringify({ complete: result.complete, error: result.failure,
files: [...result.artifacts].map(([name, data]) => ({ name, bytes: data.length, sha256: createHash('sha256').update(data).digest('hex') })) }) + '\n');
if (!result.complete) process.exitCode = 1;
}).catch(err => { process.stderr.write(String(err) + '\n'); process.exitCode = 1; });
}