2026-01-14 05:39:59 +00:00
import type { AgentTool , AgentToolResult } from "@mariozechner/pi-agent-core" ;
2026-02-01 10:03:47 +09:00
import crypto from "node:crypto" ;
2026-02-16 10:34:29 -08:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
2026-02-01 10:03:47 +09:00
import type { BashSandboxConfig } from "./bash-tools.shared.js" ;
2026-01-18 04:27:33 +00:00
import {
type ExecAsk ,
type ExecHost ,
type ExecSecurity ,
2026-01-21 22:02:17 -08:00
type ExecApprovalsFile ,
2026-01-18 04:27:33 +00:00
addAllowlistEntry ,
2026-01-23 00:10:19 +00:00
evaluateShellAllowlist ,
2026-01-18 04:27:33 +00:00
maxAsk ,
minSecurity ,
2026-01-21 22:02:17 -08:00
requiresExecApproval ,
2026-01-21 21:44:28 +00:00
resolveSafeBins ,
2026-01-18 04:27:33 +00:00
recordAllowlistUse ,
resolveExecApprovals ,
2026-01-21 22:02:17 -08:00
resolveExecApprovalsFromFile ,
2026-02-14 19:42:52 +01:00
buildSafeShellCommand ,
2026-02-14 19:59:03 +01:00
buildSafeBinsShellCommand ,
2026-01-18 04:27:33 +00:00
} from "../infra/exec-approvals.js" ;
2026-01-18 07:44:28 +00:00
import { buildNodeShellCommand } from "../infra/node-shell.js" ;
2026-01-20 14:03:59 +00:00
import {
getShellPathFromLoginShell ,
resolveShellEnvFallbackTimeoutMs ,
} from "../infra/shell-env.js" ;
2026-02-13 17:49:29 +00:00
import { logInfo } from "../logger.js" ;
2026-02-01 10:03:47 +09:00
import { parseAgentSessionKey , resolveAgentIdFromSessionKey } from "../routing/session-key.js" ;
2026-02-13 17:49:29 +00:00
import { markBackgrounded , tail } from "./bash-process-registry.js" ;
2026-01-17 04:57:04 +00:00
import {
2026-02-13 17:49:29 +00:00
DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS ,
DEFAULT_APPROVAL_TIMEOUT_MS ,
DEFAULT_MAX_OUTPUT ,
DEFAULT_NOTIFY_TAIL_CHARS ,
DEFAULT_PATH ,
DEFAULT_PENDING_MAX_OUTPUT ,
applyPathPrepend ,
applyShellPath ,
createApprovalSlug ,
emitExecSystemEvent ,
normalizeExecAsk ,
normalizeExecHost ,
normalizeExecSecurity ,
normalizeNotifyOutput ,
normalizePathPrepend ,
renderExecHostLabel ,
resolveApprovalRunningNoticeMs ,
runExecProcess ,
execSchema ,
type ExecProcessHandle ,
validateHostEnv ,
} from "./bash-tools.exec-runtime.js" ;
2026-01-14 05:39:59 +00:00
import {
buildSandboxEnv ,
2026-02-08 23:59:43 -08:00
clampWithDefault ,
2026-01-14 05:39:59 +00:00
coerceEnv ,
readEnvInt ,
resolveSandboxWorkdir ,
resolveWorkdir ,
truncateMiddle ,
} from "./bash-tools.shared.js" ;
2026-01-18 04:27:33 +00:00
import { callGatewayTool } from "./tools/gateway.js" ;
import { listNodes , resolveNodeIdFromList } from "./tools/nodes-utils.js" ;
2026-01-14 05:39:59 +00:00
export type ExecToolDefaults = {
2026-01-18 04:27:33 +00:00
host? : ExecHost ;
security? : ExecSecurity ;
ask? : ExecAsk ;
node? : string ;
2026-01-19 00:35:39 +00:00
pathPrepend? : string [ ] ;
2026-01-21 21:44:28 +00:00
safeBins? : string [ ] ;
2026-01-18 04:27:33 +00:00
agentId? : string ;
2026-01-14 05:39:59 +00:00
backgroundMs? : number ;
timeoutSec? : number ;
2026-01-22 00:49:02 +00:00
approvalRunningNoticeMs? : number ;
2026-01-14 05:39:59 +00:00
sandbox? : BashSandboxConfig ;
elevated? : ExecElevatedDefaults ;
allowBackground? : boolean ;
scopeKey? : string ;
2026-01-17 05:43:27 +00:00
sessionKey? : string ;
2026-01-17 17:55:04 +00:00
messageProvider? : string ;
2026-01-17 05:43:27 +00:00
notifyOnExit? : boolean ;
2026-02-14 18:32:45 -05:00
notifyOnExitEmptySuccess? : boolean ;
2026-01-14 05:39:59 +00:00
cwd? : string ;
} ;
export type { BashSandboxConfig } from "./bash-tools.shared.js" ;
export type ExecElevatedDefaults = {
enabled : boolean ;
allowed : boolean ;
2026-01-22 05:32:13 +00:00
defaultLevel : "on" | "off" | "ask" | "full" ;
2026-01-14 05:39:59 +00:00
} ;
export type ExecToolDetails =
| {
status : "running" ;
sessionId : string ;
pid? : number ;
startedAt : number ;
cwd? : string ;
tail? : string ;
}
| {
status : "completed" | "failed" ;
exitCode : number | null ;
durationMs : number ;
aggregated : string ;
cwd? : string ;
2026-01-22 00:49:02 +00:00
}
| {
status : "approval-pending" ;
approvalId : string ;
approvalSlug : string ;
expiresAtMs : number ;
host : ExecHost ;
command : string ;
cwd? : string ;
nodeId? : string ;
2026-01-14 05:39:59 +00:00
} ;
2026-02-16 10:34:29 -08:00
function extractScriptTargetFromCommand (
command : string ,
) : { kind : "python" ; relOrAbsPath : string } | { kind : "node" ; relOrAbsPath : string } | null {
const raw = command . trim ( ) ;
if ( ! raw ) {
return null ;
}
// Intentionally simple parsing: we only support common forms like
// python file.py
// python3 -u file.py
// node --experimental-something file.js
// If the command is more complex (pipes, heredocs, quoted paths with spaces), skip preflight.
const pythonMatch = raw . match ( /^\s*(python3?|python)\s+(?:-[^\s]+\s+)*([^\s]+\.py)\b/i ) ;
if ( pythonMatch ? . [ 2 ] ) {
return { kind : "python" , relOrAbsPath : pythonMatch [ 2 ] } ;
}
const nodeMatch = raw . match ( /^\s*(node)\s+(?:--[^\s]+\s+)*([^\s]+\.js)\b/i ) ;
if ( nodeMatch ? . [ 2 ] ) {
return { kind : "node" , relOrAbsPath : nodeMatch [ 2 ] } ;
}
return null ;
}
async function validateScriptFileForShellBleed ( params : {
command : string ;
workdir : string ;
} ) : Promise < void > {
const target = extractScriptTargetFromCommand ( params . command ) ;
if ( ! target ) {
return ;
}
const absPath = path . isAbsolute ( target . relOrAbsPath )
? path . resolve ( target . relOrAbsPath )
: path . resolve ( params . workdir , target . relOrAbsPath ) ;
// Best-effort: only validate if file exists and is reasonably small.
let stat : { isFile ( ) : boolean ; size : number } ;
try {
stat = await fs . stat ( absPath ) ;
} catch {
return ;
}
if ( ! stat . isFile ( ) ) {
return ;
}
if ( stat . size > 512 * 1024 ) {
return ;
}
const content = await fs . readFile ( absPath , "utf-8" ) ;
// Common failure mode: shell env var syntax leaking into Python/JS.
// We deliberately match all-caps/underscore vars to avoid false positives with `$` as a JS identifier.
const envVarRegex = /\$[A-Z_][A-Z0-9_]{1,}/g ;
const first = envVarRegex . exec ( content ) ;
if ( first ) {
const idx = first . index ;
const before = content . slice ( 0 , idx ) ;
const line = before . split ( "\n" ) . length ;
const token = first [ 0 ] ;
throw new Error (
[
` exec preflight: detected likely shell variable injection ( ${ token } ) in ${ target . kind } script: ${ path . basename (
absPath ,
) } : $ { line } . ` ,
target . kind === "python"
? ` In Python, use os.environ.get( ${ JSON . stringify ( token . slice ( 1 ) ) } ) instead of raw ${ token } . `
: ` In Node.js, use process.env[ ${ JSON . stringify ( token . slice ( 1 ) ) } ] instead of raw ${ token } . ` ,
"(If this is inside a string literal on purpose, escape it or restructure the code.)" ,
] . join ( "\n" ) ,
) ;
}
// Another recurring pattern from the issue: shell commands accidentally emitted as JS.
if ( target . kind === "node" ) {
const firstNonEmpty = content
. split ( /\r?\n/ )
. map ( ( l ) = > l . trim ( ) )
. find ( ( l ) = > l . length > 0 ) ;
if ( firstNonEmpty && /^NODE\b/ . test ( firstNonEmpty ) ) {
throw new Error (
` exec preflight: JS file starts with shell syntax ( ${ firstNonEmpty } ). ` +
` This looks like a shell command, not JavaScript. ` ,
) ;
}
}
}
2026-01-14 05:39:59 +00:00
export function createExecTool (
defaults? : ExecToolDefaults ,
2026-02-02 15:45:05 +09:00
// oxlint-disable-next-line typescript/no-explicit-any
2026-01-14 05:39:59 +00:00
) : AgentTool < any , ExecToolDetails > {
2026-02-08 23:59:43 -08:00
const defaultBackgroundMs = clampWithDefault (
2026-01-14 05:39:59 +00:00
defaults ? . backgroundMs ? ? readEnvInt ( "PI_BASH_YIELD_MS" ) ,
10 _000 ,
10 ,
120 _000 ,
) ;
const allowBackground = defaults ? . allowBackground ? ? true ;
const defaultTimeoutSec =
typeof defaults ? . timeoutSec === "number" && defaults . timeoutSec > 0
? defaults . timeoutSec
: 1800 ;
2026-01-19 00:35:39 +00:00
const defaultPathPrepend = normalizePathPrepend ( defaults ? . pathPrepend ) ;
2026-01-21 21:44:28 +00:00
const safeBins = resolveSafeBins ( defaults ? . safeBins ) ;
2026-01-17 05:43:27 +00:00
const notifyOnExit = defaults ? . notifyOnExit !== false ;
2026-02-14 18:32:45 -05:00
const notifyOnExitEmptySuccess = defaults ? . notifyOnExitEmptySuccess === true ;
2026-01-17 05:43:27 +00:00
const notifySessionKey = defaults ? . sessionKey ? . trim ( ) || undefined ;
2026-01-22 00:49:02 +00:00
const approvalRunningNoticeMs = resolveApprovalRunningNoticeMs ( defaults ? . approvalRunningNoticeMs ) ;
2026-01-21 18:55:32 -08:00
// Derive agentId only when sessionKey is an agent session key.
const parsedAgentSession = parseAgentSessionKey ( defaults ? . sessionKey ) ;
const agentId =
defaults ? . agentId ? ?
( parsedAgentSession ? resolveAgentIdFromSessionKey ( defaults ? . sessionKey ) : undefined ) ;
2026-01-14 05:39:59 +00:00
return {
name : "exec" ,
label : "exec" ,
description :
2026-01-17 04:57:04 +00:00
"Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. Use pty=true for TTY-required commands (terminal UIs, coding agents)." ,
2026-01-14 05:39:59 +00:00
parameters : execSchema ,
execute : async ( _toolCallId , args , signal , onUpdate ) = > {
const params = args as {
command : string ;
workdir? : string ;
env? : Record < string , string > ;
yieldMs? : number ;
background? : boolean ;
timeout? : number ;
2026-01-17 04:57:04 +00:00
pty? : boolean ;
2026-01-14 05:39:59 +00:00
elevated? : boolean ;
2026-01-18 04:27:33 +00:00
host? : string ;
security? : string ;
ask? : string ;
node? : string ;
2026-01-14 05:39:59 +00:00
} ;
if ( ! params . command ) {
throw new Error ( "Provide a command to start." ) ;
}
const maxOutput = DEFAULT_MAX_OUTPUT ;
2026-01-17 08:18:27 +00:00
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT ;
2026-01-14 05:39:59 +00:00
const warnings : string [ ] = [ ] ;
2026-02-14 19:42:52 +01:00
let execCommandOverride : string | undefined ;
2026-01-14 05:39:59 +00:00
const backgroundRequested = params . background === true ;
const yieldRequested = typeof params . yieldMs === "number" ;
if ( ! allowBackground && ( backgroundRequested || yieldRequested ) ) {
2026-01-14 14:31:43 +00:00
warnings . push ( "Warning: background execution is disabled; running synchronously." ) ;
2026-01-14 05:39:59 +00:00
}
const yieldWindow = allowBackground
? backgroundRequested
? 0
2026-02-08 23:59:43 -08:00
: clampWithDefault (
params . yieldMs ? ? defaultBackgroundMs ,
defaultBackgroundMs ,
10 ,
120 _000 ,
)
2026-01-14 05:39:59 +00:00
: null ;
const elevatedDefaults = defaults ? . elevated ;
2026-01-22 06:03:15 +00:00
const elevatedAllowed = Boolean ( elevatedDefaults ? . enabled && elevatedDefaults . allowed ) ;
2026-01-22 05:32:13 +00:00
const elevatedDefaultMode =
elevatedDefaults ? . defaultLevel === "full"
? "full"
: elevatedDefaults ? . defaultLevel === "ask"
? "ask"
: elevatedDefaults ? . defaultLevel === "on"
? "ask"
: "off" ;
2026-01-22 06:03:15 +00:00
const effectiveDefaultMode = elevatedAllowed ? elevatedDefaultMode : "off" ;
2026-01-22 05:32:13 +00:00
const elevatedMode =
typeof params . elevated === "boolean"
? params . elevated
? elevatedDefaultMode === "full"
? "full"
: "ask"
: "off"
2026-01-22 06:03:15 +00:00
: effectiveDefaultMode ;
2026-01-22 05:32:13 +00:00
const elevatedRequested = elevatedMode !== "off" ;
2026-01-14 05:39:59 +00:00
if ( elevatedRequested ) {
if ( ! elevatedDefaults ? . enabled || ! elevatedDefaults . allowed ) {
const runtime = defaults ? . sandbox ? "sandboxed" : "direct" ;
const gates : string [ ] = [ ] ;
2026-01-17 17:55:04 +00:00
const contextParts : string [ ] = [ ] ;
const provider = defaults ? . messageProvider ? . trim ( ) ;
const sessionKey = defaults ? . sessionKey ? . trim ( ) ;
2026-01-31 16:19:20 +09:00
if ( provider ) {
contextParts . push ( ` provider= ${ provider } ` ) ;
}
if ( sessionKey ) {
contextParts . push ( ` session= ${ sessionKey } ` ) ;
}
2026-01-14 05:39:59 +00:00
if ( ! elevatedDefaults ? . enabled ) {
2026-01-14 14:31:43 +00:00
gates . push ( "enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)" ) ;
2026-01-14 05:39:59 +00:00
} else {
gates . push (
"allowFrom (tools.elevated.allowFrom.<provider> / agents.list[].tools.elevated.allowFrom.<provider>)" ,
) ;
}
throw new Error (
[
` elevated is not available right now (runtime= ${ runtime } ). ` ,
` Failing gates: ${ gates . join ( ", " ) } ` ,
2026-01-17 17:55:04 +00:00
contextParts . length > 0 ? ` Context: ${ contextParts . join ( " " ) } ` : undefined ,
2026-01-14 05:39:59 +00:00
"Fix-it keys:" ,
"- tools.elevated.enabled" ,
"- tools.elevated.allowFrom.<provider>" ,
"- agents.list[].tools.elevated.enabled" ,
"- agents.list[].tools.elevated.allowFrom.<provider>" ,
2026-01-17 17:55:04 +00:00
]
. filter ( Boolean )
. join ( "\n" ) ,
2026-01-14 05:39:59 +00:00
) ;
}
2026-01-22 06:47:37 +00:00
}
if ( elevatedRequested ) {
2026-01-22 00:49:02 +00:00
logInfo ( ` exec: elevated command ${ truncateMiddle ( params . command , 120 ) } ` ) ;
2026-01-14 05:39:59 +00:00
}
2026-01-18 04:27:33 +00:00
const configuredHost = defaults ? . host ? ? "sandbox" ;
const requestedHost = normalizeExecHost ( params . host ) ? ? null ;
let host : ExecHost = requestedHost ? ? configuredHost ;
if ( ! elevatedRequested && requestedHost && requestedHost !== configuredHost ) {
throw new Error (
` exec host not allowed (requested ${ renderExecHostLabel ( requestedHost ) } ; ` +
` configure tools.exec.host= ${ renderExecHostLabel ( configuredHost ) } to allow). ` ,
) ;
}
if ( elevatedRequested ) {
host = "gateway" ;
}
2026-01-14 05:39:59 +00:00
2026-01-21 03:40:21 +00:00
const configuredSecurity = defaults ? . security ? ? ( host === "sandbox" ? "deny" : "allowlist" ) ;
2026-01-18 04:27:33 +00:00
const requestedSecurity = normalizeExecSecurity ( params . security ) ;
2026-01-18 04:37:15 +00:00
let security = minSecurity ( configuredSecurity , requestedSecurity ? ? configuredSecurity ) ;
2026-01-24 20:55:21 +00:00
if ( elevatedRequested && elevatedMode === "full" ) {
2026-01-18 04:27:33 +00:00
security = "full" ;
}
const configuredAsk = defaults ? . ask ? ? "on-miss" ;
const requestedAsk = normalizeExecAsk ( params . ask ) ;
let ask = maxAsk ( configuredAsk , requestedAsk ? ? configuredAsk ) ;
2026-01-22 05:32:13 +00:00
const bypassApprovals = elevatedRequested && elevatedMode === "full" ;
if ( bypassApprovals ) {
ask = "off" ;
}
2026-01-18 04:27:33 +00:00
const sandbox = host === "sandbox" ? defaults?.sandbox : undefined ;
2026-01-14 14:31:43 +00:00
const rawWorkdir = params . workdir ? . trim ( ) || defaults ? . cwd || process . cwd ( ) ;
2026-01-14 05:39:59 +00:00
let workdir = rawWorkdir ;
let containerWorkdir = sandbox ? . containerWorkdir ;
if ( sandbox ) {
const resolved = await resolveSandboxWorkdir ( {
workdir : rawWorkdir ,
sandbox ,
warnings ,
} ) ;
workdir = resolved . hostWorkdir ;
containerWorkdir = resolved . containerWorkdir ;
} else {
workdir = resolveWorkdir ( rawWorkdir , warnings ) ;
}
const baseEnv = coerceEnv ( process . env ) ;
2026-02-02 02:36:24 +03:00
// Logic: Sandbox gets raw env. Host (gateway/node) must pass validation.
// We validate BEFORE merging to prevent any dangerous vars from entering the stream.
if ( host !== "sandbox" && params . env ) {
validateHostEnv ( params . env ) ;
}
2026-01-14 05:39:59 +00:00
const mergedEnv = params . env ? { . . . baseEnv , . . . params . env } : baseEnv ;
2026-02-02 02:36:24 +03:00
2026-01-14 05:39:59 +00:00
const env = sandbox
? buildSandboxEnv ( {
defaultPath : DEFAULT_PATH ,
paramsEnv : params.env ,
sandboxEnv : sandbox.env ,
containerWorkdir : containerWorkdir ? ? sandbox . containerWorkdir ,
} )
: mergedEnv ;
2026-02-02 02:36:24 +03:00
2026-01-20 14:03:59 +00:00
if ( ! sandbox && host === "gateway" && ! params . env ? . PATH ) {
const shellPath = getShellPathFromLoginShell ( {
env : process.env ,
timeoutMs : resolveShellEnvFallbackTimeoutMs ( process . env ) ,
} ) ;
applyShellPath ( env , shellPath ) ;
}
2026-02-14 20:44:25 +01:00
// `tools.exec.pathPrepend` is only meaningful when exec runs locally (gateway) or in the sandbox.
// Node hosts intentionally ignore request-scoped PATH overrides, so don't pretend this applies.
if ( host === "node" && defaultPathPrepend . length > 0 ) {
warnings . push (
"Warning: tools.exec.pathPrepend is ignored for host=node. Configure PATH on the node host/service instead." ,
) ;
} else {
applyPathPrepend ( env , defaultPathPrepend ) ;
}
2026-01-18 04:27:33 +00:00
if ( host === "node" ) {
2026-01-24 04:53:26 +00:00
const approvals = resolveExecApprovals ( agentId , { security , ask } ) ;
2026-01-19 04:50:07 +00:00
const hostSecurity = minSecurity ( security , approvals . agent . security ) ;
const hostAsk = maxAsk ( ask , approvals . agent . ask ) ;
const askFallback = approvals . agent . askFallback ;
if ( hostSecurity === "deny" ) {
2026-01-18 04:27:33 +00:00
throw new Error ( "exec denied: host=node security=deny" ) ;
}
const boundNode = defaults ? . node ? . trim ( ) ;
const requestedNode = params . node ? . trim ( ) ;
if ( boundNode && requestedNode && boundNode !== requestedNode ) {
throw new Error ( ` exec node not allowed (bound to ${ boundNode } ) ` ) ;
}
const nodeQuery = boundNode || requestedNode ;
const nodes = await listNodes ( { } ) ;
if ( nodes . length === 0 ) {
throw new Error (
2026-01-18 07:44:28 +00:00
"exec host=node requires a paired node (none available). This requires a companion app or node host." ,
2026-01-18 04:27:33 +00:00
) ;
}
let nodeId : string ;
try {
nodeId = resolveNodeIdFromList ( nodes , nodeQuery , ! nodeQuery ) ;
} catch ( err ) {
if ( ! nodeQuery && String ( err ) . includes ( "node required" ) ) {
throw new Error (
"exec host=node requires a node id when multiple nodes are available (set tools.exec.node or exec.node)." ,
2026-01-31 16:03:28 +09:00
{ cause : err } ,
2026-01-18 04:27:33 +00:00
) ;
}
throw err ;
}
const nodeInfo = nodes . find ( ( entry ) = > entry . nodeId === nodeId ) ;
const supportsSystemRun = Array . isArray ( nodeInfo ? . commands )
? nodeInfo ? . commands ? . includes ( "system.run" )
: false ;
if ( ! supportsSystemRun ) {
2026-01-18 07:44:28 +00:00
throw new Error (
"exec host=node requires a node that supports system.run (companion app or node host)." ,
) ;
2026-01-18 04:27:33 +00:00
}
2026-01-18 07:44:28 +00:00
const argv = buildNodeShellCommand ( params . command , nodeInfo ? . platform ) ;
2026-02-02 02:36:24 +03:00
2026-01-19 00:35:39 +00:00
const nodeEnv = params . env ? { . . . params . env } : undefined ;
2026-01-23 00:10:19 +00:00
const baseAllowlistEval = evaluateShellAllowlist ( {
command : params.command ,
allowlist : [ ] ,
safeBins : new Set ( ) ,
cwd : workdir ,
env ,
2026-02-03 09:34:08 -08:00
platform : nodeInfo?.platform ,
2026-01-23 00:10:19 +00:00
} ) ;
let analysisOk = baseAllowlistEval . analysisOk ;
2026-01-21 22:02:17 -08:00
let allowlistSatisfied = false ;
2026-01-23 00:10:19 +00:00
if ( hostAsk === "on-miss" && hostSecurity === "allowlist" && analysisOk ) {
2026-01-21 22:02:17 -08:00
try {
2026-01-31 16:46:45 +09:00
const approvalsSnapshot = await callGatewayTool < { file : string } > (
2026-01-21 22:02:17 -08:00
"exec.approvals.node.get" ,
{ timeoutMs : 10_000 } ,
{ nodeId } ,
2026-01-31 16:03:28 +09:00
) ;
2026-01-21 22:02:17 -08:00
const approvalsFile =
approvalsSnapshot && typeof approvalsSnapshot === "object"
? approvalsSnapshot . file
: undefined ;
if ( approvalsFile && typeof approvalsFile === "object" ) {
const resolved = resolveExecApprovalsFromFile ( {
file : approvalsFile as ExecApprovalsFile ,
agentId ,
overrides : { security : "allowlist" } ,
} ) ;
// Allowlist-only precheck; safe bins are node-local and may diverge.
2026-01-23 00:10:19 +00:00
const allowlistEval = evaluateShellAllowlist ( {
command : params.command ,
2026-01-21 22:02:17 -08:00
allowlist : resolved.allowlist ,
safeBins : new Set ( ) ,
cwd : workdir ,
2026-01-23 00:10:19 +00:00
env ,
2026-02-03 09:34:08 -08:00
platform : nodeInfo?.platform ,
2026-01-23 00:10:19 +00:00
} ) ;
allowlistSatisfied = allowlistEval . allowlistSatisfied ;
analysisOk = allowlistEval . analysisOk ;
2026-01-21 22:02:17 -08:00
}
} catch {
// Fall back to requiring approval if node approvals cannot be fetched.
}
}
const requiresAsk = requiresExecApproval ( {
ask : hostAsk ,
security : hostSecurity ,
2026-01-23 00:10:19 +00:00
analysisOk ,
2026-01-21 22:02:17 -08:00
allowlistSatisfied ,
} ) ;
2026-01-22 00:49:02 +00:00
const commandText = params . command ;
const invokeTimeoutMs = Math . max (
10 _000 ,
( typeof params . timeout === "number" ? params.timeout : defaultTimeoutSec ) * 1000 + 5 _000 ,
) ;
const buildInvokeParams = (
approvedByAsk : boolean ,
approvalDecision : "allow-once" | "allow-always" | null ,
runId? : string ,
) = >
( {
nodeId ,
command : "system.run" ,
params : {
command : argv ,
rawCommand : params.command ,
2026-01-19 11:32:15 +00:00
cwd : workdir ,
2026-01-22 00:49:02 +00:00
env : nodeEnv ,
timeoutMs : typeof params . timeout === "number" ? params . timeout * 1000 : undefined ,
2026-01-21 18:55:32 -08:00
agentId ,
2026-01-22 00:49:02 +00:00
sessionKey : defaults?.sessionKey ,
approved : approvedByAsk ,
approvalDecision : approvalDecision ? ? undefined ,
runId : runId ? ? undefined ,
2026-01-19 08:54:13 +00:00
} ,
2026-01-22 00:49:02 +00:00
idempotencyKey : crypto.randomUUID ( ) ,
} ) satisfies Record < string , unknown > ;
if ( requiresAsk ) {
const approvalId = crypto . randomUUID ( ) ;
const approvalSlug = createApprovalSlug ( approvalId ) ;
2026-02-13 17:49:29 +00:00
const expiresAtMs = Date . now ( ) + DEFAULT_APPROVAL_TIMEOUT_MS ;
2026-01-22 00:49:02 +00:00
const contextKey = ` exec: ${ approvalId } ` ;
const noticeSeconds = Math . max ( 1 , Math . round ( approvalRunningNoticeMs / 1000 ) ) ;
const warningText = warnings . length ? ` ${ warnings . join ( "\n" ) } \ n \ n ` : "" ;
void ( async ( ) = > {
let decision : string | null = null ;
try {
2026-02-13 17:49:29 +00:00
const decisionResult = await callGatewayTool < { decision : string } > (
"exec.approval.request" ,
2026-01-22 00:49:02 +00:00
{ timeoutMs : DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS } ,
2026-02-13 17:49:29 +00:00
{
id : approvalId ,
command : commandText ,
cwd : workdir ,
host : "node" ,
security : hostSecurity ,
ask : hostAsk ,
agentId ,
resolvedPath : undefined ,
sessionKey : defaults?.sessionKey ,
timeoutMs : DEFAULT_APPROVAL_TIMEOUT_MS ,
} ,
2026-01-31 16:03:28 +09:00
) ;
2026-01-31 07:51:26 +00:00
const decisionValue =
2026-01-22 00:49:02 +00:00
decisionResult && typeof decisionResult === "object"
2026-01-31 07:51:26 +00:00
? ( decisionResult as { decision? : unknown } ) . decision
: undefined ;
decision = typeof decisionValue === "string" ? decisionValue : null ;
2026-01-22 00:49:02 +00:00
} catch {
emitExecSystemEvent (
` Exec denied (node= ${ nodeId } id= ${ approvalId } , approval-request-failed): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
return ;
}
let approvedByAsk = false ;
let approvalDecision : "allow-once" | "allow-always" | null = null ;
let deniedReason : string | null = null ;
if ( decision === "deny" ) {
deniedReason = "user-denied" ;
} else if ( ! decision ) {
if ( askFallback === "full" ) {
approvedByAsk = true ;
approvalDecision = "allow-once" ;
} else if ( askFallback === "allowlist" ) {
// Defer allowlist enforcement to the node host.
} else {
deniedReason = "approval-timeout" ;
}
} else if ( decision === "allow-once" ) {
2026-01-19 04:50:07 +00:00
approvedByAsk = true ;
2026-01-20 12:03:18 +00:00
approvalDecision = "allow-once" ;
2026-01-22 00:49:02 +00:00
} else if ( decision === "allow-always" ) {
approvedByAsk = true ;
approvalDecision = "allow-always" ;
2026-01-19 04:50:07 +00:00
}
2026-01-22 00:49:02 +00:00
if ( deniedReason ) {
emitExecSystemEvent (
` Exec denied (node= ${ nodeId } id= ${ approvalId } , ${ deniedReason } ): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
return ;
}
let runningTimer : NodeJS.Timeout | null = null ;
if ( approvalRunningNoticeMs > 0 ) {
runningTimer = setTimeout ( ( ) = > {
emitExecSystemEvent (
` Exec running (node= ${ nodeId } id= ${ approvalId } , > ${ noticeSeconds } s): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
} , approvalRunningNoticeMs ) ;
}
try {
await callGatewayTool (
"node.invoke" ,
{ timeoutMs : invokeTimeoutMs } ,
buildInvokeParams ( approvedByAsk , approvalDecision , approvalId ) ,
) ;
} catch {
emitExecSystemEvent (
` Exec denied (node= ${ nodeId } id= ${ approvalId } , invoke-failed): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
} finally {
2026-01-31 16:19:20 +09:00
if ( runningTimer ) {
clearTimeout ( runningTimer ) ;
}
2026-01-22 00:49:02 +00:00
}
} ) ( ) ;
return {
content : [
{
type : "text" ,
text :
` ${ warningText } Approval required (id ${ approvalSlug } ). ` +
"Approve to run; updates will arrive after completion." ,
} ,
] ,
details : {
status : "approval-pending" ,
approvalId ,
approvalSlug ,
expiresAtMs ,
host : "node" ,
command : commandText ,
cwd : workdir ,
nodeId ,
} ,
} ;
2026-01-19 04:50:07 +00:00
}
2026-01-22 00:49:02 +00:00
const startedAt = Date . now ( ) ;
2026-01-31 07:51:26 +00:00
const raw = await callGatewayTool (
"node.invoke" ,
{ timeoutMs : invokeTimeoutMs } ,
buildInvokeParams ( false , null ) ,
) ;
const payload =
raw && typeof raw === "object" ? ( raw as { payload? : unknown } ) . payload : undefined ;
const payloadObj =
payload && typeof payload === "object" ? ( payload as Record < string , unknown > ) : { } ;
const stdout = typeof payloadObj . stdout === "string" ? payloadObj . stdout : "" ;
const stderr = typeof payloadObj . stderr === "string" ? payloadObj . stderr : "" ;
const errorText = typeof payloadObj . error === "string" ? payloadObj . error : "" ;
const success = typeof payloadObj . success === "boolean" ? payloadObj.success : false ;
const exitCode = typeof payloadObj . exitCode === "number" ? payloadObj.exitCode : null ;
2026-01-18 04:27:33 +00:00
return {
content : [
{
type : "text" ,
2026-01-31 07:51:26 +00:00
text : stdout || stderr || errorText || "" ,
2026-01-18 04:27:33 +00:00
} ,
] ,
details : {
2026-01-31 07:51:26 +00:00
status : success ? "completed" : "failed" ,
exitCode ,
2026-01-18 04:27:33 +00:00
durationMs : Date.now ( ) - startedAt ,
2026-01-31 07:51:26 +00:00
aggregated : [ stdout , stderr , errorText ] . filter ( Boolean ) . join ( "\n" ) ,
2026-01-18 04:27:33 +00:00
cwd : workdir ,
} satisfies ExecToolDetails ,
} ;
}
2026-01-22 05:32:13 +00:00
if ( host === "gateway" && ! bypassApprovals ) {
2026-01-24 04:53:26 +00:00
const approvals = resolveExecApprovals ( agentId , { security , ask } ) ;
2026-01-18 04:27:33 +00:00
const hostSecurity = minSecurity ( security , approvals . agent . security ) ;
const hostAsk = maxAsk ( ask , approvals . agent . ask ) ;
const askFallback = approvals . agent . askFallback ;
if ( hostSecurity === "deny" ) {
throw new Error ( "exec denied: host=gateway security=deny" ) ;
}
2026-01-23 00:10:19 +00:00
const allowlistEval = evaluateShellAllowlist ( {
command : params.command ,
2026-01-21 22:02:17 -08:00
allowlist : approvals.allowlist ,
safeBins ,
cwd : workdir ,
2026-01-23 00:10:19 +00:00
env ,
2026-02-03 09:34:08 -08:00
platform : process.platform ,
2026-01-21 22:02:17 -08:00
} ) ;
const allowlistMatches = allowlistEval . allowlistMatches ;
2026-01-23 00:10:19 +00:00
const analysisOk = allowlistEval . analysisOk ;
2026-01-21 22:02:17 -08:00
const allowlistSatisfied =
2026-01-23 00:10:19 +00:00
hostSecurity === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false ;
2026-01-21 22:02:17 -08:00
const requiresAsk = requiresExecApproval ( {
ask : hostAsk ,
security : hostSecurity ,
2026-01-23 00:10:19 +00:00
analysisOk ,
2026-01-21 22:02:17 -08:00
allowlistSatisfied ,
} ) ;
2026-01-18 04:27:33 +00:00
if ( requiresAsk ) {
2026-01-22 00:49:02 +00:00
const approvalId = crypto . randomUUID ( ) ;
const approvalSlug = createApprovalSlug ( approvalId ) ;
2026-02-13 17:49:29 +00:00
const expiresAtMs = Date . now ( ) + DEFAULT_APPROVAL_TIMEOUT_MS ;
2026-01-22 00:49:02 +00:00
const contextKey = ` exec: ${ approvalId } ` ;
2026-01-23 00:10:19 +00:00
const resolvedPath = allowlistEval . segments [ 0 ] ? . resolution ? . resolvedPath ;
2026-01-22 00:49:02 +00:00
const noticeSeconds = Math . max ( 1 , Math . round ( approvalRunningNoticeMs / 1000 ) ) ;
const commandText = params . command ;
const effectiveTimeout =
typeof params . timeout === "number" ? params.timeout : defaultTimeoutSec ;
const warningText = warnings . length ? ` ${ warnings . join ( "\n" ) } \ n \ n ` : "" ;
void ( async ( ) = > {
let decision : string | null = null ;
try {
2026-02-13 17:49:29 +00:00
const decisionResult = await callGatewayTool < { decision : string } > (
"exec.approval.request" ,
2026-01-22 00:49:02 +00:00
{ timeoutMs : DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS } ,
2026-02-13 17:49:29 +00:00
{
id : approvalId ,
command : commandText ,
cwd : workdir ,
host : "gateway" ,
security : hostSecurity ,
ask : hostAsk ,
agentId ,
resolvedPath ,
sessionKey : defaults?.sessionKey ,
timeoutMs : DEFAULT_APPROVAL_TIMEOUT_MS ,
} ,
2026-01-31 16:03:28 +09:00
) ;
2026-01-31 07:51:26 +00:00
const decisionValue =
2026-01-22 00:49:02 +00:00
decisionResult && typeof decisionResult === "object"
2026-01-31 07:51:26 +00:00
? ( decisionResult as { decision? : unknown } ) . decision
: undefined ;
decision = typeof decisionValue === "string" ? decisionValue : null ;
2026-01-22 00:49:02 +00:00
} catch {
emitExecSystemEvent (
` Exec denied (gateway id= ${ approvalId } , approval-request-failed): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
return ;
}
let approvedByAsk = false ;
let deniedReason : string | null = null ;
if ( decision === "deny" ) {
deniedReason = "user-denied" ;
} else if ( ! decision ) {
if ( askFallback === "full" ) {
approvedByAsk = true ;
} else if ( askFallback === "allowlist" ) {
2026-01-23 00:10:19 +00:00
if ( ! analysisOk || ! allowlistSatisfied ) {
2026-01-22 00:49:02 +00:00
deniedReason = "approval-timeout (allowlist-miss)" ;
} else {
approvedByAsk = true ;
}
} else {
deniedReason = "approval-timeout" ;
2026-01-18 04:27:33 +00:00
}
2026-01-22 00:49:02 +00:00
} else if ( decision === "allow-once" ) {
2026-01-18 07:44:28 +00:00
approvedByAsk = true ;
2026-01-22 00:49:02 +00:00
} else if ( decision === "allow-always" ) {
approvedByAsk = true ;
if ( hostSecurity === "allowlist" ) {
2026-01-23 00:10:19 +00:00
for ( const segment of allowlistEval . segments ) {
2026-01-22 00:49:02 +00:00
const pattern = segment . resolution ? . resolvedPath ? ? "" ;
if ( pattern ) {
2026-01-21 18:55:32 -08:00
addAllowlistEntry ( approvals . file , agentId , pattern ) ;
2026-01-22 00:49:02 +00:00
}
2026-01-21 21:44:28 +00:00
}
2026-01-18 07:44:28 +00:00
}
2026-01-18 04:27:33 +00:00
}
2026-01-22 00:49:02 +00:00
if (
hostSecurity === "allowlist" &&
2026-01-23 00:10:19 +00:00
( ! analysisOk || ! allowlistSatisfied ) &&
2026-01-22 00:49:02 +00:00
! approvedByAsk
) {
deniedReason = deniedReason ? ? "allowlist-miss" ;
}
if ( deniedReason ) {
emitExecSystemEvent (
` Exec denied (gateway id= ${ approvalId } , ${ deniedReason } ): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
return ;
}
if ( allowlistMatches . length > 0 ) {
const seen = new Set < string > ( ) ;
for ( const match of allowlistMatches ) {
2026-01-31 16:19:20 +09:00
if ( seen . has ( match . pattern ) ) {
continue ;
}
2026-01-22 00:49:02 +00:00
seen . add ( match . pattern ) ;
recordAllowlistUse (
approvals . file ,
2026-01-21 18:55:32 -08:00
agentId ,
2026-01-22 00:49:02 +00:00
match ,
commandText ,
resolvedPath ? ? undefined ,
) ;
}
}
let run : ExecProcessHandle | null = null ;
try {
run = await runExecProcess ( {
command : commandText ,
workdir ,
env ,
sandbox : undefined ,
containerWorkdir : null ,
usePty : params.pty === true && ! sandbox ,
warnings ,
maxOutput ,
pendingMaxOutput ,
notifyOnExit : false ,
2026-02-14 18:32:45 -05:00
notifyOnExitEmptySuccess : false ,
2026-01-22 00:49:02 +00:00
scopeKey : defaults?.scopeKey ,
sessionKey : notifySessionKey ,
timeoutSec : effectiveTimeout ,
} ) ;
} catch {
emitExecSystemEvent (
` Exec denied (gateway id= ${ approvalId } , spawn-failed): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
return ;
}
markBackgrounded ( run . session ) ;
let runningTimer : NodeJS.Timeout | null = null ;
if ( approvalRunningNoticeMs > 0 ) {
runningTimer = setTimeout ( ( ) = > {
emitExecSystemEvent (
` Exec running (gateway id= ${ approvalId } , session= ${ run ? . session . id } , > ${ noticeSeconds } s): ${ commandText } ` ,
{ sessionKey : notifySessionKey , contextKey } ,
) ;
} , approvalRunningNoticeMs ) ;
}
const outcome = await run . promise ;
2026-01-31 16:19:20 +09:00
if ( runningTimer ) {
clearTimeout ( runningTimer ) ;
}
2026-01-22 00:49:02 +00:00
const output = normalizeNotifyOutput (
tail ( outcome . aggregated || "" , DEFAULT_NOTIFY_TAIL_CHARS ) ,
) ;
const exitLabel = outcome . timedOut ? "timeout" : ` code ${ outcome . exitCode ? ? "?" } ` ;
const summary = output
? ` Exec finished (gateway id= ${ approvalId } , session= ${ run . session . id } , ${ exitLabel } ) \ n ${ output } `
: ` Exec finished (gateway id= ${ approvalId } , session= ${ run . session . id } , ${ exitLabel } ) ` ;
emitExecSystemEvent ( summary , { sessionKey : notifySessionKey , contextKey } ) ;
} ) ( ) ;
return {
content : [
{
type : "text" ,
text :
2026-02-02 15:37:05 +09:00
` ${ warningText } Approval required (id ${ approvalSlug } ). ` +
2026-01-22 00:49:02 +00:00
"Approve to run; updates will arrive after completion." ,
} ,
] ,
details : {
status : "approval-pending" ,
approvalId ,
approvalSlug ,
expiresAtMs ,
host : "gateway" ,
command : params.command ,
cwd : workdir ,
} ,
} ;
2026-01-18 07:44:28 +00:00
}
2026-01-23 00:10:19 +00:00
if ( hostSecurity === "allowlist" && ( ! analysisOk || ! allowlistSatisfied ) ) {
2026-01-18 07:44:28 +00:00
throw new Error ( "exec denied: allowlist miss" ) ;
2026-01-18 04:27:33 +00:00
}
2026-02-14 19:59:03 +01:00
// If allowlist uses safeBins, sanitize only those stdin-only segments:
// disable glob/var expansion by forcing argv tokens to be literal via single-quoting.
2026-02-14 19:42:52 +01:00
if (
hostSecurity === "allowlist" &&
analysisOk &&
allowlistSatisfied &&
2026-02-14 19:59:03 +01:00
allowlistEval . segmentSatisfiedBy . some ( ( by ) = > by === "safeBins" )
2026-02-14 19:42:52 +01:00
) {
2026-02-14 19:59:03 +01:00
const safe = buildSafeBinsShellCommand ( {
2026-02-14 19:42:52 +01:00
command : params.command ,
2026-02-14 19:59:03 +01:00
segments : allowlistEval.segments ,
segmentSatisfiedBy : allowlistEval.segmentSatisfiedBy ,
2026-02-14 19:42:52 +01:00
platform : process.platform ,
} ) ;
if ( ! safe . ok || ! safe . command ) {
2026-02-14 19:59:03 +01:00
// Fallback: quote everything (safe, but may change glob behavior).
const fallback = buildSafeShellCommand ( {
command : params.command ,
platform : process.platform ,
} ) ;
if ( ! fallback . ok || ! fallback . command ) {
throw new Error (
` exec denied: safeBins sanitize failed ( ${ safe . reason ? ? "unknown" } ) ` ,
) ;
}
warnings . push (
"Warning: safeBins hardening used fallback quoting due to parser mismatch." ,
) ;
execCommandOverride = fallback . command ;
} else {
warnings . push (
"Warning: safeBins hardening disabled glob/variable expansion for stdin-only segments." ,
) ;
execCommandOverride = safe . command ;
2026-02-14 19:42:52 +01:00
}
}
2026-01-21 21:44:28 +00:00
if ( allowlistMatches . length > 0 ) {
const seen = new Set < string > ( ) ;
for ( const match of allowlistMatches ) {
2026-01-31 16:19:20 +09:00
if ( seen . has ( match . pattern ) ) {
continue ;
}
2026-01-21 21:44:28 +00:00
seen . add ( match . pattern ) ;
recordAllowlistUse (
approvals . file ,
2026-01-21 18:55:32 -08:00
agentId ,
2026-01-21 21:44:28 +00:00
match ,
params . command ,
2026-01-23 00:10:19 +00:00
allowlistEval . segments [ 0 ] ? . resolution ? . resolvedPath ,
2026-01-21 21:44:28 +00:00
) ;
}
2026-01-18 04:27:33 +00:00
}
}
2026-01-22 00:49:02 +00:00
const effectiveTimeout =
typeof params . timeout === "number" ? params.timeout : defaultTimeoutSec ;
2026-01-23 06:26:30 +00:00
const getWarningText = ( ) = > ( warnings . length ? ` ${ warnings . join ( "\n" ) } \ n \ n ` : "" ) ;
2026-01-17 04:57:04 +00:00
const usePty = params . pty === true && ! sandbox ;
2026-02-16 10:34:29 -08:00
// Preflight: catch a common model failure mode (shell syntax leaking into Python/JS sources)
// before we execute and burn tokens in cron loops.
await validateScriptFileForShellBleed ( { command : params.command , workdir } ) ;
2026-01-22 00:49:02 +00:00
const run = await runExecProcess ( {
2026-01-14 05:39:59 +00:00
command : params.command ,
2026-02-14 19:42:52 +01:00
execCommand : execCommandOverride ,
2026-01-22 00:49:02 +00:00
workdir ,
env ,
sandbox ,
containerWorkdir ,
usePty ,
warnings ,
maxOutput ,
pendingMaxOutput ,
notifyOnExit ,
2026-02-14 18:32:45 -05:00
notifyOnExitEmptySuccess ,
2026-01-14 05:39:59 +00:00
scopeKey : defaults?.scopeKey ,
2026-01-17 05:43:27 +00:00
sessionKey : notifySessionKey ,
2026-01-22 00:49:02 +00:00
timeoutSec : effectiveTimeout ,
onUpdate ,
} ) ;
2026-01-14 05:39:59 +00:00
let yielded = false ;
let yieldTimer : NodeJS.Timeout | null = null ;
2026-01-17 03:52:37 +00:00
2026-01-16 10:43:08 +00:00
// Tool-call abort should not kill backgrounded sessions; timeouts still must.
const onAbortSignal = ( ) = > {
2026-01-31 16:19:20 +09:00
if ( yielded || run . session . backgrounded ) {
return ;
}
2026-01-22 00:49:02 +00:00
run . kill ( ) ;
2026-01-16 10:43:08 +00:00
} ;
2026-01-31 16:19:20 +09:00
if ( signal ? . aborted ) {
onAbortSignal ( ) ;
} else if ( signal ) {
2026-01-16 10:43:08 +00:00
signal . addEventListener ( "abort" , onAbortSignal , { once : true } ) ;
2026-01-14 05:39:59 +00:00
}
2026-01-14 14:31:43 +00:00
return new Promise < AgentToolResult < ExecToolDetails > > ( ( resolve , reject ) = > {
2026-01-22 00:49:02 +00:00
const resolveRunning = ( ) = >
resolve ( {
content : [
{
type : "text" ,
2026-02-02 15:37:05 +09:00
text : ` ${ getWarningText ( ) } Command still running (session ${ run . session . id } , pid ${
run . session . pid ? ? "n/a"
} ) . Use process ( list / poll / log / write / kill / clear / remove ) for follow - up . ` ,
2026-01-14 14:31:43 +00:00
} ,
2026-01-22 00:49:02 +00:00
] ,
details : {
status : "running" ,
sessionId : run.session.id ,
pid : run.session.pid ? ? undefined ,
startedAt : run.startedAt ,
cwd : run.session.cwd ,
tail : run.session.tail ,
} ,
} ) ;
2026-01-14 14:31:43 +00:00
const onYieldNow = ( ) = > {
2026-01-31 16:19:20 +09:00
if ( yieldTimer ) {
clearTimeout ( yieldTimer ) ;
}
if ( yielded ) {
return ;
}
2026-01-14 14:31:43 +00:00
yielded = true ;
2026-01-22 00:49:02 +00:00
markBackgrounded ( run . session ) ;
2026-01-14 14:31:43 +00:00
resolveRunning ( ) ;
} ;
if ( allowBackground && yieldWindow !== null ) {
if ( yieldWindow === 0 ) {
onYieldNow ( ) ;
} else {
yieldTimer = setTimeout ( ( ) = > {
2026-01-31 16:19:20 +09:00
if ( yielded ) {
return ;
}
2026-01-14 14:31:43 +00:00
yielded = true ;
2026-01-22 00:49:02 +00:00
markBackgrounded ( run . session ) ;
2026-01-14 14:31:43 +00:00
resolveRunning ( ) ;
} , yieldWindow ) ;
2026-01-14 05:39:59 +00:00
}
2026-01-14 14:31:43 +00:00
}
2026-01-14 05:39:59 +00:00
2026-01-22 00:49:02 +00:00
run . promise
. then ( ( outcome ) = > {
2026-01-31 16:19:20 +09:00
if ( yieldTimer ) {
clearTimeout ( yieldTimer ) ;
}
if ( yielded || run . session . backgrounded ) {
return ;
}
2026-01-22 00:49:02 +00:00
if ( outcome . status === "failed" ) {
reject ( new Error ( outcome . reason ? ? "Command failed." ) ) ;
return ;
}
2026-01-14 14:31:43 +00:00
resolve ( {
content : [
{
type : "text" ,
2026-01-23 06:26:30 +00:00
text : ` ${ getWarningText ( ) } ${ outcome . aggregated || "(no output)" } ` ,
2026-01-14 05:39:59 +00:00
} ,
2026-01-14 14:31:43 +00:00
] ,
details : {
status : "completed" ,
2026-01-22 00:49:02 +00:00
exitCode : outcome.exitCode ? ? 0 ,
durationMs : outcome.durationMs ,
aggregated : outcome.aggregated ,
cwd : run.session.cwd ,
2026-01-14 14:31:43 +00:00
} ,
2026-01-22 00:49:02 +00:00
} ) ;
} )
. catch ( ( err ) = > {
2026-01-31 16:19:20 +09:00
if ( yieldTimer ) {
clearTimeout ( yieldTimer ) ;
}
if ( yielded || run . session . backgrounded ) {
return ;
}
2026-01-22 00:49:02 +00:00
reject ( err as Error ) ;
2026-01-17 04:57:04 +00:00
} ) ;
2026-01-14 14:31:43 +00:00
} ) ;
2026-01-14 05:39:59 +00:00
} ,
} ;
}
export const execTool = createExecTool ( ) ;