@probelabs/probe 0.6.0-rc331 → 0.6.0-rc332

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/bin/binaries/{probe-v0.6.0-rc331-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc332-aarch64-apple-darwin.tar.gz} +0 -0
  2. package/bin/binaries/{probe-v0.6.0-rc331-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc332-aarch64-unknown-linux-musl.tar.gz} +0 -0
  3. package/bin/binaries/{probe-v0.6.0-rc331-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc332-x86_64-apple-darwin.tar.gz} +0 -0
  4. package/bin/binaries/{probe-v0.6.0-rc331-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc332-x86_64-pc-windows-msvc.zip} +0 -0
  5. package/bin/binaries/{probe-v0.6.0-rc331-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc332-x86_64-unknown-linux-musl.tar.gz} +0 -0
  6. package/build/agent/ProbeAgent.d.ts +105 -4
  7. package/build/agent/ProbeAgent.js +209 -12
  8. package/build/agent/bashExecutor.js +36 -101
  9. package/build/agent/engines/codex.js +367 -88
  10. package/build/agent/engines/governed-answer-failure.js +152 -0
  11. package/build/agent/engines/governed-codex-profile.js +198 -0
  12. package/build/agent/governance/acknowledgedJsonlChannel.js +328 -0
  13. package/build/agent/governance/atomicTerminalReceipt.js +188 -0
  14. package/build/agent/governance/index.d.ts +130 -0
  15. package/build/agent/governance/index.js +8 -0
  16. package/build/agent/mcp/built-in-server.js +152 -53
  17. package/build/agent/mcp/index.d.ts +65 -0
  18. package/build/agent/mcp/index.js +6 -1
  19. package/build/agent/probeTool.js +1 -1
  20. package/build/agent/processSupervisor.js +351 -0
  21. package/build/agent/tools.js +14 -8
  22. package/build/index.js +2 -0
  23. package/cjs/agent/ProbeAgent.cjs +13238 -11968
  24. package/cjs/index.cjs +75955 -74126
  25. package/index.d.ts +149 -4
  26. package/package.json +6 -2
  27. package/src/agent/ProbeAgent.d.ts +105 -4
  28. package/src/agent/ProbeAgent.js +209 -12
  29. package/src/agent/bashExecutor.js +36 -101
  30. package/src/agent/engines/codex.js +367 -88
  31. package/src/agent/engines/governed-answer-failure.js +152 -0
  32. package/src/agent/engines/governed-codex-profile.js +198 -0
  33. package/src/agent/governance/acknowledgedJsonlChannel.js +328 -0
  34. package/src/agent/governance/atomicTerminalReceipt.js +188 -0
  35. package/src/agent/governance/index.d.ts +130 -0
  36. package/src/agent/governance/index.js +8 -0
  37. package/src/agent/mcp/built-in-server.js +152 -53
  38. package/src/agent/mcp/index.d.ts +65 -0
  39. package/src/agent/mcp/index.js +6 -1
  40. package/src/agent/probeTool.js +1 -1
  41. package/src/agent/processSupervisor.js +351 -0
  42. package/src/agent/tools.js +14 -8
  43. package/src/index.js +2 -0
@@ -4,16 +4,239 @@
4
4
  */
5
5
 
6
6
  import { spawn } from 'child_process';
7
- import { randomBytes } from 'crypto';
7
+ import { createHash, randomBytes } from 'crypto';
8
8
  import { createInterface } from 'readline';
9
9
  import { BuiltInMCPServer } from '../mcp/built-in-server.js';
10
+ import { governSpawnedProcess } from '../processSupervisor.js';
10
11
  import { Session } from '../shared/Session.js';
12
+ import { attestGovernedCodexSession, buildGovernedCodexInitialToolArgs, validateGovernedCodexProfile } from './governed-codex-profile.js';
13
+ import { governedAnswerFailure, normalizeGovernedAnswerFailure } from './governed-answer-failure.js';
14
+
15
+ const GOVERNED_NATIVE_EVENT_LIMIT = 256;
16
+ const GOVERNED_SAFE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
17
+ const GOVERNED_SAFE_KIND = /^[A-Za-z0-9._:-]{1,64}$/;
18
+ const GOVERNED_CODEX_NATIVE_CALLS = new Map([['exec', 'exec']]);
19
+ const GOVERNED_PROBE_MCP_CALLS = new Map([
20
+ ['mcp__probe__search', 'search'], ['mcp__probe__extract', 'extract'], ['mcp__probe__listFiles', 'listFiles'],
21
+ ]);
22
+
23
+ function governedRawItemInvalid() { throw governedAnswerFailure('native_event_grammar', 'raw_item_predicate'); }
24
+ function governedLiveEnvelopeInvalid(subreason, correlationOperand = null) {
25
+ throw governedAnswerFailure('native_event_grammar', 'live_envelope_session', subreason, correlationOperand);
26
+ }
27
+ function governedExactObject(value, keys, invalid = governedRawItemInvalid) {
28
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) invalid();
29
+ const proto = Object.getPrototypeOf(value);
30
+ if (proto !== Object.prototype && proto !== null) invalid();
31
+ const actual = Reflect.ownKeys(value).filter((key) => Object.prototype.propertyIsEnumerable.call(value, key));
32
+ if (actual.some((key) => typeof key !== 'string') || actual.length !== keys.length || keys.some((key) => !actual.includes(key))) invalid();
33
+ for (const key of keys) if (!Object.prototype.hasOwnProperty.call(Object.getOwnPropertyDescriptor(value, key), 'value')) invalid();
34
+ return value;
35
+ }
36
+ function governedSafeId(value) { if (typeof value !== 'string' || !GOVERNED_SAFE_ID.test(value)) governedRawItemInvalid(); }
37
+ function governedProbeMcpCallCount(evidence) {
38
+ const invalid = () => { throw new TypeError('Invalid attester input'); };
39
+ const snapshot = governedExactObject(evidence, ['admitted', 'closed', 'overflow'], invalid);
40
+ if (!Object.isFrozen(snapshot) || !Number.isInteger(snapshot.admitted) || snapshot.admitted < 0 ||
41
+ snapshot.admitted > GOVERNED_NATIVE_EVENT_LIMIT || !Number.isInteger(snapshot.closed) ||
42
+ snapshot.closed < 0 || snapshot.closed > GOVERNED_NATIVE_EVENT_LIMIT ||
43
+ typeof snapshot.overflow !== 'boolean' || snapshot.admitted !== snapshot.closed || snapshot.overflow) invalid();
44
+ return snapshot.closed;
45
+ }
46
+ function governedPassthrough(value, message = false) {
47
+ const keys = Object.keys(value ?? {}).sort().join(',');
48
+ const legacy = keys === 'turn_id';
49
+ const current = keys === (message ? 'content_item_kinds,create_time,turn_id' : 'create_time,turn_id');
50
+ if (!legacy && !current) governedRawItemInvalid();
51
+ governedSafeId(value.turn_id);
52
+ if (current) {
53
+ if (typeof value.create_time !== 'number' || !Number.isFinite(value.create_time) ||
54
+ value.create_time < 0 || value.create_time > Number.MAX_SAFE_INTEGER) governedRawItemInvalid();
55
+ if (message) {
56
+ if (!Array.isArray(value.content_item_kinds) || value.content_item_kinds.length > 16) governedRawItemInvalid();
57
+ for (const kind of value.content_item_kinds) if (typeof kind !== 'string' || !GOVERNED_SAFE_KIND.test(kind)) governedRawItemInvalid();
58
+ }
59
+ }
60
+ }
61
+ function validateGovernedRawMessage(item) {
62
+ const assistant = item.role === 'assistant';
63
+ const keys = assistant
64
+ ? ['type', 'id', 'role', 'content', 'phase', 'internal_chat_message_metadata_passthrough']
65
+ : Object.prototype.hasOwnProperty.call(item ?? {}, 'id')
66
+ ? ['type', 'id', 'role', 'content', 'internal_chat_message_metadata_passthrough']
67
+ : ['type', 'role', 'content', 'internal_chat_message_metadata_passthrough'];
68
+ governedExactObject(item, keys);
69
+ if (item.type !== 'message' || !['developer', 'user', 'assistant'].includes(item.role)) governedRawItemInvalid();
70
+ if (Object.prototype.hasOwnProperty.call(item, 'id')) governedSafeId(item.id);
71
+ if (assistant && !['commentary', 'final_answer'].includes(item.phase)) governedRawItemInvalid();
72
+ if (!Array.isArray(item.content) || item.content.length < 1 || item.content.length > 64) governedRawItemInvalid();
73
+ for (const part of item.content) {
74
+ governedExactObject(part, ['type', 'text']);
75
+ const allowed = assistant ? part.type === 'output_text' : part.type === 'input_text';
76
+ if (!allowed || typeof part.text !== 'string' || Buffer.byteLength(part.text, 'utf8') > 131072) governedRawItemInvalid();
77
+ }
78
+ governedPassthrough(item.internal_chat_message_metadata_passthrough, true);
79
+ }
80
+ function validateGovernedRawReasoning(item) {
81
+ governedExactObject(item, ['type', 'id', 'summary', 'encrypted_content', 'internal_chat_message_metadata_passthrough']);
82
+ if (item.type !== 'reasoning') governedRawItemInvalid(); governedSafeId(item.id);
83
+ if (!Array.isArray(item.summary) || item.summary.length !== 0 || typeof item.encrypted_content !== 'string' || Buffer.byteLength(item.encrypted_content, 'utf8') > 1048576) governedRawItemInvalid();
84
+ governedPassthrough(item.internal_chat_message_metadata_passthrough);
85
+ }
86
+ function createGovernedNativeCollector(profile) {
87
+ let sessionEvent = null, requestId = null, threadId = null, nativeCallCount = 0;
88
+ let relevantEventCount = 0, totalCallCount = 0, rawResponseItemCount = 0, assistantMessageCount = 0, finalAnswerCount = 0;
89
+ const rawIds = new Set(), callOrigins = new Map(), outputIds = new Set();
90
+ function observe(event) {
91
+ const type = event?.params?.msg?.type;
92
+ if (type === 'session_configured') {
93
+ if (sessionEvent) governedLiveEnvelopeInvalid('session_sequence');
94
+ sessionEvent = event;
95
+ requestId = event.params?._meta?.requestId;
96
+ threadId = event.params?._meta?.threadId;
97
+ return;
98
+ }
99
+ if (profile.version !== 'probe.governed-codex-profile/v2' || type !== 'raw_response_item') return;
100
+ if (!sessionEvent) governedLiveEnvelopeInvalid('session_sequence');
101
+ governedExactObject(event, ['jsonrpc', 'method', 'params'], () => governedLiveEnvelopeInvalid('envelope_shape'));
102
+ if (event.jsonrpc !== '2.0' || event.method !== 'codex/event') governedLiveEnvelopeInvalid('envelope_shape');
103
+ const params = governedExactObject(event.params, ['_meta', 'msg', 'id'], () => governedLiveEnvelopeInvalid('envelope_shape'));
104
+ const meta = governedExactObject(params._meta, ['requestId', 'threadId'], () => governedLiveEnvelopeInvalid('envelope_shape'));
105
+ if (meta.requestId !== requestId) governedLiveEnvelopeInvalid('correlation');
106
+ if (meta.threadId !== threadId) governedLiveEnvelopeInvalid('correlation', 'thread_id');
107
+ if (typeof params.id !== 'string') governedLiveEnvelopeInvalid('envelope_shape');
108
+ const msg = governedExactObject(params.msg, ['type', 'item'], () => governedLiveEnvelopeInvalid('envelope_shape'));
109
+ if (msg.type !== 'raw_response_item') governedLiveEnvelopeInvalid('envelope_shape');
110
+ const item = msg.item;
111
+ if (++rawResponseItemCount > GOVERNED_NATIVE_EVENT_LIMIT) governedRawItemInvalid();
112
+ if (item?.type === 'message') {
113
+ validateGovernedRawMessage(item);
114
+ if (Object.prototype.hasOwnProperty.call(item, 'id')) {
115
+ if (rawIds.has(item.id)) governedRawItemInvalid(); rawIds.add(item.id);
116
+ }
117
+ if (item.role === 'assistant') { assistantMessageCount++; if (item.phase === 'final_answer') finalAnswerCount++; }
118
+ return;
119
+ }
120
+ if (item?.type === 'reasoning') {
121
+ validateGovernedRawReasoning(item);
122
+ if (rawIds.has(item.id)) governedRawItemInvalid(); rawIds.add(item.id);
123
+ return;
124
+ }
125
+ if (item?.type === 'custom_tool_call') {
126
+ governedExactObject(item, ['type', 'id', 'status', 'call_id', 'name', 'input', 'internal_chat_message_metadata_passthrough']);
127
+ governedSafeId(item.id); governedSafeId(item.call_id);
128
+ if (rawIds.has(item.id) || callOrigins.has(item.call_id)) governedRawItemInvalid();
129
+ const nativeName = GOVERNED_CODEX_NATIVE_CALLS.get(item.name);
130
+ const probeMcpName = GOVERNED_PROBE_MCP_CALLS.get(item.name);
131
+ if ((nativeName !== undefined) === (probeMcpName !== undefined)) governedRawItemInvalid();
132
+ if (nativeName !== undefined && !profile.codexNativeTools.includes(nativeName)) governedRawItemInvalid();
133
+ if (probeMcpName !== undefined && !profile.probeMcpTools.includes(probeMcpName)) governedRawItemInvalid();
134
+ if (item.status !== 'completed' || typeof item.input !== 'string' || Buffer.byteLength(item.input, 'utf8') > 131072) governedRawItemInvalid();
135
+ governedPassthrough(item.internal_chat_message_metadata_passthrough);
136
+ if (++relevantEventCount > GOVERNED_NATIVE_EVENT_LIMIT || ++totalCallCount > GOVERNED_NATIVE_EVENT_LIMIT) governedRawItemInvalid();
137
+ const origin = nativeName !== undefined ? 'codex-native' : 'probe-mcp';
138
+ rawIds.add(item.id); callOrigins.set(item.call_id, origin);
139
+ if (origin === 'codex-native') nativeCallCount++;
140
+ return;
141
+ }
142
+ if (item?.type === 'custom_tool_call_output') {
143
+ const hasId = Object.prototype.hasOwnProperty.call(item, 'id');
144
+ governedExactObject(item, hasId
145
+ ? ['type', 'id', 'call_id', 'output', 'internal_chat_message_metadata_passthrough']
146
+ : ['type', 'call_id', 'output', 'internal_chat_message_metadata_passthrough']);
147
+ if (hasId) { governedSafeId(item.id); if (rawIds.has(item.id)) governedRawItemInvalid(); }
148
+ governedSafeId(item.call_id);
149
+ if (!callOrigins.has(item.call_id) || outputIds.has(item.call_id) || !Array.isArray(item.output) || item.output.length > 64) governedRawItemInvalid();
150
+ for (const part of item.output) {
151
+ governedExactObject(part, ['type', 'text']);
152
+ if (part.type !== 'input_text' || typeof part.text !== 'string' || Buffer.byteLength(part.text, 'utf8') > 1048576) governedRawItemInvalid();
153
+ }
154
+ governedPassthrough(item.internal_chat_message_metadata_passthrough);
155
+ if (++relevantEventCount > GOVERNED_NATIVE_EVENT_LIMIT) governedRawItemInvalid();
156
+ if (hasId) rawIds.add(item.id);
157
+ outputIds.add(item.call_id);
158
+ return;
159
+ }
160
+ governedRawItemInvalid();
161
+ }
162
+ function evidence() {
163
+ if (!sessionEvent) governedLiveEnvelopeInvalid('session_sequence');
164
+ if (assistantMessageCount > 0 && finalAnswerCount !== 1) governedRawItemInvalid();
165
+ const tools = nativeCallCount === 0 ? [] : [{ name: 'exec', status: 'completed', count: nativeCallCount }];
166
+ return { sessionEvent, capabilities: { nativeTools: { total: nativeCallCount, tools } } };
167
+ }
168
+ return { observe, evidence };
169
+ }
170
+
171
+ function externalReceipt(attestation, capabilityCounts) {
172
+ if (attestation.version === 'probe.governed-codex-attestation/v3') return externalV3Receipt(attestation, {}, capabilityCounts);
173
+ return {
174
+ version: attestation.version, profileId: attestation.profileId,
175
+ requested: { profileDigest: attestation.requested.profileDigest, cwdDigest: attestation.requested.cwdDigest, probeToolsDigest: attestation.requested.probeToolsDigest, model: attestation.requested.model, reasoningEffort: attestation.requested.reasoningEffort, sandbox: attestation.requested.sandbox, approvalPolicy: attestation.requested.approvalPolicy },
176
+ observed: { source: attestation.observed.source, model: attestation.observed.model, modelProviderId: attestation.observed.modelProviderId, reasoningEffort: attestation.observed.reasoningEffort, approvalPolicy: attestation.observed.approvalPolicy, cwdDigest: attestation.observed.cwdDigest, permissionProfileDigest: attestation.observed.permissionProfileDigest, filesystem: attestation.observed.filesystem, network: attestation.observed.network },
177
+ evidence: { eventCount: 1 }, usage: { status: 'unavailable' },
178
+ };
179
+ }
180
+
181
+ function externalV3Receipt(attestation, extra, capabilityCounts) {
182
+ return {
183
+ version: attestation.version, profileId: attestation.profileId,
184
+ requested: { profileDigest: attestation.requested.profileDigest, cwdDigest: attestation.requested.cwdDigest,
185
+ probeMcpToolsDigest: attestation.requested.probeMcpToolsDigest, codexNativeToolsDigest: attestation.requested.codexNativeToolsDigest,
186
+ probeMcpTools: [...attestation.requested.probeMcpTools], codexNativeTools: [...attestation.requested.codexNativeTools],
187
+ model: attestation.requested.model, reasoningEffort: attestation.requested.reasoningEffort,
188
+ sandbox: attestation.requested.sandbox, approvalPolicy: attestation.requested.approvalPolicy },
189
+ observed: { source: attestation.observed.source, model: attestation.observed.model,
190
+ modelProviderId: attestation.observed.modelProviderId, reasoningEffort: attestation.observed.reasoningEffort,
191
+ approvalPolicy: attestation.observed.approvalPolicy, cwdDigest: attestation.observed.cwdDigest,
192
+ permissionProfileDigest: attestation.observed.permissionProfileDigest, filesystem: attestation.observed.filesystem,
193
+ network: attestation.observed.network, nativeTools: { total: attestation.observed.nativeTools.total,
194
+ tools: attestation.observed.nativeTools.tools.map((item) => ({ ...item })) } },
195
+ ...extra, evidence: { sessionEventCount: 1, nativeCallCount: capabilityCounts.nativeCallCount,
196
+ probeMcpCallCount: capabilityCounts.probeMcpCallCount }, usage: { status: 'unavailable' },
197
+ };
198
+ }
199
+
200
+ export function governedCodexDispatch(prompt) {
201
+ const promptBytes = Buffer.byteLength(prompt, 'utf8');
202
+ const byteLength = Buffer.alloc(8);
203
+ byteLength.writeBigUInt64BE(BigInt(promptBytes));
204
+ const promptDigest = `sha256:${createHash('sha256')
205
+ .update('probe.governed-codex-dispatch/prompt/v1', 'utf8')
206
+ .update(Buffer.from([0])).update(byteLength).update(prompt, 'utf8').digest('hex')}`;
207
+ return Object.freeze({ source: 'probe-host-tools-call', tool: 'codex', promptDigest, promptBytes });
208
+ }
209
+
210
+ export function composeCodexInitialPrompt({ systemPrompt, customPrompt, prompt }) {
211
+ const fullPrompt = combinePrompts(systemPrompt, customPrompt);
212
+ return fullPrompt ? `${fullPrompt}\n\n${prompt}` : prompt;
213
+ }
214
+
215
+ export function previewGovernedCodexInitialDispatch(input) {
216
+ return governedCodexDispatch(composeCodexInitialPrompt(input));
217
+ }
218
+
219
+ function externalBoundReceipt(internal, dispatch, invocationDigest, capabilityCounts) {
220
+ if (internal.version === 'probe.governed-codex-attestation/v3') return externalV3Receipt(internal, {
221
+ executionContext: { source: 'caller', invocationDigest },
222
+ dispatch: { source: dispatch.source, tool: dispatch.tool, promptDigest: dispatch.promptDigest, promptBytes: dispatch.promptBytes },
223
+ }, capabilityCounts);
224
+ return {
225
+ version: 'probe.governed-codex-attestation/v2', profileId: 'luna-xhigh-readonly-v1',
226
+ requested: { profileDigest: internal.requested.profileDigest, cwdDigest: internal.requested.cwdDigest, probeToolsDigest: internal.requested.probeToolsDigest, model: internal.requested.model, reasoningEffort: internal.requested.reasoningEffort, sandbox: internal.requested.sandbox, approvalPolicy: internal.requested.approvalPolicy },
227
+ observed: { source: internal.observed.source, model: internal.observed.model, modelProviderId: internal.observed.modelProviderId, reasoningEffort: internal.observed.reasoningEffort, approvalPolicy: internal.observed.approvalPolicy, cwdDigest: internal.observed.cwdDigest, permissionProfileDigest: internal.observed.permissionProfileDigest, filesystem: internal.observed.filesystem, network: internal.observed.network },
228
+ executionContext: { source: 'caller', invocationDigest },
229
+ dispatch: { source: dispatch.source, tool: dispatch.tool, promptDigest: dispatch.promptDigest, promptBytes: dispatch.promptBytes },
230
+ evidence: { eventCount: 1 }, usage: { status: 'unavailable' },
231
+ };
232
+ }
11
233
 
12
234
  /**
13
235
  * Codex Engine using MCP Server with event streaming
14
236
  */
15
237
  export async function createCodexEngine(options = {}) {
16
238
  const { agent, systemPrompt, customPrompt, debug, sessionId, allowedTools, model } = options;
239
+ const governedProfile = options.governedCodexProfile === undefined ? null : validateGovernedCodexProfile(options.governedCodexProfile);
17
240
 
18
241
  const session = new Session(
19
242
  sessionId || randomBytes(8).toString('hex'),
@@ -29,7 +252,9 @@ export async function createCodexEngine(options = {}) {
29
252
  mcpServer = new BuiltInMCPServer(agent, {
30
253
  port: 0,
31
254
  host: '127.0.0.1',
32
- debug: debug
255
+ debug: debug,
256
+ ...(governedProfile?.version === 'probe.governed-codex-profile/v2'
257
+ ? { governedProfileVersion: governedProfile.version } : {})
33
258
  });
34
259
 
35
260
  const { host, port } = await mcpServer.start();
@@ -48,13 +273,21 @@ export async function createCodexEngine(options = {}) {
48
273
  }
49
274
 
50
275
  const codexProcess = spawn('codex', ['mcp-server'], {
51
- stdio: ['pipe', 'pipe', 'pipe']
276
+ stdio: ['pipe', 'pipe', 'pipe'],
277
+ detached: true
278
+ });
279
+ const governedProcess = governSpawnedProcess(codexProcess, {
280
+ captureStdout: false,
281
+ signalScope: 'process-group',
282
+ stdoutByteCap: 0
52
283
  });
53
284
 
54
285
  // Setup JSON-RPC communication
55
286
  let requestId = 0;
56
287
  const pendingRequests = new Map();
57
288
  const eventHandlers = new Map();
289
+ const governedEvidenceHandlers = new Map();
290
+ let governedQueryStarted = false, closePromise = null;
58
291
 
59
292
  // Read stdout line by line
60
293
  const stdoutReader = createInterface({
@@ -74,8 +307,9 @@ export async function createCodexEngine(options = {}) {
74
307
 
75
308
  // Handle responses to our requests
76
309
  if (message.id !== undefined && pendingRequests.has(message.id)) {
77
- const { resolve, reject } = pendingRequests.get(message.id);
310
+ const { resolve, reject, timer } = pendingRequests.get(message.id);
78
311
  pendingRequests.delete(message.id);
312
+ clearTimeout(timer);
79
313
 
80
314
  if (message.error) {
81
315
  reject(new Error(message.error.message || JSON.stringify(message.error)));
@@ -87,6 +321,9 @@ export async function createCodexEngine(options = {}) {
87
321
  // Handle notifications (codex/event)
88
322
  if (message.method === 'codex/event' && message.params) {
89
323
  const requestId = message.params._meta?.requestId;
324
+ if (requestId !== undefined && governedEvidenceHandlers.has(requestId)) {
325
+ governedEvidenceHandlers.get(requestId)(message);
326
+ }
90
327
  if (requestId !== undefined && eventHandlers.has(requestId)) {
91
328
  eventHandlers.get(requestId)(message.params);
92
329
  }
@@ -98,6 +335,8 @@ export async function createCodexEngine(options = {}) {
98
335
  }
99
336
  });
100
337
 
338
+ codexProcess.once('error', (error) => rejectPending(error)); codexProcess.once('close', () => rejectPending(new Error('Codex process closed')));
339
+
101
340
  // Handle stderr
102
341
  if (debug) {
103
342
  codexProcess.stderr.on('data', (data) => {
@@ -108,6 +347,7 @@ export async function createCodexEngine(options = {}) {
108
347
  // Send JSON-RPC request
109
348
  function sendRequest(method, params = {}) {
110
349
  return new Promise((resolve, reject) => {
350
+ if (closePromise) return reject(new Error('Codex engine is closed'));
111
351
  const id = ++requestId;
112
352
  const request = {
113
353
  jsonrpc: '2.0',
@@ -116,37 +356,48 @@ export async function createCodexEngine(options = {}) {
116
356
  params
117
357
  };
118
358
 
119
- pendingRequests.set(id, { resolve, reject });
120
-
121
359
  // Timeout after 10 minutes
122
- setTimeout(() => {
360
+ const timer = setTimeout(() => {
123
361
  if (pendingRequests.has(id)) {
124
362
  pendingRequests.delete(id);
125
363
  reject(new Error(`Request ${method} timed out after 10 minutes`));
126
364
  }
127
365
  }, 600000);
366
+ pendingRequests.set(id, { resolve, reject, timer });
128
367
 
129
368
  codexProcess.stdin.write(JSON.stringify(request) + '\n');
130
369
  });
131
370
  }
132
371
 
372
+ function rejectPending(error) { for (const { reject, timer } of pendingRequests.values()) { clearTimeout(timer); reject(error); } pendingRequests.clear(); }
373
+
374
+ async function cleanup(reason = new Error('Codex engine closed')) {
375
+ if (closePromise) return closePromise;
376
+ closePromise = (async () => {
377
+ rejectPending(reason); eventHandlers.clear(); governedEvidenceHandlers.clear();
378
+ stdoutReader.close(); codexProcess.stdin.destroy();
379
+ const receipt = await governedProcess.terminate('codex_engine_closed');
380
+ const processCleanupFailed = receipt.classification === 'cleanup_timeout' ||
381
+ !receipt.barriers.close || !receipt.barriers.stdoutEOF || !receipt.barriers.stderrEOF;
382
+ if (mcpServer) await mcpServer.stop();
383
+ if (processCleanupFailed) throw new Error('Codex process cleanup failed');
384
+ })();
385
+ return closePromise;
386
+ }
387
+
133
388
  // Initialize MCP connection
134
- await sendRequest('initialize', {
135
- protocolVersion: '2024-11-05',
136
- capabilities: { tools: {} },
137
- clientInfo: {
138
- name: 'probe-codex-client',
139
- version: '1.0.0'
140
- }
141
- });
389
+ try {
390
+ await sendRequest('initialize', {
391
+ protocolVersion: '2024-11-05', capabilities: { tools: {} },
392
+ clientInfo: { name: 'probe-codex-client', version: '1.0.0' }
393
+ });
394
+ } catch (error) { await cleanup(error); throw error; }
142
395
 
143
396
  if (debug) {
144
397
  console.log('[DEBUG] Connected to Codex MCP server');
145
398
  console.log('[DEBUG] Session:', session.id);
146
399
  }
147
400
 
148
- const fullPrompt = combinePrompts(systemPrompt, customPrompt, agent);
149
-
150
401
  return {
151
402
  sessionId: session.id,
152
403
  session,
@@ -157,15 +408,24 @@ export async function createCodexEngine(options = {}) {
157
408
  async *query(prompt, opts = {}) {
158
409
  // Build prompt
159
410
  let finalPrompt = prompt;
160
- if (!session.conversationId && fullPrompt) {
161
- finalPrompt = `${fullPrompt}\n\n${prompt}`;
411
+ if (!session.conversationId) {
412
+ finalPrompt = composeCodexInitialPrompt({ systemPrompt, customPrompt, prompt });
162
413
  }
163
414
 
164
415
  const isFollowUp = session.conversationId !== null;
165
416
  const toolName = isFollowUp ? 'codex-reply' : 'codex';
417
+ let abortHandler, queryError = null;
166
418
 
167
- // Build arguments
168
- const toolArgs = { prompt: finalPrompt };
419
+ try {
420
+ const hasInvocationDigest = Object.prototype.hasOwnProperty.call(opts,
421
+ 'invocationDigest');
422
+ const invocationDigest = hasInvocationDigest ? opts.invocationDigest : undefined;
423
+ if (hasInvocationDigest && (typeof invocationDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(invocationDigest))) {
424
+ throw new TypeError('answerGoverned invocationDigest must match sha256:<64 lowercase hexadecimal digits>');
425
+ }
426
+
427
+ // Build arguments
428
+ let toolArgs = { prompt: finalPrompt };
169
429
 
170
430
  if (isFollowUp) {
171
431
  toolArgs.conversationId = session.conversationId;
@@ -173,10 +433,14 @@ export async function createCodexEngine(options = {}) {
173
433
  console.log(`[DEBUG] Follow-up with conversationId: ${session.conversationId}`);
174
434
  }
175
435
  } else {
176
- if (model) {
436
+ if (governedProfile) {
437
+ if (governedQueryStarted) throw new Error('Governed Codex engine permits one initial query');
438
+ governedQueryStarted = true;
439
+ toolArgs = buildGovernedCodexInitialToolArgs({ profile: governedProfile, prompt: finalPrompt, mcp: { name: mcpServerName, url: mcpServerUrl } });
440
+ } else if (model) {
177
441
  toolArgs.model = model;
178
442
  }
179
- if (mcpServerUrl && mcpServerName) {
443
+ if (!governedProfile && mcpServerUrl && mcpServerName) {
180
444
  toolArgs.config = {
181
445
  mcp_servers: {
182
446
  [mcpServerName]: { url: mcpServerUrl }
@@ -188,53 +452,91 @@ export async function createCodexEngine(options = {}) {
188
452
  }
189
453
  }
190
454
 
191
- try {
192
455
  const reqId = requestId + 1;
193
456
  let fullResponse = '';
194
457
  let gotSessionId = false;
458
+ const collector = governedProfile ? createGovernedNativeCollector(governedProfile) : null;
459
+ let evidenceFailure = null;
460
+ if (governedProfile) governedEvidenceHandlers.set(reqId, (event) => {
461
+ try { collector.observe(event); } catch (error) { evidenceFailure ??= normalizeGovernedAnswerFailure(error, 'native_event_grammar'); }
462
+ });
463
+ if (opts.abortSignal) {
464
+ if (opts.abortSignal.aborted) throw new Error('Codex query cancelled');
465
+ abortHandler = () => { void cleanup(new Error('Codex query cancelled')).catch(() => {}); };
466
+ opts.abortSignal.addEventListener('abort', abortHandler, { once: true });
467
+ }
195
468
 
196
469
  // Register event handler for this request
197
- const eventPromise = new Promise((resolve) => {
198
- eventHandlers.set(reqId, (eventParams) => {
199
- const msg = eventParams.msg;
200
-
201
- // Extract session_id from session_configured event
202
- if (msg.type === 'session_configured' && msg.session_id && !gotSessionId) {
203
- session.setConversationId(msg.session_id);
204
- gotSessionId = true;
205
- }
470
+ eventHandlers.set(reqId, (eventParams) => {
471
+ const msg = eventParams.msg;
206
472
 
207
- // Collect agent messages
208
- if (msg.type === 'raw_response_item' && msg.item?.role === 'assistant') {
209
- const content = msg.item.content;
210
- if (Array.isArray(content)) {
211
- for (const part of content) {
212
- if (part.type === 'text' && part.text) {
213
- fullResponse += part.text;
214
- }
473
+ // Extract session_id from session_configured event
474
+ if (!governedProfile && msg.type === 'session_configured' && msg.session_id && !gotSessionId) {
475
+ session.setConversationId(msg.session_id);
476
+ gotSessionId = true;
477
+ }
478
+
479
+ // Collect agent messages
480
+ if (msg.type === 'raw_response_item' && msg.item?.role === 'assistant') {
481
+ const content = msg.item.content;
482
+ if (Array.isArray(content)) {
483
+ for (const part of content) {
484
+ if (part.type === 'text' && part.text) {
485
+ fullResponse += part.text;
215
486
  }
216
487
  }
217
488
  }
218
- });
219
-
220
- // Mark as resolved when we're done
221
- setTimeout(() => {
222
- eventHandlers.delete(reqId);
223
- resolve();
224
- }, 600000); // 10 min timeout
489
+ }
225
490
  });
226
491
 
227
492
  // Call the tool
493
+ const dispatch = governedProfile ? governedCodexDispatch(toolArgs.prompt) : null;
228
494
  const resultPromise = sendRequest('tools/call', {
229
495
  name: toolName,
230
496
  arguments: toolArgs
231
497
  });
232
498
 
233
499
  // Wait for result
234
- const result = await resultPromise;
500
+ let result;
501
+ try { result = await resultPromise; }
502
+ catch (error) {
503
+ throw governedProfile
504
+ ? governedAnswerFailure(evidenceFailure ? 'unknown' : 'provider_engine')
505
+ : error;
506
+ }
235
507
 
236
508
  // Clean up event handler
237
509
  eventHandlers.delete(reqId);
510
+ governedEvidenceHandlers.delete(reqId);
511
+ let attestation = null;
512
+ if (governedProfile) {
513
+ if (evidenceFailure) throw evidenceFailure;
514
+ let collected, internal, probeMcpCallCount;
515
+ try {
516
+ collected = collector.evidence();
517
+ } catch (error) {
518
+ throw normalizeGovernedAnswerFailure(error, 'native_event_grammar', 'live_envelope_session');
519
+ }
520
+ try {
521
+ internal = attestGovernedCodexSession({ profile: governedProfile,
522
+ events: governedProfile.version === 'probe.governed-codex-profile/v2'
523
+ ? [collected.sessionEvent, collected.capabilities.nativeTools] : [collected.sessionEvent] });
524
+ probeMcpCallCount = governedProfile.version === 'probe.governed-codex-profile/v2'
525
+ ? governedProbeMcpCallCount(mcpServer?.getGovernedCallEvidence()) : 0;
526
+ } catch (error) {
527
+ throw normalizeGovernedAnswerFailure(error, 'native_event_grammar', 'live_envelope_session', 'attestation');
528
+ }
529
+ attestation = hasInvocationDigest
530
+ ? externalBoundReceipt(internal, dispatch, invocationDigest, {
531
+ nativeCallCount: collected.capabilities.nativeTools.total,
532
+ probeMcpCallCount,
533
+ })
534
+ : externalReceipt(internal, {
535
+ nativeCallCount: collected.capabilities.nativeTools.total,
536
+ probeMcpCallCount,
537
+ });
538
+ session.setConversationId(collected.sessionEvent.params.msg.session_id);
539
+ }
238
540
 
239
541
  // Parse result
240
542
  if (result && result.content && Array.isArray(result.content)) {
@@ -257,11 +559,16 @@ export async function createCodexEngine(options = {}) {
257
559
  };
258
560
  }
259
561
 
562
+ if (governedProfile?.version === 'probe.governed-codex-profile/v2') {
563
+ yield { type: 'toolBatch', total: attestation.observed.nativeTools.total,
564
+ tools: attestation.observed.nativeTools.tools.map((item) => ({ ...item })) };
565
+ }
566
+
260
567
  session.incrementMessageCount();
261
568
 
262
569
  yield {
263
570
  type: 'metadata',
264
- data: {
571
+ data: governedProfile ? { attestation } : {
265
572
  sessionId: session.id,
266
573
  conversationId: session.conversationId,
267
574
  messageCount: session.messageCount
@@ -269,6 +576,8 @@ export async function createCodexEngine(options = {}) {
269
576
  };
270
577
 
271
578
  } catch (error) {
579
+ if (governedProfile) error = normalizeGovernedAnswerFailure(error, 'unknown');
580
+ queryError = error;
272
581
  if (debug) {
273
582
  console.error('[DEBUG] Codex query error:', error);
274
583
  }
@@ -276,6 +585,12 @@ export async function createCodexEngine(options = {}) {
276
585
  type: 'error',
277
586
  error: error
278
587
  };
588
+ } finally {
589
+ if (opts.abortSignal && abortHandler) opts.abortSignal.removeEventListener('abort', abortHandler);
590
+ if (governedProfile) {
591
+ try { await cleanup(); }
592
+ catch (cleanupError) { if (!queryError) throw cleanupError; }
593
+ }
279
594
  }
280
595
  },
281
596
 
@@ -290,43 +605,7 @@ export async function createCodexEngine(options = {}) {
290
605
  * Clean up resources
291
606
  */
292
607
  async close() {
293
- try {
294
- // Close readline interface first to remove event listeners
295
- if (stdoutReader) {
296
- stdoutReader.close();
297
- if (debug) {
298
- console.log('[DEBUG] Closed stdout reader');
299
- }
300
- }
301
-
302
- // Clear all pending requests and event handlers
303
- pendingRequests.clear();
304
- eventHandlers.clear();
305
-
306
- // Kill Codex process
307
- if (codexProcess && !codexProcess.killed) {
308
- codexProcess.kill();
309
- if (debug) {
310
- console.log('[DEBUG] Killed Codex MCP server process');
311
- }
312
- }
313
-
314
- // Stop Probe MCP server
315
- if (mcpServer) {
316
- await mcpServer.stop();
317
- if (debug) {
318
- console.log('[DEBUG] Stopped Probe MCP server');
319
- }
320
- }
321
-
322
- if (debug) {
323
- console.log('[DEBUG] Engine closed, session:', session.id);
324
- }
325
- } catch (error) {
326
- if (debug) {
327
- console.error('[DEBUG] Error during cleanup:', error.message);
328
- }
329
- }
608
+ await cleanup();
330
609
  }
331
610
  };
332
611
  }
@@ -334,7 +613,7 @@ export async function createCodexEngine(options = {}) {
334
613
  /**
335
614
  * Combine prompts intelligently
336
615
  */
337
- function combinePrompts(systemPrompt, customPrompt, agent) {
616
+ function combinePrompts(systemPrompt, customPrompt) {
338
617
  if (!systemPrompt && customPrompt) {
339
618
  return customPrompt;
340
619
  }