{"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,EAAE,KAAK,cAAc,EAA6B,MAAM,iBAAiB,CAAC;AAOjF,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC5B,2FAA2F;IAC3F,eAAe,EAAE,OAAO,CAAC;IACzB,uGAAuG;IACvG,uBAAuB,EAAE,OAAO,CAAC;IACjC;;;;;;OAMG;IACH,cAAc,EAAE,yBAAyB,GAAG,2BAA2B,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;IAClH,+FAA+F;IAC/F,4BAA4B,EAAE,KAAK,CAAC;IACpC;;;OAGG;IACH,+BAA+B,EAAE,OAAO,CAAC;IACzC,6FAA6F;IAC7F,iBAAiB,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,mBAAmB,EAAE,KAAK,CAAC;IAC3B,gCAAgC;IAChC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;IAIxB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,YAAY,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACvB;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAE/F;AA6CD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAkOrB","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport stripAnsi from \"strip-ansi\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.js\";\nimport { type BashOperations, createLocalBashOperations } from \"./tools/bash.js\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n\t/** Timeout in seconds (optional). When exceeded, the process tree is killed. */\n\ttimeout?: number;\n}\n\n/**\n * Evidence metadata for every shell command execution.\n *\n * All commands receive this evidence block, not just pipelines. The exit code\n * returned by Bash represents the final status of the supplied source, but for\n * compound commands (sequences, functions, subshells, recovery operators) it\n * does not prove that every internal command succeeded.\n *\n * Stage exit codes are never captured from shared file descriptors because\n * those channels are observable and writable by the executed command itself.\n */\nexport interface BashEvidence {\n\t/** Whether the exit status is known. False for timeout, cancellation, and spawn errors. */\n\texitStatusKnown: boolean;\n\t/** Whether the reported exit status faithfully reflects what Bash returned for the supplied source. */\n\texitStatusAuthoritative: boolean;\n\t/**\n\t * Authority scope for the exit code.\n\t * - \"final_shell_exit_status\": command completed — exit code represents the final Bash status.\n\t * - \"final_pipeline_stage_only\": pipeline suspected — exit code may represent only the last stage.\n\t * - \"no_exit_status\": no exit code produced (timeout/cancellation).\n\t * - \"no_process_started\": shell never spawned (spawn error).\n\t */\n\tauthorityScope: \"final_shell_exit_status\" | \"final_pipeline_stage_only\" | \"no_exit_status\" | \"no_process_started\";\n\t/** Always false: the internal status of each command within compound source is not tracked. */\n\tinternalCommandStatusesKnown: false;\n\t/**\n\t * Whether the evidence can be used for validation decisions.\n\t * False for pipelines (stage exit codes unknown) and error states.\n\t */\n\tvalidationEvidenceAuthoritative: boolean;\n\t/** Whether the command is suspected to contain a pipeline (contains | outside of quoting) */\n\tpipelineSuspected: boolean;\n\t/** Always false: stage exit codes are never known from untrusted channels */\n\tstageExitCodesKnown: false;\n\t/** The final shell exit code */\n\tfinalShellExitCode: number | undefined;\n\t/** Warning message for the model when evidence is non-authoritative */\n\twarning?: string;\n}\n\n/**\n * Public result of a bash command execution.\n *\n * All fields added after 1.1.6 are optional in the public type for backward\n * compatibility with consumers that construct mocks, adapters, or fixtures\n * using the 1.1.6 shape. The runtime always produces every field.\n */\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled/timedOut/spawnError) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n\n\t// --- Added in 1.1.7 (optional in public type; always present at runtime) ---\n\n\t/** Separate stdout stream (empty string if no stdout was produced) */\n\tstdout?: string;\n\t/** Separate stderr stream (empty string if no stderr was produced) */\n\tstderr?: string;\n\t/** Whether the command timed out */\n\ttimedOut?: boolean;\n\t/** ISO timestamp when command execution started */\n\tstartedAt?: string;\n\t/** ISO timestamp when command execution finished */\n\tfinishedAt?: string;\n\t/** Spawn error message if the process failed to start (e.g., executable not found) */\n\tspawnError?: string;\n\t/** Evidence metadata for every execution (exit status, authority scope, pipeline flag) */\n\tevidence?: BashEvidence;\n}\n\n/**\n * Internal resolved type: every runtime-produced BashResult satisfies this\n * contract. The formatter and other internal consumers use this type so they\n * can rely on the fields being present without optional chaining, while the\n * public BashResult remains backward-compatible with the 1.1.6 surface.\n */\nexport interface ResolvedBashResult extends BashResult {\n\tstdout: string;\n\tstderr: string;\n\ttimedOut: boolean;\n\tstartedAt: string;\n\tfinishedAt: string;\n\tevidence: BashEvidence;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command with optional streaming and cancellation support.\n *\n * Uses the same local BashOperations backend as createBashTool() so interactive\n * user bash and tool-invoked bash share the same process spawning behavior.\n * Sanitization, newline normalization, temp-file capture, and truncation still\n * happen in executeBashWithOperations(), so reusing the local backend does not\n * change output processing behavior.\n *\n * @param command - The bash command to execute\n * @param options - Optional streaming callback and abort signal\n * @returns Promise resolving to execution result\n */\nexport function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {\n\treturn executeBashWithOperations(command, process.cwd(), createLocalBashOperations(), options);\n}\n\n/**\n * Detect whether a command string is suspected to contain a pipeline.\n * This is a conservative heuristic prioritizing recall over precision.\n *\n * A false positive (flagging a non-pipeline as a pipeline) only causes\n * the model to see a non-authoritative warning — safe. A false negative\n * (missing a real pipeline) would incorrectly present an authoritative\n * exit code — dangerous.\n *\n * The detector checks for unquoted | characters, skipping || (OR operator).\n * Known false positives: quoted pipe-like patterns in complex bash constructs.\n * These are acceptable because they only produce non-authoritative warnings,\n * never false authoritative results.\n */\nfunction suspectPipeline(command: string): boolean {\n\t// Two-pass detection:\n\t// Pass 1: Quick check — does the command contain | at all?\n\t// Pass 2: If yes, do quoting-aware scan to exclude quoted pipes.\n\t// Both passes are conservative: pass 1 may catch edge cases that\n\t// pass 2 would miss, so we run pass 1 first for recall.\n\n\t// Scan for unquoted | not part of ||, &&, or within quotes\n\tlet inSingle = false;\n\tlet inDouble = false;\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\t\tconst prev = i > 0 ? command[i - 1] : null;\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\tinSingle = !inSingle;\n\t\t} else if (ch === '\"' && !inSingle && prev !== \"\\\\\") {\n\t\t\tinDouble = !inDouble;\n\t\t} else if (ch === \"|\" && !inSingle && !inDouble) {\n\t\t\t// Skip || (OR operator) — two consecutive pipes\n\t\t\tif (i + 1 < command.length && command[i + 1] === \"|\") {\n\t\t\t\ti++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst startedAt = new Date().toISOString();\n\tconst outputChunks: string[] = [];\n\tconst stdoutChunks: string[] = [];\n\tconst stderrChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tlet spawnErr: string | undefined;\n\tconst timedOut = false;\n\tconst pipelineSuspected = suspectPipeline(command);\n\n\tconst WARNING_TEXT =\n\t\t\"This command appears to contain a shell pipeline. The reported shell exit code may represent only the final pipeline stage. Do not use this result as authoritative validation. Re-run the validation command without a pipeline.\";\n\n\tconst COMPOUND_WARNING_TEXT =\n\t\t\"The final Bash exit status is authoritative for its stated scope. It does not verify every internal command. For compound shell source (sequences, functions, subshells), exit code 0 does not prove all internal commands succeeded. Prefer direct single-command validation.\";\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\ttempFileStream.write(chunk);\n\t\t\t}\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst onStdout = (data: Buffer) => {\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\t\tstdoutChunks.push(text);\n\t};\n\n\tconst onStderr = (data: Buffer) => {\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\t\tstderrChunks.push(text);\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tonStdout,\n\t\t\tonStderr,\n\t\t\tsignal: options?.signal,\n\t\t\ttimeout: options?.timeout,\n\t\t});\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.end();\n\t\t}\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\t\tconst finishedAt = new Date().toISOString();\n\n\t\tconst exitCode = result.exitCode ?? undefined;\n\n\t\tconst evidence: BashEvidence = pipelineSuspected\n\t\t\t? {\n\t\t\t\t\texitStatusKnown: true,\n\t\t\t\t\texitStatusAuthoritative: true,\n\t\t\t\t\tauthorityScope: \"final_pipeline_stage_only\",\n\t\t\t\t\tinternalCommandStatusesKnown: false,\n\t\t\t\t\tvalidationEvidenceAuthoritative: false,\n\t\t\t\t\tpipelineSuspected: true,\n\t\t\t\t\tstageExitCodesKnown: false,\n\t\t\t\t\tfinalShellExitCode: exitCode,\n\t\t\t\t\twarning: WARNING_TEXT,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\texitStatusKnown: true,\n\t\t\t\t\texitStatusAuthoritative: true,\n\t\t\t\t\tauthorityScope: \"final_shell_exit_status\",\n\t\t\t\t\tinternalCommandStatusesKnown: false,\n\t\t\t\t\tvalidationEvidenceAuthoritative: true,\n\t\t\t\t\tpipelineSuspected: false,\n\t\t\t\t\tstageExitCodesKnown: false,\n\t\t\t\t\tfinalShellExitCode: exitCode,\n\t\t\t\t\twarning: COMPOUND_WARNING_TEXT,\n\t\t\t\t};\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\tstdout: stdoutChunks.join(\"\"),\n\t\t\tstderr: stderrChunks.join(\"\"),\n\t\t\texitCode: cancelled || timedOut ? undefined : exitCode,\n\t\t\tcancelled,\n\t\t\ttimedOut,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t\tstartedAt,\n\t\t\tfinishedAt,\n\t\t\tspawnError: spawnErr,\n\t\t\tevidence,\n\t\t};\n\t} catch (err) {\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.end();\n\t\t}\n\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tconst finishedAt = new Date().toISOString();\n\t\t\tconst evidence: BashEvidence = {\n\t\t\t\texitStatusKnown: false,\n\t\t\t\texitStatusAuthoritative: false,\n\t\t\t\tauthorityScope: \"no_exit_status\",\n\t\t\t\tinternalCommandStatusesKnown: false,\n\t\t\t\tvalidationEvidenceAuthoritative: false,\n\t\t\t\tpipelineSuspected,\n\t\t\t\tstageExitCodesKnown: false,\n\t\t\t\tfinalShellExitCode: undefined,\n\t\t\t};\n\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\tstdout: stdoutChunks.join(\"\"),\n\t\t\t\tstderr: stderrChunks.join(\"\"),\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttimedOut: false,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\tstartedAt,\n\t\t\t\tfinishedAt,\n\t\t\t\tspawnError: undefined,\n\t\t\t\tevidence,\n\t\t\t};\n\t\t}\n\n\t\t// Check if it was a timeout\n\t\tif (err instanceof Error && err.message.startsWith(\"timeout:\")) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tconst finishedAt = new Date().toISOString();\n\t\t\tconst evidence: BashEvidence = {\n\t\t\t\texitStatusKnown: false,\n\t\t\t\texitStatusAuthoritative: false,\n\t\t\t\tauthorityScope: \"no_exit_status\",\n\t\t\t\tinternalCommandStatusesKnown: false,\n\t\t\t\tvalidationEvidenceAuthoritative: false,\n\t\t\t\tpipelineSuspected,\n\t\t\t\tstageExitCodesKnown: false,\n\t\t\t\tfinalShellExitCode: undefined,\n\t\t\t};\n\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\tstdout: stdoutChunks.join(\"\"),\n\t\t\t\tstderr: stderrChunks.join(\"\"),\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: false,\n\t\t\t\ttimedOut: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\tstartedAt,\n\t\t\t\tfinishedAt,\n\t\t\t\tspawnError: undefined,\n\t\t\t\tevidence,\n\t\t\t};\n\t\t}\n\n\t\t// Spawn error or other failure — propagate\n\t\tconst finishedAt = new Date().toISOString();\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tconst spawnError = err instanceof Error ? err.message : String(err);\n\t\tconst evidence: BashEvidence = {\n\t\t\texitStatusKnown: false,\n\t\t\texitStatusAuthoritative: false,\n\t\t\tauthorityScope: \"no_process_started\",\n\t\t\tinternalCommandStatusesKnown: false,\n\t\t\tvalidationEvidenceAuthoritative: false,\n\t\t\tpipelineSuspected: false,\n\t\t\tstageExitCodesKnown: false,\n\t\t\tfinalShellExitCode: undefined,\n\t\t};\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\tstdout: stdoutChunks.join(\"\"),\n\t\t\tstderr: stderrChunks.join(\"\"),\n\t\t\texitCode: undefined,\n\t\t\tcancelled: false,\n\t\t\ttimedOut: false,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t\tstartedAt,\n\t\t\tfinishedAt,\n\t\t\tspawnError,\n\t\t\tevidence,\n\t\t};\n\t}\n}\n"]}