@probelabs/probe 0.6.0-rc331 → 0.6.0-rc334
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.
- package/bin/binaries/{probe-v0.6.0-rc331-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc334-aarch64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc334-aarch64-unknown-linux-musl.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc334-x86_64-apple-darwin.tar.gz} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc334-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/{probe-v0.6.0-rc331-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc334-x86_64-unknown-linux-musl.tar.gz} +0 -0
- package/build/agent/ProbeAgent.d.ts +105 -4
- package/build/agent/ProbeAgent.js +209 -12
- package/build/agent/bashExecutor.js +36 -101
- package/build/agent/engines/codex.js +367 -88
- package/build/agent/engines/governed-answer-failure.js +152 -0
- package/build/agent/engines/governed-codex-profile.js +198 -0
- package/build/agent/governance/acknowledgedJsonlChannel.js +328 -0
- package/build/agent/governance/atomicTerminalReceipt.js +188 -0
- package/build/agent/governance/index.d.ts +130 -0
- package/build/agent/governance/index.js +8 -0
- package/build/agent/mcp/built-in-server.js +152 -53
- package/build/agent/mcp/index.d.ts +65 -0
- package/build/agent/mcp/index.js +6 -1
- package/build/agent/probeTool.js +1 -1
- package/build/agent/processSupervisor.js +351 -0
- package/build/agent/tools.js +14 -8
- package/build/index.js +2 -0
- package/build/utils/provider.js +9 -3
- package/cjs/agent/ProbeAgent.cjs +13463 -12187
- package/cjs/index.cjs +75974 -74139
- package/index.d.ts +149 -4
- package/package.json +6 -2
- package/src/agent/ProbeAgent.d.ts +105 -4
- package/src/agent/ProbeAgent.js +209 -12
- package/src/agent/bashExecutor.js +36 -101
- package/src/agent/engines/codex.js +367 -88
- package/src/agent/engines/governed-answer-failure.js +152 -0
- package/src/agent/engines/governed-codex-profile.js +198 -0
- package/src/agent/governance/acknowledgedJsonlChannel.js +328 -0
- package/src/agent/governance/atomicTerminalReceipt.js +188 -0
- package/src/agent/governance/index.d.ts +130 -0
- package/src/agent/governance/index.js +8 -0
- package/src/agent/mcp/built-in-server.js +152 -53
- package/src/agent/mcp/index.d.ts +65 -0
- package/src/agent/mcp/index.js +6 -1
- package/src/agent/probeTool.js +1 -1
- package/src/agent/processSupervisor.js +351 -0
- package/src/agent/tools.js +14 -8
- package/src/index.js +2 -0
- package/src/utils/provider.js +9 -3
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
// Core ProbeAgent class adapted from examples/chat/probeChat.js
|
|
2
2
|
|
|
3
|
-
// Load .env file if present (silent fail if not found)
|
|
4
|
-
import dotenv from 'dotenv';
|
|
5
|
-
dotenv.config();
|
|
6
|
-
|
|
7
3
|
// ============================================================================
|
|
8
4
|
// Timeout Configuration Constants
|
|
9
5
|
// ============================================================================
|
|
@@ -29,7 +25,7 @@ export const ENGINE_ACTIVITY_TIMEOUT_MAX = 600000;
|
|
|
29
25
|
|
|
30
26
|
import { createProviderInstance, DEFAULT_MODELS } from '../utils/provider.js';
|
|
31
27
|
import { streamText, generateText, tool, stepCountIs, jsonSchema, Output } from 'ai';
|
|
32
|
-
import { randomUUID } from 'crypto';
|
|
28
|
+
import { createHash, randomUUID } from 'crypto';
|
|
33
29
|
import { EventEmitter } from 'events';
|
|
34
30
|
import { existsSync } from 'fs';
|
|
35
31
|
import { readFile, stat, readdir } from 'fs/promises';
|
|
@@ -70,7 +66,7 @@ import {
|
|
|
70
66
|
clearToolExecutionData
|
|
71
67
|
} from './probeTool.js';
|
|
72
68
|
import { createMockProvider } from './mockProvider.js';
|
|
73
|
-
import { listFilesByLevel } from '../
|
|
69
|
+
import { listFilesByLevel } from '../utils/file-lister.js';
|
|
74
70
|
import {
|
|
75
71
|
cleanSchemaResponse,
|
|
76
72
|
isJsonSchema,
|
|
@@ -106,6 +102,69 @@ import {
|
|
|
106
102
|
createTaskCompletionBlockedMessage
|
|
107
103
|
} from './tasks/index.js';
|
|
108
104
|
import { z } from 'zod';
|
|
105
|
+
import { validateGovernedCodexProfile } from './engines/governed-codex-profile.js';
|
|
106
|
+
import { governedAnswerFailure, normalizeGovernedAnswerFailure } from './engines/governed-answer-failure.js';
|
|
107
|
+
|
|
108
|
+
const GOVERNED_RESULT_IDENTITY = 'probe.governed-result-identity/v1';
|
|
109
|
+
const GOVERNED_RESULT_DOMAIN = 'probe.governed-result-identity/data/v1';
|
|
110
|
+
|
|
111
|
+
function normalizeGovernedJson(value) {
|
|
112
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;
|
|
113
|
+
if (typeof value === 'number') {
|
|
114
|
+
if (!Number.isFinite(value)) throw new TypeError('answerGoverned validated result is not canonical JSON');
|
|
115
|
+
return Object.is(value, -0) ? 0 : value;
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(value)) return value.map(normalizeGovernedJson);
|
|
118
|
+
if (typeof value === 'object' && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
119
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, child]) => [key, normalizeGovernedJson(child)]));
|
|
120
|
+
}
|
|
121
|
+
throw new TypeError('answerGoverned validated result is not canonical JSON');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function freezeGovernedTree(value) {
|
|
125
|
+
if (value && typeof value === 'object') {
|
|
126
|
+
for (const child of Object.values(value)) freezeGovernedTree(child);
|
|
127
|
+
Object.freeze(value);
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function identifyGovernedResult(value) {
|
|
133
|
+
const data = freezeGovernedTree(normalizeGovernedJson(value));
|
|
134
|
+
const canonical = Buffer.from(JSON.stringify(data), 'utf8');
|
|
135
|
+
const byteLength = Buffer.alloc(8);
|
|
136
|
+
byteLength.writeBigUInt64BE(BigInt(canonical.length));
|
|
137
|
+
const resultDigest = `sha256:${createHash('sha256').update(GOVERNED_RESULT_DOMAIN, 'utf8').update(Buffer.from([0])).update(byteLength).update(canonical).digest('hex')}`;
|
|
138
|
+
const resultIdentity = Object.freeze({ version: GOVERNED_RESULT_IDENTITY, source: 'probe-host-schema-valid-json', resultDigest, canonicalBytes: canonical.length });
|
|
139
|
+
return { data, resultIdentity };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function governedSchemaResultValidationFailure(validation) {
|
|
143
|
+
const subreason = validation?.error === 'Invalid schema provided' ||
|
|
144
|
+
validation?.error === 'Schema compilation failed' ? 'schema_definition'
|
|
145
|
+
: validation?.error === 'Schema validation failed' ? 'schema_mismatch'
|
|
146
|
+
: 'response_json';
|
|
147
|
+
const schemaResultValidationKeyword = subreason === 'schema_mismatch'
|
|
148
|
+
? classifyGovernedSchemaResultValidationKeyword(validation) : null;
|
|
149
|
+
return governedAnswerFailure('schema_result_validation', null, null, null, null, subreason,
|
|
150
|
+
schemaResultValidationKeyword);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const GOVERNED_SCHEMA_RESULT_VALIDATION_KEYWORDS = new Set([
|
|
154
|
+
'required', 'additionalProperties', 'type', 'pattern', 'enum', 'minItems', 'maxItems',
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
function classifyGovernedSchemaResultValidationKeyword(validation) {
|
|
158
|
+
const errors = validation?.schemaErrors;
|
|
159
|
+
if (!Array.isArray(errors) || errors.length === 0) return 'unknown';
|
|
160
|
+
const recognized = new Set();
|
|
161
|
+
for (const error of errors) {
|
|
162
|
+
const keyword = error?.keyword;
|
|
163
|
+
if (!GOVERNED_SCHEMA_RESULT_VALIDATION_KEYWORDS.has(keyword)) return 'unknown';
|
|
164
|
+
recognized.add(keyword);
|
|
165
|
+
}
|
|
166
|
+
return recognized.size === 1 ? [...recognized][0] : 'multiple';
|
|
167
|
+
}
|
|
109
168
|
|
|
110
169
|
// Maximum tool iterations to prevent infinite loops - configurable via MAX_TOOL_ITERATIONS env var
|
|
111
170
|
const MAX_TOOL_ITERATIONS = (() => {
|
|
@@ -287,6 +346,7 @@ export class ProbeAgent {
|
|
|
287
346
|
this.debug = options.debug || process.env.DEBUG === '1';
|
|
288
347
|
this.cancelled = false;
|
|
289
348
|
this._abortController = new AbortController();
|
|
349
|
+
this._codexNativeSystemPromptPromise = null;
|
|
290
350
|
this._activeSubagents = new Map(); // sessionId → subagent ProbeAgent instance
|
|
291
351
|
this.tracer = options.tracer || null;
|
|
292
352
|
this.outline = !!options.outline;
|
|
@@ -322,6 +382,13 @@ export class ProbeAgent {
|
|
|
322
382
|
// Accepted values: 'off' (default), 'low', 'medium', 'high', or a number (budget tokens)
|
|
323
383
|
this.thinkingEffort = options.thinkingEffort || null;
|
|
324
384
|
|
|
385
|
+
if (options.governedCodexProfile !== undefined) {
|
|
386
|
+
if (options.provider !== 'codex') throw new TypeError('governedCodexProfile requires provider codex'); const profile = validateGovernedCodexProfile(options.governedCodexProfile);
|
|
387
|
+
const probeTools = profile.probeTools ?? profile.probeMcpTools;
|
|
388
|
+
if (options.disableTools || !Array.isArray(options.allowedTools) || options.allowedTools.length !== probeTools.length || options.allowedTools.some((tool, index) => tool !== probeTools[index])) throw new TypeError('allowedTools must exactly match governedCodexProfile Probe MCP tools');
|
|
389
|
+
this.governedCodexProfile = profile;
|
|
390
|
+
}
|
|
391
|
+
|
|
325
392
|
// Tool filtering configuration
|
|
326
393
|
// Parse allowedTools option: ['*'] = all tools, [] or null = no tools, ['tool1', 'tool2'] = specific tools
|
|
327
394
|
// Supports exclusion with '!' prefix: ['*', '!bash'] = all tools except bash
|
|
@@ -1752,6 +1819,7 @@ export class ProbeAgent {
|
|
|
1752
1819
|
result = ProbeAgent._wrapEngineStreamWithLimiter(result, limiter, this.debug);
|
|
1753
1820
|
}
|
|
1754
1821
|
} catch (error) {
|
|
1822
|
+
if (this.governedCodexProfile) throw error;
|
|
1755
1823
|
if (this.debug) {
|
|
1756
1824
|
const engineType = useClaudeCode ? 'Claude Code' : 'Codex';
|
|
1757
1825
|
console.log(`[DEBUG] Failed to use ${engineType} engine, falling back to Vercel:`, error.message);
|
|
@@ -2415,7 +2483,7 @@ export class ProbeAgent {
|
|
|
2415
2483
|
|
|
2416
2484
|
// For Codex CLI, use a cleaner system prompt without XML formatting
|
|
2417
2485
|
// since it has native MCP support for tools
|
|
2418
|
-
const systemPrompt = await this.
|
|
2486
|
+
const systemPrompt = await this._getCachedCodexNativeSystemPrompt();
|
|
2419
2487
|
|
|
2420
2488
|
this.engine = await createCodexEngine({
|
|
2421
2489
|
agent: this, // Pass reference to ProbeAgent for tool access
|
|
@@ -2424,7 +2492,8 @@ export class ProbeAgent {
|
|
|
2424
2492
|
sessionId: this.options?.sessionId,
|
|
2425
2493
|
debug: this.debug,
|
|
2426
2494
|
allowedTools: this.allowedTools, // Pass tool filtering configuration
|
|
2427
|
-
model: this.model // Pass model name (e.g., gpt-5.2, o3, etc.)
|
|
2495
|
+
model: this.model, // Pass model name (e.g., gpt-5.2, o3, etc.)
|
|
2496
|
+
governedCodexProfile: this.governedCodexProfile
|
|
2428
2497
|
});
|
|
2429
2498
|
if (this.debug) {
|
|
2430
2499
|
console.log('[DEBUG] Using Codex CLI engine with Probe tools');
|
|
@@ -2434,6 +2503,7 @@ export class ProbeAgent {
|
|
|
2434
2503
|
}
|
|
2435
2504
|
return this.engine;
|
|
2436
2505
|
} catch (error) {
|
|
2506
|
+
if (this.governedCodexProfile) throw error;
|
|
2437
2507
|
console.warn('[WARNING] Failed to load Codex CLI engine:', error.message);
|
|
2438
2508
|
console.warn('[WARNING] Falling back to Vercel AI SDK');
|
|
2439
2509
|
this.clientApiProvider = null;
|
|
@@ -3230,6 +3300,43 @@ ${extractGuidance2}
|
|
|
3230
3300
|
return systemPrompt;
|
|
3231
3301
|
}
|
|
3232
3302
|
|
|
3303
|
+
/**
|
|
3304
|
+
* Resolve the Codex system prompt once so preview and runtime bind identical bytes.
|
|
3305
|
+
* @returns {Promise<string>}
|
|
3306
|
+
* @private
|
|
3307
|
+
*/
|
|
3308
|
+
_getCachedCodexNativeSystemPrompt() {
|
|
3309
|
+
if (!this._codexNativeSystemPromptPromise) {
|
|
3310
|
+
this._codexNativeSystemPromptPromise = this.getCodexNativeSystemPrompt();
|
|
3311
|
+
}
|
|
3312
|
+
return this._codexNativeSystemPromptPromise;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
_prepareGovernedAnswerPrompt(message, options) {
|
|
3316
|
+
if (!this.governedCodexProfile) throw new Error('answerGoverned requires governedCodexProfile');
|
|
3317
|
+
if (!message || typeof message !== 'string' || message.trim().length === 0) throw new Error('Message is required and must be a non-empty string');
|
|
3318
|
+
if (!options) throw new Error('answerGoverned requires a valid JSON schema string');
|
|
3319
|
+
const schema = options.schema;
|
|
3320
|
+
if (typeof schema !== 'string' || !schema.trim() || !isJsonSchema(schema)) throw new Error('answerGoverned requires a valid JSON schema string');
|
|
3321
|
+
return { schema, prompt: message.trim() + generateSchemaInstructions(schema, { debug: this.debug }) };
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
/**
|
|
3325
|
+
* Preview the exact governed initial Codex dispatch without acquiring an engine or MCP server.
|
|
3326
|
+
* @param {string} message
|
|
3327
|
+
* @param {{schema: string}} options
|
|
3328
|
+
* @returns {Promise<{source: 'probe-host-tools-call', tool: 'codex', promptDigest: string, promptBytes: number}>}
|
|
3329
|
+
*/
|
|
3330
|
+
async previewGovernedAnswerDispatch(message, options) {
|
|
3331
|
+
if (!options || Reflect.ownKeys(options).length !== 1 || !Object.prototype.hasOwnProperty.call(options, 'schema')) {
|
|
3332
|
+
throw new Error('previewGovernedAnswerDispatch requires exactly {schema}');
|
|
3333
|
+
}
|
|
3334
|
+
const { prompt } = this._prepareGovernedAnswerPrompt(message, options);
|
|
3335
|
+
const systemPrompt = await this._getCachedCodexNativeSystemPrompt();
|
|
3336
|
+
const { previewGovernedCodexInitialDispatch } = await import('./engines/codex.js');
|
|
3337
|
+
return previewGovernedCodexInitialDispatch({ systemPrompt, customPrompt: this.customPrompt, prompt });
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3233
3340
|
/**
|
|
3234
3341
|
* Get the system message with instructions for the AI (XML Tool Format)
|
|
3235
3342
|
*/
|
|
@@ -3392,6 +3499,94 @@ Follow these instructions carefully:
|
|
|
3392
3499
|
return systemMessage;
|
|
3393
3500
|
}
|
|
3394
3501
|
|
|
3502
|
+
/**
|
|
3503
|
+
* Return one schema-validated result with its governed Codex attestation.
|
|
3504
|
+
* @param {string} message - The user's question
|
|
3505
|
+
* @param {Object} options - Governed answer options
|
|
3506
|
+
* @param {string} options.schema - Required JSON schema
|
|
3507
|
+
* @param {Array} [images] - Unsupported; must be empty
|
|
3508
|
+
* @returns {Promise<{data: unknown, runtimeAttestation: Object}>}
|
|
3509
|
+
*/
|
|
3510
|
+
async answerGoverned(message, options, images = []) {
|
|
3511
|
+
const { schema, prompt } = this._prepareGovernedAnswerPrompt(message, options);
|
|
3512
|
+
if (!Array.isArray(images) || images.length > 0) throw new Error('answerGoverned does not support images');
|
|
3513
|
+
|
|
3514
|
+
const hasInvocationDigest = Object.prototype.hasOwnProperty.call(options,
|
|
3515
|
+
'invocationDigest');
|
|
3516
|
+
const invocationDigest = hasInvocationDigest ? options.invocationDigest : undefined;
|
|
3517
|
+
if (hasInvocationDigest && (typeof invocationDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(invocationDigest))) {
|
|
3518
|
+
throw new TypeError('answerGoverned invocationDigest must match sha256:<64 lowercase hexadecimal digits>');
|
|
3519
|
+
}
|
|
3520
|
+
const hasResultIdentity = Object.prototype.hasOwnProperty.call(options,
|
|
3521
|
+
'resultIdentity');
|
|
3522
|
+
const requestedResultIdentity = hasResultIdentity ? options.resultIdentity : undefined;
|
|
3523
|
+
if (hasResultIdentity && requestedResultIdentity !== GOVERNED_RESULT_IDENTITY) {
|
|
3524
|
+
throw new TypeError('answerGoverned resultIdentity must equal probe.governed-result-identity/v1');
|
|
3525
|
+
}
|
|
3526
|
+
if (hasResultIdentity && !hasInvocationDigest) {
|
|
3527
|
+
throw new TypeError('answerGoverned resultIdentity requires an own invocationDigest');
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
let engine, answerFailure = null;
|
|
3531
|
+
try {
|
|
3532
|
+
try { engine = await this.getEngine(); }
|
|
3533
|
+
catch { throw governedAnswerFailure('provider_engine'); }
|
|
3534
|
+
if (!engine?.query) throw governedAnswerFailure('provider_engine');
|
|
3535
|
+
const candidateChunks = [];
|
|
3536
|
+
let runtimeAttestation;
|
|
3537
|
+
let attestationCount = 0;
|
|
3538
|
+
let nativeToolBatch;
|
|
3539
|
+
let nativeToolBatchCount = 0;
|
|
3540
|
+
const queryOptions = hasInvocationDigest
|
|
3541
|
+
? { abortSignal: this._abortController.signal, invocationDigest: invocationDigest }
|
|
3542
|
+
: { abortSignal: this._abortController.signal };
|
|
3543
|
+
for await (const chunk of engine.query(prompt, queryOptions)) {
|
|
3544
|
+
if (chunk.type === 'text' && chunk.content) candidateChunks.push(chunk.content);
|
|
3545
|
+
else if (chunk.type === 'metadata' && chunk.data?.attestation) {
|
|
3546
|
+
runtimeAttestation = chunk.data.attestation;
|
|
3547
|
+
attestationCount++;
|
|
3548
|
+
} else if (chunk.type === 'toolBatch') {
|
|
3549
|
+
nativeToolBatch = chunk;
|
|
3550
|
+
nativeToolBatchCount++;
|
|
3551
|
+
} else if (chunk.type === 'error') throw normalizeGovernedAnswerFailure(chunk.error, 'unknown');
|
|
3552
|
+
}
|
|
3553
|
+
if (hasInvocationDigest) {
|
|
3554
|
+
const expectedAttestation = this.governedCodexProfile?.version === 'probe.governed-codex-profile/v2'
|
|
3555
|
+
? 'probe.governed-codex-attestation/v3' : 'probe.governed-codex-attestation/v2';
|
|
3556
|
+
if (attestationCount !== 1 || runtimeAttestation?.version !== expectedAttestation || runtimeAttestation?.executionContext?.source !== 'caller' || runtimeAttestation?.executionContext?.invocationDigest !== invocationDigest) {
|
|
3557
|
+
throw new Error('Expected exactly one matching governed invocation attestation');
|
|
3558
|
+
}
|
|
3559
|
+
} else if (attestationCount !== 1) throw new Error(`Expected exactly one governed runtime attestation; received ${attestationCount}`);
|
|
3560
|
+
if (this.governedCodexProfile?.version === 'probe.governed-codex-profile/v2') {
|
|
3561
|
+
if (nativeToolBatchCount !== 1 || !Number.isSafeInteger(nativeToolBatch?.total) ||
|
|
3562
|
+
!Array.isArray(nativeToolBatch?.tools) || nativeToolBatch.total !== runtimeAttestation?.observed?.nativeTools?.total ||
|
|
3563
|
+
JSON.stringify(nativeToolBatch.tools) !== JSON.stringify(runtimeAttestation?.observed?.nativeTools?.tools)) {
|
|
3564
|
+
throw new Error('Expected exactly one matching governed native capability aggregate');
|
|
3565
|
+
}
|
|
3566
|
+
for (const toolEvent of nativeToolBatch.tools) this.events.emit('toolCall', toolEvent);
|
|
3567
|
+
} else if (nativeToolBatchCount !== 0) throw new Error('Unexpected governed native capability aggregate');
|
|
3568
|
+
const validation = validateJsonResponse(candidateChunks.join(''), {debug:this.debug,schema});
|
|
3569
|
+
if (!validation.isValid) throw governedSchemaResultValidationFailure(validation);
|
|
3570
|
+
if (hasResultIdentity) {
|
|
3571
|
+
let identified;
|
|
3572
|
+
try { identified = identifyGovernedResult(validation.parsed); }
|
|
3573
|
+
catch { throw governedAnswerFailure('schema_result_validation', null, null, null, null, 'result_identity'); }
|
|
3574
|
+
const { data, resultIdentity } = identified;
|
|
3575
|
+
freezeGovernedTree(runtimeAttestation);
|
|
3576
|
+
return Object.freeze({ data, runtimeAttestation, resultIdentity });
|
|
3577
|
+
}
|
|
3578
|
+
return { data: validation.parsed, runtimeAttestation };
|
|
3579
|
+
} catch (error) {
|
|
3580
|
+
answerFailure = normalizeGovernedAnswerFailure(error, 'unknown');
|
|
3581
|
+
throw answerFailure;
|
|
3582
|
+
} finally {
|
|
3583
|
+
if (engine) {
|
|
3584
|
+
try { await engine.close(); }
|
|
3585
|
+
catch { if (!answerFailure) throw governedAnswerFailure('unknown'); }
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3395
3590
|
/**
|
|
3396
3591
|
* Answer a question using the agentic flow
|
|
3397
3592
|
* @param {string} message - The user's question
|
|
@@ -3643,14 +3838,14 @@ Follow these instructions carefully:
|
|
|
3643
3838
|
}
|
|
3644
3839
|
|
|
3645
3840
|
// Send the message directly to Codex and collect the response
|
|
3646
|
-
try {
|
|
3647
|
-
|
|
3841
|
+
let engine; try {
|
|
3842
|
+
engine = await this.getEngine();
|
|
3648
3843
|
if (engine && engine.query) {
|
|
3649
3844
|
let assistantResponseContent = '';
|
|
3650
3845
|
let toolBatch = null;
|
|
3651
3846
|
|
|
3652
3847
|
// Query Codex directly with the message and schema
|
|
3653
|
-
for await (const chunk of engine.query(message, options)) {
|
|
3848
|
+
for await (const chunk of engine.query(message, this.governedCodexProfile ? { ...options, abortSignal: this._abortController.signal } : options)) {
|
|
3654
3849
|
if (chunk.type === 'text' && chunk.content) {
|
|
3655
3850
|
assistantResponseContent += chunk.content;
|
|
3656
3851
|
if (options.onStream) {
|
|
@@ -3702,7 +3897,7 @@ Follow these instructions carefully:
|
|
|
3702
3897
|
console.error('[DEBUG] Codex error:', error);
|
|
3703
3898
|
}
|
|
3704
3899
|
throw error;
|
|
3705
|
-
}
|
|
3900
|
+
} finally { if (this.governedCodexProfile && engine) await engine.close(); }
|
|
3706
3901
|
}
|
|
3707
3902
|
|
|
3708
3903
|
if (this.debug) {
|
|
@@ -5659,6 +5854,8 @@ Double-check your response based on the criteria above. If everything looks good
|
|
|
5659
5854
|
this._abortController.abort();
|
|
5660
5855
|
}
|
|
5661
5856
|
|
|
5857
|
+
if (this.governedCodexProfile && this.engine?.close) await this.engine.close();
|
|
5858
|
+
|
|
5662
5859
|
// Clean up MCP bridge
|
|
5663
5860
|
if (this.mcpBridge) {
|
|
5664
5861
|
try {
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* @module agent/bashExecutor
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { resolve, join } from 'path';
|
|
6
|
+
import { resolve } from 'path';
|
|
8
7
|
import { existsSync } from 'fs';
|
|
9
8
|
import { parseCommandForExecution, isComplexCommand } from './bashCommandUtils.js';
|
|
9
|
+
import { spawnGovernedProcess } from './processSupervisor.js';
|
|
10
10
|
|
|
11
11
|
// ─── Interactive Command Detection ─────────────────────────────────────────
|
|
12
12
|
|
|
@@ -261,7 +261,7 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
261
261
|
console.log(`[BashExecutor] Timeout: ${timeout}ms`);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
return new Promise((resolve
|
|
264
|
+
return new Promise((resolve) => {
|
|
265
265
|
// Create environment with non-interactive safety defaults.
|
|
266
266
|
// These prevent commands from opening editors or TTY prompts
|
|
267
267
|
// when stdin is not available (which would cause hangs).
|
|
@@ -276,14 +276,13 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
276
276
|
// Check if this is a complex command (contains pipes, operators, etc.)
|
|
277
277
|
const isComplex = isComplexCommand(command);
|
|
278
278
|
|
|
279
|
-
let cmd, cmdArgs
|
|
279
|
+
let cmd, cmdArgs;
|
|
280
280
|
|
|
281
281
|
if (isComplex) {
|
|
282
282
|
// For complex commands, use sh -c to execute through shell
|
|
283
283
|
// This is only reached if the permission checker allowed the complex command
|
|
284
284
|
cmd = 'sh';
|
|
285
285
|
cmdArgs = ['-c', command];
|
|
286
|
-
useShell = false; // We explicitly use sh -c, not spawn's shell option
|
|
287
286
|
if (debug) {
|
|
288
287
|
console.log(`[BashExecutor] Complex command - using sh -c`);
|
|
289
288
|
}
|
|
@@ -304,7 +303,6 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
304
303
|
return;
|
|
305
304
|
}
|
|
306
305
|
[cmd, ...cmdArgs] = args;
|
|
307
|
-
useShell = false;
|
|
308
306
|
}
|
|
309
307
|
|
|
310
308
|
// Spawn the process in a new session (detached: true → setsid on Linux).
|
|
@@ -312,83 +310,42 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
312
310
|
// /dev/tty unavailable. Any program that tries to open an interactive
|
|
313
311
|
// editor or TTY prompt (e.g. vim from git rebase) will get ENXIO and
|
|
314
312
|
// fail immediately instead of hanging forever.
|
|
315
|
-
const
|
|
313
|
+
const governed = spawnGovernedProcess({
|
|
314
|
+
command: cmd,
|
|
315
|
+
args: cmdArgs,
|
|
316
316
|
cwd,
|
|
317
317
|
env: processEnv,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
318
|
+
executionTimeoutMs: timeout,
|
|
319
|
+
terminationGraceMs: 5000,
|
|
320
|
+
cleanupTimeoutMs: 10000,
|
|
321
|
+
stdoutByteCap: maxBuffer,
|
|
322
|
+
stderrByteCap: maxBuffer,
|
|
323
|
+
signalScope: 'process-group'
|
|
322
324
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// Helper: kill the entire process group (negative PID) so that
|
|
330
|
-
// sub-processes spawned by the command (e.g. an editor) are also killed.
|
|
331
|
-
// Falls back to killing just the child if process.kill fails.
|
|
332
|
-
const killProcessGroup = (signal) => {
|
|
333
|
-
try {
|
|
334
|
-
if (child.pid) process.kill(-child.pid, signal);
|
|
335
|
-
} catch {
|
|
336
|
-
try { child.kill(signal); } catch { /* already dead */ }
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
// Set timeout
|
|
341
|
-
if (timeout > 0) {
|
|
342
|
-
timeoutHandle = setTimeout(() => {
|
|
343
|
-
if (!killed) {
|
|
344
|
-
killed = true;
|
|
345
|
-
killProcessGroup('SIGTERM');
|
|
346
|
-
|
|
347
|
-
// Force kill after 5 seconds if still running
|
|
348
|
-
setTimeout(() => {
|
|
349
|
-
if (child.exitCode === null) {
|
|
350
|
-
killProcessGroup('SIGKILL');
|
|
351
|
-
}
|
|
352
|
-
}, 5000);
|
|
353
|
-
}
|
|
354
|
-
}, timeout);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Handle stdout
|
|
358
|
-
child.stdout.on('data', (data) => {
|
|
359
|
-
const chunk = data.toString();
|
|
360
|
-
if (stdout.length + chunk.length <= maxBuffer) {
|
|
361
|
-
stdout += chunk;
|
|
362
|
-
} else {
|
|
363
|
-
// Buffer overflow
|
|
364
|
-
if (!killed) {
|
|
365
|
-
killed = true;
|
|
366
|
-
killProcessGroup('SIGTERM');
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Handle stderr
|
|
372
|
-
child.stderr.on('data', (data) => {
|
|
373
|
-
const chunk = data.toString();
|
|
374
|
-
if (stderr.length + chunk.length <= maxBuffer) {
|
|
375
|
-
stderr += chunk;
|
|
376
|
-
} else {
|
|
377
|
-
// Buffer overflow
|
|
378
|
-
if (!killed) {
|
|
379
|
-
killed = true;
|
|
380
|
-
killProcessGroup('SIGTERM');
|
|
325
|
+
governed.result.then(receipt => {
|
|
326
|
+
const duration = Date.now() - startTime;
|
|
327
|
+
if (receipt.classification === 'spawn_error') {
|
|
328
|
+
if (debug) {
|
|
329
|
+
console.log(`[BashExecutor] Spawn error: ${receipt.error ?? 'spawn failed'}`);
|
|
381
330
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
331
|
+
resolve({
|
|
332
|
+
success: false,
|
|
333
|
+
error: `Failed to execute command: ${receipt.error ?? 'spawn failed'}`,
|
|
334
|
+
stdout: '',
|
|
335
|
+
stderr: '',
|
|
336
|
+
exitCode: 1,
|
|
337
|
+
command,
|
|
338
|
+
workingDirectory: cwd,
|
|
339
|
+
duration
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
389
342
|
}
|
|
390
343
|
|
|
391
|
-
const
|
|
344
|
+
const stdout = receipt.stdout;
|
|
345
|
+
const stderr = receipt.stderr;
|
|
346
|
+
const code = receipt.exitCode;
|
|
347
|
+
const signal = receipt.signal;
|
|
348
|
+
const killed = ['execution_timeout', 'output_overflow', 'terminated', 'aborted', 'cleanup_timeout'].includes(receipt.classification);
|
|
392
349
|
|
|
393
350
|
if (debug) {
|
|
394
351
|
console.log(`[BashExecutor] Command completed - Code: ${code}, Signal: ${signal}, Duration: ${duration}ms`);
|
|
@@ -400,7 +357,7 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
400
357
|
|
|
401
358
|
if (killed) {
|
|
402
359
|
success = false;
|
|
403
|
-
if (
|
|
360
|
+
if (receipt.classification === 'output_overflow') {
|
|
404
361
|
error = `Command output exceeded maximum buffer size (${maxBuffer} bytes)`;
|
|
405
362
|
} else {
|
|
406
363
|
error = `Command timed out after ${timeout}ms`;
|
|
@@ -423,28 +380,6 @@ export async function executeBashCommand(command, options = {}) {
|
|
|
423
380
|
killed
|
|
424
381
|
});
|
|
425
382
|
});
|
|
426
|
-
|
|
427
|
-
// Handle spawn errors
|
|
428
|
-
child.on('error', (error) => {
|
|
429
|
-
if (timeoutHandle) {
|
|
430
|
-
clearTimeout(timeoutHandle);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (debug) {
|
|
434
|
-
console.log(`[BashExecutor] Spawn error:`, error);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
resolve({
|
|
438
|
-
success: false,
|
|
439
|
-
error: `Failed to execute command: ${error.message}`,
|
|
440
|
-
stdout: '',
|
|
441
|
-
stderr: '',
|
|
442
|
-
exitCode: 1,
|
|
443
|
-
command,
|
|
444
|
-
workingDirectory: cwd,
|
|
445
|
-
duration: Date.now() - startTime
|
|
446
|
-
});
|
|
447
|
-
});
|
|
448
383
|
});
|
|
449
384
|
}
|
|
450
385
|
|
|
@@ -557,4 +492,4 @@ export function validateExecutionOptions(options = {}) {
|
|
|
557
492
|
errors,
|
|
558
493
|
warnings
|
|
559
494
|
};
|
|
560
|
-
}
|
|
495
|
+
}
|