170 lines
12 KiB
JavaScript
170 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
// Deterministic provider only. The production Rust runner, tool authority, and
|
|
// authenticated MCP gateway still execute every tool and enforce access.
|
|
import { randomUUID } from 'node:crypto';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
import { createInterface } from 'node:readline';
|
|
if (process.argv.includes('--version')) { console.log('codex-cli 0.115.0 (in-feed fixture)'); process.exit(0); }
|
|
let threadId = `fixture-${randomUUID()}`;
|
|
let turnId, toolSequence = 0, declined = false;
|
|
const recoveryFixture = process.env.PAPERCLIP_RECOVERY_FIXTURE === "1";
|
|
let currentObjective = "";
|
|
let emitCeoLineage = false;
|
|
let completionContract = { revision: "1", criterionIds: ["objective"] };
|
|
const pending = new Map();
|
|
const send = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
const call = (tool, args) => new Promise((resolve, reject) => {
|
|
const id = `connection-tool-${++toolSequence}`;
|
|
pending.set(id, { resolve, reject });
|
|
send({ id, method: 'item/tool/call', params: { threadId, turnId, callId: id, tool, arguments: args } });
|
|
});
|
|
function unwrap(result) {
|
|
const texts = result?.contentItems ?? result?.content ?? [];
|
|
for (const item of texts) {
|
|
try { const parsed = JSON.parse(item.text); return parsed.value ?? parsed; } catch {}
|
|
}
|
|
return result;
|
|
}
|
|
async function mcp(method, params = {}) {
|
|
// Native execution creates a dedicated provider home and issues a short-lived
|
|
// gateway token for this fixture run. Never load the user's normal Codex home.
|
|
if (!process.env.CODEX_HOME || !process.env.HOME
|
|
|| resolve(process.env.CODEX_HOME) !== resolve(process.env.HOME)) {
|
|
throw new Error('The fixture requires an isolated native provider home');
|
|
}
|
|
const config = readFileSync(join(process.env.CODEX_HOME, 'config.toml'), 'utf8');
|
|
const url = JSON.parse(config.match(/^url = (.+)$/m)?.[1] ?? 'null');
|
|
const authorization = JSON.parse(config.match(/Authorization = (".*?")/m)?.[1] ?? 'null');
|
|
if (!url || !authorization) throw new Error('Native continuation did not install the MCP gateway');
|
|
const endpoint = new URL(url);
|
|
if (endpoint.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname)
|
|
|| endpoint.username || endpoint.password || endpoint.hash) {
|
|
throw new Error('The fixture only accepts a local native gateway');
|
|
}
|
|
const response = await fetch(endpoint, { redirect: 'error', method: 'POST', headers: { Authorization: authorization, 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++toolSequence, method, params }) });
|
|
if (!response.ok) throw new Error(`Gateway HTTP ${response.status}`);
|
|
const text = await response.text();
|
|
const envelope = JSON.parse(text.startsWith('event:') || text.startsWith('data:') ? text.split('\n').find((line) => line.startsWith('data:')).slice(5) : text);
|
|
if (envelope.error) throw new Error(envelope.error.message);
|
|
return envelope.result;
|
|
}
|
|
async function finish(text, evidenceRef) {
|
|
return call('paperclip_finish', { schema: 'paperclip.run_result.v1', reportedWorkDisposition: 'done', summary: text,
|
|
completionClaim: { contractRevision: completionContract.revision, objectiveSatisfied: true, criteria: completionContract.criterionIds.map((criterionId) => ({ criterionId, status: 'satisfied', evidenceRefs: [evidenceRef] })), remainingWork: [] },
|
|
evidence: [{ ref: evidenceRef }], verification: [{ commandOrCheck: 'Fixture outcome', status: 'passed' }], attentionRequests: [], artifacts: [] });
|
|
}
|
|
async function execute() {
|
|
if (process.argv.includes('--completion-stream-fixture')) {
|
|
// Report completion before a deliberately slow final answer. The real
|
|
// runner and control plane must keep listening after the tool succeeds.
|
|
await finish('The fixture work is complete.', 'fixture:STREAM-42');
|
|
// Exceed both former completion deadlines: cold (5s) and reusable (30s).
|
|
await new Promise((resolve) => setTimeout(resolve, 31_000));
|
|
const itemId = `answer-${turnId}`;
|
|
const text = 'STREAM-42: The launch has two stages. Source: https://example.invalid/launch/STREAM-42';
|
|
send({ method: 'item/started', params: { threadId, turnId, item: { id: itemId, type: 'agentMessage', phase: 'final_answer', text: '' } } });
|
|
send({ method: 'item/agentMessage/delta', params: { threadId, turnId, itemId, delta: text.slice(0, 40) } });
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
send({ method: 'item/agentMessage/delta', params: { threadId, turnId, itemId, delta: text.slice(40) } });
|
|
send({ method: 'item/completed', params: { threadId, turnId, item: { id: itemId, type: 'agentMessage', phase: 'final_answer', text } } });
|
|
send({ method: 'turn/completed', params: { threadId, turn: { id: turnId, status: 'completed' } } });
|
|
return;
|
|
}
|
|
|
|
if (recoveryFixture) {
|
|
send({ method: 'item/agentMessage/delta', params: { threadId, turnId, itemId: `progress-${turnId}`, delta: 'Checking the current request and available connections.' } });
|
|
}
|
|
if (emitCeoLineage) {
|
|
const child = `child-${turnId}`;
|
|
send({ method: 'thread/started', params: { thread: { id: child, source: { subAgent: { thread_spawn: { parent_thread_id: threadId, depth: 1 } } } } } });
|
|
send({ method: 'turn/started', params: { threadId: child, turn: { id: `child-turn-${turnId}`, status: 'inProgress' } } });
|
|
send({ method: 'turn/completed', params: { threadId: child, turn: { id: `child-turn-${turnId}`, status: 'completed' } } });
|
|
await call('report_progress', { idempotencyKey: `lineage-${turnId}`, body: 'Provider child finished; root execution continues.' });
|
|
}
|
|
if (declined) {
|
|
const text = 'Connection declined. I will use the information already in this task and pursue alternatives.';
|
|
await call('report_progress', { idempotencyKey: `declined-${turnId}`, body: text });
|
|
await finish(text, 'task:declined-alternative');
|
|
send({ method: 'item/completed', params: { threadId, turnId, item: { id: `answer-${turnId}`, type: 'agentMessage', text } } });
|
|
send({ method: 'turn/completed', params: { threadId, turn: { id: turnId, status: 'completed' } } });
|
|
return;
|
|
}
|
|
const query = recoveryFixture && /gmail/i.test(currentObjective) ? 'gmail-fixture' : 'heliotrope';
|
|
const discovery = unwrap(await call('connections_search', { query }));
|
|
const service = discovery.results?.find((item) => item.source === 'configured');
|
|
if (!service) throw new Error('Authorized Research Archive fixture was not discoverable');
|
|
const request = unwrap(await call('connection_request', { service: service.service }));
|
|
const refreshingTools = request.state === 'ready' && request.instruction?.includes('fresh continuation with updated tools is queued');
|
|
let text;
|
|
if (request.state === 'needs_user_action') {
|
|
text = 'I need access to the research archive. I can organize the launch checklist while you connect it.';
|
|
} else if (refreshingTools) {
|
|
// Follow the authority's yield instruction when authorization arrives after
|
|
// this provider session pinned its tools. The next session must do the read.
|
|
text = 'Access is ready. I will continue the archive read with the updated tools.';
|
|
} else {
|
|
await mcp('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'in-feed-fixture', version: '1' } });
|
|
const list = await mcp('tools/list');
|
|
const tool = list.tools.find((item) => (query === 'gmail-fixture' ? /gmail-fixture/.test(item.description ?? '') : /heliotrope/.test(item.description ?? '')));
|
|
if (!tool) throw new Error('Updated native tool snapshot does not contain archive_read');
|
|
const result = await mcp('tools/call', { name: tool.name, arguments: {} });
|
|
if (result.isError) throw new Error(JSON.stringify(result));
|
|
text = result.content.filter((item) => item.type === 'text').map((item) => item.text).join('\n');
|
|
if (recoveryFixture && text.includes('RECOVERY-INJECT-')) {
|
|
// An unrelated informational event must not terminate the root. The next
|
|
// event deliberately supplies the terminal failure used by recovery tests.
|
|
send({ method: 'thread/status/changed', params: { threadId: 'unrelated-fixture-thread', status: { type: 'idle' } } });
|
|
if (text.includes('RECOVERY-INJECT-UNKNOWN-WRITE')) {
|
|
send({ method: 'item/started', params: { threadId, turnId, item: { id: 'uncertain-email-write', type: 'commandExecution', command: 'send_fixture_email', status: 'inProgress' } } });
|
|
}
|
|
send({ method: 'turn/completed', params: { threadId, turn: { id: turnId, status: 'failed', error: { code: 'fixture_checkpoint_unusable', message: 'Deterministic failed provider session after Gmail access' } } } });
|
|
return;
|
|
}
|
|
if (!text.includes(query === 'gmail-fixture' ? 'GMAIL-73' : 'HELIOTROPE-42')) throw new Error('Provider fixture value missing');
|
|
}
|
|
await call('report_progress', { idempotencyKey: `fixture-answer-${turnId}`, body: text });
|
|
if (request.state === 'ready' && !refreshingTools) await finish(text, 'mcp:archive_read');
|
|
send({ method: 'item/completed', params: { threadId, turnId, item: { id: `answer-${turnId}`, type: 'agentMessage', text } } });
|
|
send({ method: 'turn/completed', params: { threadId, turn: { id: turnId, status: 'completed' } } });
|
|
}
|
|
createInterface({ input: process.stdin }).on('line', (line) => {
|
|
const message = JSON.parse(line);
|
|
if (!message.method && pending.has(message.id)) {
|
|
const promise = pending.get(message.id); pending.delete(message.id);
|
|
message.error ? promise.reject(new Error(message.error.message)) : promise.resolve(message.result); return;
|
|
}
|
|
const { id, method } = message;
|
|
if (method === 'initialize') send({ id, result: { user: { sessionId: threadId } } });
|
|
else if (method === 'thread/start' || method === 'thread/resume') {
|
|
if (recoveryFixture) send({ method: 'configWarning', params: { message: 'Recovery fixture startup notice before the first turn' } });
|
|
if (method === 'thread/resume' && message.params?.threadId) threadId = message.params.threadId;
|
|
if (message.params?.completionContract) completionContract = message.params.completionContract;
|
|
send({ id, result: { model: 'in-feed-fixture', modelProvider: 'fixture', thread: { id: threadId, sessionId: threadId } } });
|
|
}
|
|
else if (method === 'thread/read') send({ id, result: { thread: { id: threadId, turns: [] } } });
|
|
else if (method === 'turn/start') {
|
|
emitCeoLineage = JSON.stringify(message.params).includes('CEO descendant fixture');
|
|
declined = /connection_intent/.test(JSON.stringify(message.params)) && /rejected/.test(JSON.stringify(message.params));
|
|
for (const part of message.params?.input ?? []) {
|
|
try {
|
|
const outer = JSON.parse(part.text);
|
|
const envelope = typeof outer.message === "string" ? JSON.parse(outer.message) : outer;
|
|
const contract = envelope.task?.completionContract ?? envelope.completionContract;
|
|
for (const line of (envelope.task?.task?.prompt ?? envelope.task?.prompt ?? '').split('\n')) {
|
|
try { const context = JSON.parse(line); if (context.version === 1 && Array.isArray(context.messages)) currentObjective = context.objective; } catch {}
|
|
}
|
|
if (contract?.revision && contract.criteria) completionContract = { revision: contract.revision, criterionIds: contract.criteria.map((criterion) => criterion.id) };
|
|
} catch { /* Non-envelope text is ordinary task context. */ }
|
|
}
|
|
turnId = randomUUID();
|
|
send({ id, result: { turn: { id: turnId, status: 'inProgress' } } });
|
|
send({ method: 'turn/started', params: { threadId, turn: { id: turnId, status: 'inProgress' } } });
|
|
// Deliver model output on a later tick, after the runner accepts turn/start.
|
|
setTimeout(() => void execute().catch((error) => {
|
|
process.stderr.write(`In-feed fixture: ${error.message}\n`);
|
|
send({ method: 'turn/completed', params: { threadId, turn: { id: turnId, status: 'failed', error: { message: error.message } } } });
|
|
}), 50);
|
|
} else if (id != null) send({ id, result: {} });
|
|
});
|