2026-02-16 10:34:29 -08:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
2026-02-19 15:41:24 +01:00
import type { AgentTool , AgentToolResult } from "@mariozechner/pi-agent-core" ;
2026-02-22 13:18:17 +01:00
import { type ExecHost , maxAsk , minSecurity } from "../infra/exec-approvals.js" ;
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.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-19 14:21:07 +01:00
import { markBackgrounded } from "./bash-process-registry.js" ;
import { processGatewayAllowlist } from "./bash-tools.exec-host-gateway.js" ;
import { executeNodeHostCommand } from "./bash-tools.exec-host-node.js" ;
2026-01-17 04:57:04 +00:00
import {
2026-02-13 17:49:29 +00:00
DEFAULT_MAX_OUTPUT ,
DEFAULT_PATH ,
DEFAULT_PENDING_MAX_OUTPUT ,
applyPathPrepend ,
applyShellPath ,
normalizeExecAsk ,
normalizeExecHost ,
normalizeExecSecurity ,
normalizePathPrepend ,
renderExecHostLabel ,
resolveApprovalRunningNoticeMs ,
runExecProcess ,
execSchema ,
validateHostEnv ,
} from "./bash-tools.exec-runtime.js" ;
2026-02-19 15:41:24 +01:00
import type {
ExecElevatedDefaults ,
ExecToolDefaults ,
ExecToolDetails ,
} from "./bash-tools.exec-types.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-02-19 15:33:25 +01:00
import { assertSandboxPath } from "./sandbox-paths.js" ;
2026-01-14 05:39:59 +00:00
export type { BashSandboxConfig } from "./bash-tools.shared.js" ;
2026-02-19 14:21:07 +01:00
export type {
ExecElevatedDefaults ,
ExecToolDefaults ,
ExecToolDetails ,
} from "./bash-tools.exec-types.js" ;
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 {
2026-02-19 15:33:25 +01:00
await assertSandboxPath ( {
filePath : absPath ,
cwd : params.workdir ,
root : params.workdir ,
} ) ;
2026-02-16 10:34:29 -08:00
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-02-22 13:18:17 +01:00
const {
safeBins ,
safeBinProfiles ,
trustedSafeBinDirs ,
unprofiledSafeBins ,
unprofiledInterpreterSafeBins ,
} = resolveExecSafeBinRuntimePolicy ( {
local : {
safeBins : defaults?.safeBins ,
2026-02-22 22:42:29 +01:00
safeBinTrustedDirs : defaults?.safeBinTrustedDirs ,
2026-02-22 13:18:17 +01:00
safeBinProfiles : defaults?.safeBinProfiles ,
} ,
} ) ;
2026-02-22 12:57:53 +01:00
if ( unprofiledSafeBins . length > 0 ) {
logInfo (
` exec: ignoring unprofiled safeBins entries ( ${ unprofiledSafeBins . toSorted ( ) . join ( ", " ) } ); use allowlist or define tools.exec.safeBinProfiles.<bin> ` ,
) ;
}
2026-02-22 13:18:17 +01:00
if ( unprofiledInterpreterSafeBins . length > 0 ) {
logInfo (
` exec: interpreter/runtime binaries in safeBins ( ${ unprofiledInterpreterSafeBins . join ( ", " ) } ) are unsafe without explicit hardened profiles; prefer allowlist entries ` ,
) ;
}
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-02-16 21:47:18 -05: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-02-23 01:48:09 +01:00
const configuredHost = defaults ? . host ? ? "sandbox" ;
2026-02-22 01:49:10 -07:00
const sandboxHostConfigured = defaults ? . host === "sandbox" ;
2026-01-18 04:27:33 +00:00
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-02-22 01:49:10 -07:00
if (
host === "sandbox" &&
! sandbox &&
( sandboxHostConfigured || requestedHost === "sandbox" )
) {
throw new Error (
[
"exec host=sandbox is configured, but sandbox runtime is unavailable for this session." ,
'Enable sandbox mode (`agents.defaults.sandbox.mode="non-main"` or `"all"`) or set tools.exec.host to "gateway"/"node".' ,
] . join ( "\n" ) ,
) ;
}
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-02-19 14:21:07 +01:00
return executeNodeHostCommand ( {
2026-01-23 00:10:19 +00:00
command : params.command ,
2026-02-19 14:21:07 +01:00
workdir ,
2026-01-23 00:10:19 +00:00
env ,
2026-02-19 14:21:07 +01:00
requestedEnv : params.env ,
requestedNode : params.node?.trim ( ) ,
boundNode : defaults?.node?.trim ( ) ,
sessionKey : defaults?.sessionKey ,
agentId ,
security ,
ask ,
timeoutSec : params.timeout ,
defaultTimeoutSec ,
approvalRunningNoticeMs ,
warnings ,
notifySessionKey ,
trustedSafeBinDirs ,
2026-01-21 22:02:17 -08:00
} ) ;
2026-01-18 04:27:33 +00:00
}
2026-01-22 05:32:13 +00:00
if ( host === "gateway" && ! bypassApprovals ) {
2026-02-19 14:21:07 +01:00
const gatewayResult = await processGatewayAllowlist ( {
2026-01-23 00:10:19 +00:00
command : params.command ,
2026-02-19 14:21:07 +01:00
workdir ,
2026-01-23 00:10:19 +00:00
env ,
2026-02-19 14:21:07 +01:00
pty : params.pty === true && ! sandbox ,
timeoutSec : params.timeout ,
defaultTimeoutSec ,
security ,
ask ,
safeBins ,
2026-02-22 12:57:53 +01:00
safeBinProfiles ,
2026-02-19 14:21:07 +01:00
agentId ,
sessionKey : defaults?.sessionKey ,
scopeKey : defaults?.scopeKey ,
warnings ,
notifySessionKey ,
approvalRunningNoticeMs ,
maxOutput ,
pendingMaxOutput ,
trustedSafeBinDirs ,
2026-01-21 22:02:17 -08:00
} ) ;
2026-02-19 14:21:07 +01:00
if ( gatewayResult . pendingResult ) {
return gatewayResult . pendingResult ;
2026-01-18 04:27:33 +00:00
}
2026-02-19 14:21:07 +01:00
execCommandOverride = gatewayResult . execCommandOverride ;
2026-01-18 04:27:33 +00:00
}
2026-02-22 23:02:17 +01:00
const explicitTimeoutSec = typeof params . timeout === "number" ? params.timeout : null ;
const backgroundTimeoutBypass =
allowBackground && explicitTimeoutSec === null && ( backgroundRequested || yieldRequested ) ;
const effectiveTimeout = backgroundTimeoutBypass
? null
: ( explicitTimeoutSec ? ? 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 ( ) ;