@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.
Files changed (45) hide show
  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  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/build/utils/provider.js +9 -3
  24. package/cjs/agent/ProbeAgent.cjs +13463 -12187
  25. package/cjs/index.cjs +75974 -74139
  26. package/index.d.ts +149 -4
  27. package/package.json +6 -2
  28. package/src/agent/ProbeAgent.d.ts +105 -4
  29. package/src/agent/ProbeAgent.js +209 -12
  30. package/src/agent/bashExecutor.js +36 -101
  31. package/src/agent/engines/codex.js +367 -88
  32. package/src/agent/engines/governed-answer-failure.js +152 -0
  33. package/src/agent/engines/governed-codex-profile.js +198 -0
  34. package/src/agent/governance/acknowledgedJsonlChannel.js +328 -0
  35. package/src/agent/governance/atomicTerminalReceipt.js +188 -0
  36. package/src/agent/governance/index.d.ts +130 -0
  37. package/src/agent/governance/index.js +8 -0
  38. package/src/agent/mcp/built-in-server.js +152 -53
  39. package/src/agent/mcp/index.d.ts +65 -0
  40. package/src/agent/mcp/index.js +6 -1
  41. package/src/agent/probeTool.js +1 -1
  42. package/src/agent/processSupervisor.js +351 -0
  43. package/src/agent/tools.js +14 -8
  44. package/src/index.js +2 -0
  45. package/src/utils/provider.js +9 -3
@@ -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 '../index.js';
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.getCodexNativeSystemPrompt();
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
- const engine = await this.getEngine();
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 { spawn } from 'child_process';
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, reject) => {
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, useShell;
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 child = spawn(cmd, cmdArgs, {
313
+ const governed = spawnGovernedProcess({
314
+ command: cmd,
315
+ args: cmdArgs,
316
316
  cwd,
317
317
  env: processEnv,
318
- stdio: ['ignore', 'pipe', 'pipe'], // stdin ignored, capture stdout/stderr
319
- shell: useShell, // false for security
320
- detached: true, // new session — no controlling terminal
321
- windowsHide: true
318
+ executionTimeoutMs: timeout,
319
+ terminationGraceMs: 5000,
320
+ cleanupTimeoutMs: 10000,
321
+ stdoutByteCap: maxBuffer,
322
+ stderrByteCap: maxBuffer,
323
+ signalScope: 'process-group'
322
324
  });
323
-
324
- let stdout = '';
325
- let stderr = '';
326
- let killed = false;
327
- let timeoutHandle;
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
- // Handle process exit
386
- child.on('close', (code, signal) => {
387
- if (timeoutHandle) {
388
- clearTimeout(timeoutHandle);
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 duration = Date.now() - startTime;
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 (stdout.length + stderr.length > maxBuffer) {
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
+ }