import crypto from "node:crypto"; import { resolveAgentConfig } from "../agents/agent-scope.js"; import { loadConfig } from "../config/config.js"; import type { GatewayClient } from "../gateway/client.js"; import { addAllowlistEntry, analyzeArgvCommand, evaluateExecAllowlist, evaluateShellAllowlist, recordAllowlistUse, resolveAllowAlwaysPatterns, resolveExecApprovals, type ExecAllowlistEntry, type ExecAsk, type ExecCommandSegment, type ExecSecurity, type SkillBinTrustEntry, } from "../infra/exec-approvals.js"; import type { ExecHostRequest, ExecHostResponse, ExecHostRunResult } from "../infra/exec-host.js"; import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js"; import { sanitizeSystemRunEnvOverrides } from "../infra/host-env-security.js"; import { resolveSystemRunCommand } from "../infra/system-run-command.js"; import { evaluateSystemRunPolicy, resolveExecApprovalDecision } from "./exec-policy.js"; import type { ExecEventPayload, RunResult, SkillBinsProvider, SystemRunParams, } from "./invoke-types.js"; type SystemRunInvokeResult = { ok: boolean; payloadJSON?: string | null; error?: { code?: string; message?: string } | null; }; type SystemRunDeniedReason = | "security=deny" | "approval-required" | "allowlist-miss" | "execution-plan-miss" | "companion-unavailable" | "permission:screenRecording"; type SystemRunExecutionContext = { sessionKey: string; runId: string; cmdText: string; }; type SystemRunAllowlistAnalysis = { analysisOk: boolean; allowlistMatches: ExecAllowlistEntry[]; allowlistSatisfied: boolean; segments: ExecCommandSegment[]; }; function normalizeDeniedReason(reason: string | null | undefined): SystemRunDeniedReason { switch (reason) { case "security=deny": case "approval-required": case "allowlist-miss": case "execution-plan-miss": case "companion-unavailable": case "permission:screenRecording": return reason; default: return "approval-required"; } } export type HandleSystemRunInvokeOptions = { client: GatewayClient; params: SystemRunParams; skillBins: SkillBinsProvider; execHostEnforced: boolean; execHostFallbackAllowed: boolean; resolveExecSecurity: (value?: string) => ExecSecurity; resolveExecAsk: (value?: string) => ExecAsk; isCmdExeInvocation: (argv: string[]) => boolean; sanitizeEnv: (overrides?: Record | null) => Record | undefined; runCommand: ( argv: string[], cwd: string | undefined, env: Record | undefined, timeoutMs: number | undefined, ) => Promise; runViaMacAppExecHost: (params: { approvals: ReturnType; request: ExecHostRequest; }) => Promise; sendNodeEvent: (client: GatewayClient, event: string, payload: unknown) => Promise; buildExecEventPayload: (payload: ExecEventPayload) => ExecEventPayload; sendInvokeResult: (result: SystemRunInvokeResult) => Promise; sendExecFinishedEvent: (params: { sessionKey: string; runId: string; cmdText: string; result: { stdout?: string; stderr?: string; error?: string | null; exitCode?: number | null; timedOut?: boolean; success?: boolean; }; }) => Promise; preferMacAppExecHost: boolean; }; async function sendSystemRunDenied( opts: Pick< HandleSystemRunInvokeOptions, "client" | "sendNodeEvent" | "buildExecEventPayload" | "sendInvokeResult" >, execution: SystemRunExecutionContext, params: { reason: SystemRunDeniedReason; message: string; }, ) { await opts.sendNodeEvent( opts.client, "exec.denied", opts.buildExecEventPayload({ sessionKey: execution.sessionKey, runId: execution.runId, host: "node", command: execution.cmdText, reason: params.reason, }), ); await opts.sendInvokeResult({ ok: false, error: { code: "UNAVAILABLE", message: params.message }, }); } function evaluateSystemRunAllowlist(params: { shellCommand: string | null; argv: string[]; approvals: ReturnType; security: ExecSecurity; safeBins: ReturnType["safeBins"]; safeBinProfiles: ReturnType["safeBinProfiles"]; trustedSafeBinDirs: ReturnType["trustedSafeBinDirs"]; cwd: string | undefined; env: Record | undefined; skillBins: SkillBinTrustEntry[]; autoAllowSkills: boolean; }): SystemRunAllowlistAnalysis { if (params.shellCommand) { const allowlistEval = evaluateShellAllowlist({ command: params.shellCommand, allowlist: params.approvals.allowlist, safeBins: params.safeBins, safeBinProfiles: params.safeBinProfiles, cwd: params.cwd, env: params.env, trustedSafeBinDirs: params.trustedSafeBinDirs, skillBins: params.skillBins, autoAllowSkills: params.autoAllowSkills, platform: process.platform, }); return { analysisOk: allowlistEval.analysisOk, allowlistMatches: allowlistEval.allowlistMatches, allowlistSatisfied: params.security === "allowlist" && allowlistEval.analysisOk ? allowlistEval.allowlistSatisfied : false, segments: allowlistEval.segments, }; } const analysis = analyzeArgvCommand({ argv: params.argv, cwd: params.cwd, env: params.env }); const allowlistEval = evaluateExecAllowlist({ analysis, allowlist: params.approvals.allowlist, safeBins: params.safeBins, safeBinProfiles: params.safeBinProfiles, cwd: params.cwd, trustedSafeBinDirs: params.trustedSafeBinDirs, skillBins: params.skillBins, autoAllowSkills: params.autoAllowSkills, }); return { analysisOk: analysis.ok, allowlistMatches: allowlistEval.allowlistMatches, allowlistSatisfied: params.security === "allowlist" && analysis.ok ? allowlistEval.allowlistSatisfied : false, segments: analysis.segments, }; } function resolvePlannedAllowlistArgv(params: { security: ExecSecurity; shellCommand: string | null; policy: { approvedByAsk: boolean; analysisOk: boolean; allowlistSatisfied: boolean; }; segments: ExecCommandSegment[]; }): string[] | undefined | null { if ( params.security !== "allowlist" || params.policy.approvedByAsk || params.shellCommand || !params.policy.analysisOk || !params.policy.allowlistSatisfied || params.segments.length !== 1 ) { return undefined; } const plannedAllowlistArgv = params.segments[0]?.resolution?.effectiveArgv; return plannedAllowlistArgv && plannedAllowlistArgv.length > 0 ? plannedAllowlistArgv : null; } function resolveSystemRunExecArgv(params: { plannedAllowlistArgv: string[] | undefined; argv: string[]; security: ExecSecurity; isWindows: boolean; policy: { approvedByAsk: boolean; analysisOk: boolean; allowlistSatisfied: boolean; }; shellCommand: string | null; segments: ExecCommandSegment[]; }): string[] { let execArgv = params.plannedAllowlistArgv ?? params.argv; if ( params.security === "allowlist" && params.isWindows && !params.policy.approvedByAsk && params.shellCommand && params.policy.analysisOk && params.policy.allowlistSatisfied && params.segments.length === 1 && params.segments[0]?.argv.length > 0 ) { execArgv = params.segments[0].argv; } return execArgv; } function applyOutputTruncation(result: RunResult) { if (!result.truncated) { return; } const suffix = "... (truncated)"; if (result.stderr.trim().length > 0) { result.stderr = `${result.stderr}\n${suffix}`; } else { result.stdout = `${result.stdout}\n${suffix}`; } } export { formatSystemRunAllowlistMissMessage } from "./exec-policy.js"; export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions): Promise { const command = resolveSystemRunCommand({ command: opts.params.command, rawCommand: opts.params.rawCommand, }); if (!command.ok) { await opts.sendInvokeResult({ ok: false, error: { code: "INVALID_REQUEST", message: command.message }, }); return; } if (command.argv.length === 0) { await opts.sendInvokeResult({ ok: false, error: { code: "INVALID_REQUEST", message: "command required" }, }); return; } const argv = command.argv; const rawCommand = command.rawCommand ?? ""; const shellCommand = command.shellCommand; const cmdText = command.cmdText; const agentId = opts.params.agentId?.trim() || undefined; const cfg = loadConfig(); const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : undefined; const configuredSecurity = opts.resolveExecSecurity( agentExec?.security ?? cfg.tools?.exec?.security, ); const configuredAsk = opts.resolveExecAsk(agentExec?.ask ?? cfg.tools?.exec?.ask); const approvals = resolveExecApprovals(agentId, { security: configuredSecurity, ask: configuredAsk, }); const security = approvals.agent.security; const ask = approvals.agent.ask; const autoAllowSkills = approvals.agent.autoAllowSkills; const sessionKey = opts.params.sessionKey?.trim() || "node"; const runId = opts.params.runId?.trim() || crypto.randomUUID(); const execution: SystemRunExecutionContext = { sessionKey, runId, cmdText }; const approvalDecision = resolveExecApprovalDecision(opts.params.approvalDecision); const envOverrides = sanitizeSystemRunEnvOverrides({ overrides: opts.params.env ?? undefined, shellWrapper: shellCommand !== null, }); const env = opts.sanitizeEnv(envOverrides); const { safeBins, safeBinProfiles, trustedSafeBinDirs } = resolveExecSafeBinRuntimePolicy({ global: cfg.tools?.exec, local: agentExec, }); const bins = autoAllowSkills ? await opts.skillBins.current() : []; let { analysisOk, allowlistMatches, allowlistSatisfied, segments } = evaluateSystemRunAllowlist({ shellCommand, argv, approvals, security, safeBins, safeBinProfiles, trustedSafeBinDirs, cwd: opts.params.cwd ?? undefined, env, skillBins: bins, autoAllowSkills, }); const isWindows = process.platform === "win32"; const cmdInvocation = shellCommand ? opts.isCmdExeInvocation(segments[0]?.argv ?? []) : opts.isCmdExeInvocation(argv); const policy = evaluateSystemRunPolicy({ security, ask, analysisOk, allowlistSatisfied, approvalDecision, approved: opts.params.approved === true, isWindows, cmdInvocation, shellWrapperInvocation: shellCommand !== null, }); analysisOk = policy.analysisOk; allowlistSatisfied = policy.allowlistSatisfied; if (!policy.allowed) { await sendSystemRunDenied(opts, execution, { reason: policy.eventReason, message: policy.errorMessage, }); return; } // Fail closed if policy/runtime drift re-allows unapproved shell wrappers. if (security === "allowlist" && shellCommand && !policy.approvedByAsk) { await sendSystemRunDenied(opts, execution, { reason: "approval-required", message: "SYSTEM_RUN_DENIED: approval required", }); return; } const plannedAllowlistArgv = resolvePlannedAllowlistArgv({ security, shellCommand, policy, segments, }); if (plannedAllowlistArgv === null) { await sendSystemRunDenied(opts, execution, { reason: "execution-plan-miss", message: "SYSTEM_RUN_DENIED: execution plan mismatch", }); return; } const useMacAppExec = opts.preferMacAppExecHost; if (useMacAppExec) { const execRequest: ExecHostRequest = { command: plannedAllowlistArgv ?? argv, rawCommand: rawCommand || shellCommand || null, cwd: opts.params.cwd ?? null, env: envOverrides ?? null, timeoutMs: opts.params.timeoutMs ?? null, needsScreenRecording: opts.params.needsScreenRecording ?? null, agentId: agentId ?? null, sessionKey: sessionKey ?? null, approvalDecision, }; const response = await opts.runViaMacAppExecHost({ approvals, request: execRequest }); if (!response) { if (opts.execHostEnforced || !opts.execHostFallbackAllowed) { await sendSystemRunDenied(opts, execution, { reason: "companion-unavailable", message: "COMPANION_APP_UNAVAILABLE: macOS app exec host unreachable", }); return; } } else if (!response.ok) { await sendSystemRunDenied(opts, execution, { reason: normalizeDeniedReason(response.error.reason), message: response.error.message, }); return; } else { const result: ExecHostRunResult = response.payload; await opts.sendExecFinishedEvent({ sessionKey, runId, cmdText, result }); await opts.sendInvokeResult({ ok: true, payloadJSON: JSON.stringify(result), }); return; } } if (policy.approvalDecision === "allow-always" && security === "allowlist") { if (policy.analysisOk) { const patterns = resolveAllowAlwaysPatterns({ segments, cwd: opts.params.cwd ?? undefined, env, platform: process.platform, }); for (const pattern of patterns) { if (pattern) { addAllowlistEntry(approvals.file, agentId, pattern); } } } } if (allowlistMatches.length > 0) { const seen = new Set(); for (const match of allowlistMatches) { if (!match?.pattern || seen.has(match.pattern)) { continue; } seen.add(match.pattern); recordAllowlistUse( approvals.file, agentId, match, cmdText, segments[0]?.resolution?.resolvedPath, ); } } if (opts.params.needsScreenRecording === true) { await sendSystemRunDenied(opts, execution, { reason: "permission:screenRecording", message: "PERMISSION_MISSING: screenRecording", }); return; } const execArgv = resolveSystemRunExecArgv({ plannedAllowlistArgv: plannedAllowlistArgv ?? undefined, argv, security, isWindows, policy, shellCommand, segments, }); const result = await opts.runCommand( execArgv, opts.params.cwd?.trim() || undefined, env, opts.params.timeoutMs ?? undefined, ); applyOutputTruncation(result); await opts.sendExecFinishedEvent({ sessionKey, runId, cmdText, result }); await opts.sendInvokeResult({ ok: true, payloadJSON: JSON.stringify({ exitCode: result.exitCode, timedOut: result.timedOut, success: result.success, stdout: result.stdout, stderr: result.stderr, error: result.error ?? null, }), }); }