import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import { APPROVALS_SCOPE, WRITE_SCOPE } from "../gateway/operator-scopes.js"; import { requiresExecApproval, resolveExecApprovalAllowedDecisions, } from "../infra/exec-approvals.js"; import { buildExecApprovalRequesterContext, buildExecApprovalTurnSourceContext, registerExecApprovalRequestForHostOrThrow, } from "./bash-tools.exec-approval-request.js"; import { analyzeNodeApprovalRequirement, buildNodeSystemRunInvoke, formatNodeRunToolResult, invokeNodeSystemRunDirect, prepareNodeSystemRun, resolveNodeExecutionTarget, shouldSkipNodeApprovalPrepare, } from "./bash-tools.exec-host-node-phases.js"; import type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js"; import * as execHostShared from "./bash-tools.exec-host-shared.js"; import { DEFAULT_NOTIFY_TAIL_CHARS, createApprovalSlug, normalizeNotifyOutput, } from "./bash-tools.exec-runtime.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js"; import { callGatewayTool } from "./tools/gateway.js"; export type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js"; const APPROVED_NODE_INVOKE_SCOPES = [WRITE_SCOPE, APPROVALS_SCOPE]; export async function executeNodeHostCommand( params: ExecuteNodeHostCommandParams, ): Promise> { const { hostSecurity, hostAsk, askFallback } = execHostShared.resolveExecHostApprovalContext({ agentId: params.agentId, security: params.security, ask: params.ask, host: "node", }); const target = await resolveNodeExecutionTarget(params); if ( shouldSkipNodeApprovalPrepare({ hostSecurity, hostAsk, strictInlineEval: params.strictInlineEval, }) ) { return await invokeNodeSystemRunDirect({ request: params, target }); } const prepared = await prepareNodeSystemRun({ request: params, target }); const approvalAnalysis = await analyzeNodeApprovalRequirement({ request: params, target, prepared, hostSecurity, hostAsk, }); const { analysisOk, allowlistSatisfied, durableApprovalSatisfied, inlineEvalHit } = approvalAnalysis; const requiresAsk = requiresExecApproval({ ask: hostAsk, security: hostSecurity, analysisOk, allowlistSatisfied, durableApprovalSatisfied, }) || inlineEvalHit !== null; let inlineApprovedByAsk = false; let inlineApprovalDecision: "allow-once" | "allow-always" | null = null; let inlineApprovalId: string | undefined; if (requiresAsk) { const requestArgs = execHostShared.buildDefaultExecApprovalRequestArgs({ warnings: params.warnings, approvalRunningNoticeMs: params.approvalRunningNoticeMs, createApprovalSlug, turnSourceChannel: params.turnSourceChannel, turnSourceAccountId: params.turnSourceAccountId, }); const registerNodeApproval = async (approvalId: string) => await registerExecApprovalRequestForHostOrThrow({ approvalId, systemRunPlan: prepared.plan, env: target.env, workdir: prepared.cwd, host: "node", nodeId: target.nodeId, security: hostSecurity, ask: hostAsk, ...buildExecApprovalRequesterContext({ agentId: prepared.agentId, sessionKey: prepared.sessionKey, }), ...buildExecApprovalTurnSourceContext(params), }); const { approvalId, approvalSlug, warningText, expiresAtMs, preResolvedDecision, initiatingSurface, sentApproverDms, unavailableReason, } = await execHostShared.createAndRegisterDefaultExecApprovalRequest({ ...requestArgs, register: registerNodeApproval, }); if ( execHostShared.shouldResolveExecApprovalUnavailableInline({ trigger: params.trigger, unavailableReason, preResolvedDecision, }) ) { const { baseDecision, approvedByAsk, deniedReason } = execHostShared.createExecApprovalDecisionState({ decision: preResolvedDecision, askFallback, }); const strictInlineEvalDecision = execHostShared.enforceStrictInlineEvalApprovalBoundary({ baseDecision, approvedByAsk, deniedReason, requiresInlineEvalApproval: inlineEvalHit !== null, }); if (strictInlineEvalDecision.deniedReason || !strictInlineEvalDecision.approvedByAsk) { throw new Error( execHostShared.buildHeadlessExecApprovalDeniedMessage({ trigger: params.trigger, host: "node", security: hostSecurity, ask: hostAsk, askFallback, }), ); } inlineApprovedByAsk = strictInlineEvalDecision.approvedByAsk; inlineApprovalDecision = strictInlineEvalDecision.approvedByAsk ? "allow-once" : null; inlineApprovalId = approvalId; } else { const followupTarget = execHostShared.buildExecApprovalFollowupTarget({ approvalId, sessionKey: params.notifySessionKey ?? params.sessionKey, turnSourceChannel: params.turnSourceChannel, turnSourceTo: params.turnSourceTo, turnSourceAccountId: params.turnSourceAccountId, turnSourceThreadId: params.turnSourceThreadId, }); void (async () => { const decision = await execHostShared.resolveApprovalDecisionOrUndefined({ approvalId, preResolvedDecision, onFailure: () => void execHostShared.sendExecApprovalFollowupResult( followupTarget, `Exec denied (node=${target.nodeId} id=${approvalId}, approval-request-failed): ${params.command}`, ), }); if (decision === undefined) { return; } const { baseDecision, approvedByAsk: initialApprovedByAsk, deniedReason: initialDeniedReason, } = execHostShared.createExecApprovalDecisionState({ decision, askFallback, }); let approvedByAsk = initialApprovedByAsk; let approvalDecision: "allow-once" | "allow-always" | null = null; let deniedReason = initialDeniedReason; if (baseDecision.timedOut && askFallback === "full" && approvedByAsk) { approvalDecision = "allow-once"; } else if (decision === "allow-once") { approvedByAsk = true; approvalDecision = "allow-once"; } else if (decision === "allow-always") { approvedByAsk = true; approvalDecision = "allow-always"; } ({ approvedByAsk, deniedReason } = execHostShared.enforceStrictInlineEvalApprovalBoundary({ baseDecision, approvedByAsk, deniedReason, requiresInlineEvalApproval: inlineEvalHit !== null, })); if (deniedReason) { approvalDecision = null; } if (deniedReason) { await execHostShared.sendExecApprovalFollowupResult( followupTarget, `Exec denied (node=${target.nodeId} id=${approvalId}, ${deniedReason}): ${params.command}`, ); return; } try { const raw = await callGatewayTool( "node.invoke", { timeoutMs: target.invokeTimeoutMs }, buildNodeSystemRunInvoke({ target, command: prepared.argv, rawCommand: prepared.rawCommand, cwd: prepared.cwd, agentId: prepared.agentId, sessionKey: prepared.sessionKey, approved: approvedByAsk, approvalDecision: approvalDecision === "allow-always" && inlineEvalHit !== null ? "allow-once" : approvalDecision, runId: approvalId, suppressNotifyOnExit: true, notifyOnExit: params.notifyOnExit, systemRunPlan: prepared.plan, }), { scopes: APPROVED_NODE_INVOKE_SCOPES }, ); const payload = raw?.payload && typeof raw.payload === "object" ? (raw.payload as { stdout?: string; stderr?: string; error?: string | null; exitCode?: number | null; timedOut?: boolean; }) : {}; const combined = [payload.stdout, payload.stderr, payload.error] .filter(Boolean) .join("\n"); const output = normalizeNotifyOutput(combined.slice(-DEFAULT_NOTIFY_TAIL_CHARS)); const exitLabel = payload.timedOut ? "timeout" : `code ${payload.exitCode ?? "?"}`; const summary = output ? `Exec finished (node=${target.nodeId} id=${approvalId}, ${exitLabel})\n${output}` : `Exec finished (node=${target.nodeId} id=${approvalId}, ${exitLabel})`; await execHostShared.sendExecApprovalFollowupResult(followupTarget, summary); } catch { await execHostShared.sendExecApprovalFollowupResult( followupTarget, `Exec denied (node=${target.nodeId} id=${approvalId}, invoke-failed): ${params.command}`, ); } })(); return execHostShared.buildExecApprovalPendingToolResult({ host: "node", command: params.command, cwd: params.workdir, warningText, approvalId, approvalSlug, expiresAtMs, initiatingSurface, sentApproverDms, unavailableReason, allowedDecisions: resolveExecApprovalAllowedDecisions({ ask: hostAsk }), nodeId: target.nodeId, }); } } const startedAt = Date.now(); const invoke = buildNodeSystemRunInvoke({ target, command: prepared.argv, rawCommand: prepared.rawCommand, cwd: prepared.cwd, agentId: prepared.agentId, sessionKey: prepared.sessionKey, approved: inlineApprovedByAsk, approvalDecision: inlineApprovalDecision, runId: inlineApprovalId, notifyOnExit: params.notifyOnExit, systemRunPlan: prepared.plan, }); const raw = inlineApprovedByAsk && inlineApprovalId ? await callGatewayTool("node.invoke", { timeoutMs: target.invokeTimeoutMs }, invoke, { scopes: APPROVED_NODE_INVOKE_SCOPES, }) : await callGatewayTool("node.invoke", { timeoutMs: target.invokeTimeoutMs }, invoke); return formatNodeRunToolResult({ raw, startedAt, cwd: params.workdir }); }