mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
refactor(exec): split system.run phases and align ts/swift validator contracts
This commit is contained in:
@@ -55,6 +55,37 @@ type SystemRunAllowlistAnalysis = {
|
||||
segments: ExecCommandSegment[];
|
||||
};
|
||||
|
||||
type ResolvedExecApprovals = ReturnType<typeof resolveExecApprovals>;
|
||||
|
||||
type SystemRunParsePhase = {
|
||||
argv: string[];
|
||||
shellCommand: string | null;
|
||||
cmdText: string;
|
||||
agentId: string | undefined;
|
||||
sessionKey: string;
|
||||
runId: string;
|
||||
execution: SystemRunExecutionContext;
|
||||
approvalDecision: ReturnType<typeof resolveExecApprovalDecision>;
|
||||
envOverrides: Record<string, string> | undefined;
|
||||
env: Record<string, string> | undefined;
|
||||
cwd: string | undefined;
|
||||
timeoutMs: number | undefined;
|
||||
needsScreenRecording: boolean;
|
||||
approved: boolean;
|
||||
};
|
||||
|
||||
type SystemRunPolicyPhase = SystemRunParsePhase & {
|
||||
approvals: ResolvedExecApprovals;
|
||||
security: ExecSecurity;
|
||||
policy: ReturnType<typeof evaluateSystemRunPolicy>;
|
||||
allowlistMatches: ExecAllowlistEntry[];
|
||||
analysisOk: boolean;
|
||||
allowlistSatisfied: boolean;
|
||||
segments: ExecCommandSegment[];
|
||||
plannedAllowlistArgv: string[] | undefined;
|
||||
isWindows: boolean;
|
||||
};
|
||||
|
||||
const safeBinTrustedDirWarningCache = new Set<string>();
|
||||
|
||||
function warnWritableTrustedDirOnce(message: string): void {
|
||||
@@ -270,7 +301,9 @@ function applyOutputTruncation(result: RunResult) {
|
||||
|
||||
export { formatSystemRunAllowlistMissMessage } from "./exec-policy.js";
|
||||
|
||||
export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions): Promise<void> {
|
||||
async function parseSystemRunPhase(
|
||||
opts: HandleSystemRunInvokeOptions,
|
||||
): Promise<SystemRunParsePhase | null> {
|
||||
const command = resolveSystemRunCommand({
|
||||
command: opts.params.command,
|
||||
rawCommand: opts.params.rawCommand,
|
||||
@@ -280,42 +313,62 @@ export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions):
|
||||
ok: false,
|
||||
error: { code: "INVALID_REQUEST", message: command.message },
|
||||
});
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (command.argv.length === 0) {
|
||||
await opts.sendInvokeResult({
|
||||
ok: false,
|
||||
error: { code: "INVALID_REQUEST", message: "command required" },
|
||||
});
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const argv = command.argv;
|
||||
const shellCommand = command.shellCommand;
|
||||
const cmdText = command.cmdText;
|
||||
const agentId = opts.params.agentId?.trim() || undefined;
|
||||
const sessionKey = opts.params.sessionKey?.trim() || "node";
|
||||
const runId = opts.params.runId?.trim() || crypto.randomUUID();
|
||||
const envOverrides = sanitizeSystemRunEnvOverrides({
|
||||
overrides: opts.params.env ?? undefined,
|
||||
shellWrapper: shellCommand !== null,
|
||||
});
|
||||
return {
|
||||
argv: command.argv,
|
||||
shellCommand,
|
||||
cmdText,
|
||||
agentId,
|
||||
sessionKey,
|
||||
runId,
|
||||
execution: { sessionKey, runId, cmdText },
|
||||
approvalDecision: resolveExecApprovalDecision(opts.params.approvalDecision),
|
||||
envOverrides,
|
||||
env: opts.sanitizeEnv(envOverrides),
|
||||
cwd: opts.params.cwd?.trim() || undefined,
|
||||
timeoutMs: opts.params.timeoutMs ?? undefined,
|
||||
needsScreenRecording: opts.params.needsScreenRecording === true,
|
||||
approved: opts.params.approved === true,
|
||||
};
|
||||
}
|
||||
|
||||
async function evaluateSystemRunPolicyPhase(
|
||||
opts: HandleSystemRunInvokeOptions,
|
||||
parsed: SystemRunParsePhase,
|
||||
): Promise<SystemRunPolicyPhase | null> {
|
||||
const cfg = loadConfig();
|
||||
const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : undefined;
|
||||
const agentExec = parsed.agentId
|
||||
? resolveAgentConfig(cfg, parsed.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, {
|
||||
const approvals = resolveExecApprovals(parsed.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,
|
||||
@@ -323,99 +376,124 @@ export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions):
|
||||
});
|
||||
const bins = autoAllowSkills ? await opts.skillBins.current() : [];
|
||||
let { analysisOk, allowlistMatches, allowlistSatisfied, segments } = evaluateSystemRunAllowlist({
|
||||
shellCommand,
|
||||
argv,
|
||||
shellCommand: parsed.shellCommand,
|
||||
argv: parsed.argv,
|
||||
approvals,
|
||||
security,
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
trustedSafeBinDirs,
|
||||
cwd: opts.params.cwd ?? undefined,
|
||||
env,
|
||||
cwd: parsed.cwd,
|
||||
env: parsed.env,
|
||||
skillBins: bins,
|
||||
autoAllowSkills,
|
||||
});
|
||||
const isWindows = process.platform === "win32";
|
||||
const cmdInvocation = shellCommand
|
||||
const cmdInvocation = parsed.shellCommand
|
||||
? opts.isCmdExeInvocation(segments[0]?.argv ?? [])
|
||||
: opts.isCmdExeInvocation(argv);
|
||||
: opts.isCmdExeInvocation(parsed.argv);
|
||||
const policy = evaluateSystemRunPolicy({
|
||||
security,
|
||||
ask,
|
||||
analysisOk,
|
||||
allowlistSatisfied,
|
||||
approvalDecision,
|
||||
approved: opts.params.approved === true,
|
||||
approvalDecision: parsed.approvalDecision,
|
||||
approved: parsed.approved,
|
||||
isWindows,
|
||||
cmdInvocation,
|
||||
shellWrapperInvocation: shellCommand !== null,
|
||||
shellWrapperInvocation: parsed.shellCommand !== null,
|
||||
});
|
||||
analysisOk = policy.analysisOk;
|
||||
allowlistSatisfied = policy.allowlistSatisfied;
|
||||
if (!policy.allowed) {
|
||||
await sendSystemRunDenied(opts, execution, {
|
||||
await sendSystemRunDenied(opts, parsed.execution, {
|
||||
reason: policy.eventReason,
|
||||
message: policy.errorMessage,
|
||||
});
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fail closed if policy/runtime drift re-allows unapproved shell wrappers.
|
||||
if (security === "allowlist" && shellCommand && !policy.approvedByAsk) {
|
||||
await sendSystemRunDenied(opts, execution, {
|
||||
if (security === "allowlist" && parsed.shellCommand && !policy.approvedByAsk) {
|
||||
await sendSystemRunDenied(opts, parsed.execution, {
|
||||
reason: "approval-required",
|
||||
message: "SYSTEM_RUN_DENIED: approval required",
|
||||
});
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const plannedAllowlistArgv = resolvePlannedAllowlistArgv({
|
||||
security,
|
||||
shellCommand,
|
||||
shellCommand: parsed.shellCommand,
|
||||
policy,
|
||||
segments,
|
||||
});
|
||||
if (plannedAllowlistArgv === null) {
|
||||
await sendSystemRunDenied(opts, execution, {
|
||||
await sendSystemRunDenied(opts, parsed.execution, {
|
||||
reason: "execution-plan-miss",
|
||||
message: "SYSTEM_RUN_DENIED: execution plan mismatch",
|
||||
});
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...parsed,
|
||||
approvals,
|
||||
security,
|
||||
policy,
|
||||
allowlistMatches,
|
||||
analysisOk,
|
||||
allowlistSatisfied,
|
||||
segments,
|
||||
plannedAllowlistArgv: plannedAllowlistArgv ?? undefined,
|
||||
isWindows,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeSystemRunPhase(
|
||||
opts: HandleSystemRunInvokeOptions,
|
||||
phase: SystemRunPolicyPhase,
|
||||
): Promise<void> {
|
||||
const useMacAppExec = opts.preferMacAppExecHost;
|
||||
if (useMacAppExec) {
|
||||
const execRequest: ExecHostRequest = {
|
||||
command: plannedAllowlistArgv ?? argv,
|
||||
command: phase.plannedAllowlistArgv ?? phase.argv,
|
||||
// Forward canonical display text so companion approval/prompt surfaces bind to
|
||||
// the exact command context already validated on the node-host.
|
||||
rawCommand: cmdText || 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,
|
||||
rawCommand: phase.cmdText || null,
|
||||
cwd: phase.cwd ?? null,
|
||||
env: phase.envOverrides ?? null,
|
||||
timeoutMs: phase.timeoutMs ?? null,
|
||||
needsScreenRecording: phase.needsScreenRecording,
|
||||
agentId: phase.agentId ?? null,
|
||||
sessionKey: phase.sessionKey ?? null,
|
||||
approvalDecision: phase.approvalDecision,
|
||||
};
|
||||
const response = await opts.runViaMacAppExecHost({ approvals, request: execRequest });
|
||||
const response = await opts.runViaMacAppExecHost({
|
||||
approvals: phase.approvals,
|
||||
request: execRequest,
|
||||
});
|
||||
if (!response) {
|
||||
if (opts.execHostEnforced || !opts.execHostFallbackAllowed) {
|
||||
await sendSystemRunDenied(opts, execution, {
|
||||
await sendSystemRunDenied(opts, phase.execution, {
|
||||
reason: "companion-unavailable",
|
||||
message: "COMPANION_APP_UNAVAILABLE: macOS app exec host unreachable",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (!response.ok) {
|
||||
await sendSystemRunDenied(opts, execution, {
|
||||
await sendSystemRunDenied(opts, phase.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.sendExecFinishedEvent({
|
||||
sessionKey: phase.sessionKey,
|
||||
runId: phase.runId,
|
||||
cmdText: phase.cmdText,
|
||||
result,
|
||||
});
|
||||
await opts.sendInvokeResult({
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify(result),
|
||||
@@ -424,41 +502,41 @@ export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions):
|
||||
}
|
||||
}
|
||||
|
||||
if (policy.approvalDecision === "allow-always" && security === "allowlist") {
|
||||
if (policy.analysisOk) {
|
||||
if (phase.policy.approvalDecision === "allow-always" && phase.security === "allowlist") {
|
||||
if (phase.policy.analysisOk) {
|
||||
const patterns = resolveAllowAlwaysPatterns({
|
||||
segments,
|
||||
cwd: opts.params.cwd ?? undefined,
|
||||
env,
|
||||
segments: phase.segments,
|
||||
cwd: phase.cwd,
|
||||
env: phase.env,
|
||||
platform: process.platform,
|
||||
});
|
||||
for (const pattern of patterns) {
|
||||
if (pattern) {
|
||||
addAllowlistEntry(approvals.file, agentId, pattern);
|
||||
addAllowlistEntry(phase.approvals.file, phase.agentId, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allowlistMatches.length > 0) {
|
||||
if (phase.allowlistMatches.length > 0) {
|
||||
const seen = new Set<string>();
|
||||
for (const match of allowlistMatches) {
|
||||
for (const match of phase.allowlistMatches) {
|
||||
if (!match?.pattern || seen.has(match.pattern)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(match.pattern);
|
||||
recordAllowlistUse(
|
||||
approvals.file,
|
||||
agentId,
|
||||
phase.approvals.file,
|
||||
phase.agentId,
|
||||
match,
|
||||
cmdText,
|
||||
segments[0]?.resolution?.resolvedPath,
|
||||
phase.cmdText,
|
||||
phase.segments[0]?.resolution?.resolvedPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.params.needsScreenRecording === true) {
|
||||
await sendSystemRunDenied(opts, execution, {
|
||||
if (phase.needsScreenRecording) {
|
||||
await sendSystemRunDenied(opts, phase.execution, {
|
||||
reason: "permission:screenRecording",
|
||||
message: "PERMISSION_MISSING: screenRecording",
|
||||
});
|
||||
@@ -466,23 +544,23 @@ export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions):
|
||||
}
|
||||
|
||||
const execArgv = resolveSystemRunExecArgv({
|
||||
plannedAllowlistArgv: plannedAllowlistArgv ?? undefined,
|
||||
argv,
|
||||
security,
|
||||
isWindows,
|
||||
policy,
|
||||
shellCommand,
|
||||
segments,
|
||||
plannedAllowlistArgv: phase.plannedAllowlistArgv,
|
||||
argv: phase.argv,
|
||||
security: phase.security,
|
||||
isWindows: phase.isWindows,
|
||||
policy: phase.policy,
|
||||
shellCommand: phase.shellCommand,
|
||||
segments: phase.segments,
|
||||
});
|
||||
|
||||
const result = await opts.runCommand(
|
||||
execArgv,
|
||||
opts.params.cwd?.trim() || undefined,
|
||||
env,
|
||||
opts.params.timeoutMs ?? undefined,
|
||||
);
|
||||
const result = await opts.runCommand(execArgv, phase.cwd, phase.env, phase.timeoutMs);
|
||||
applyOutputTruncation(result);
|
||||
await opts.sendExecFinishedEvent({ sessionKey, runId, cmdText, result });
|
||||
await opts.sendExecFinishedEvent({
|
||||
sessionKey: phase.sessionKey,
|
||||
runId: phase.runId,
|
||||
cmdText: phase.cmdText,
|
||||
result,
|
||||
});
|
||||
|
||||
await opts.sendInvokeResult({
|
||||
ok: true,
|
||||
@@ -496,3 +574,15 @@ export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions):
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleSystemRunInvoke(opts: HandleSystemRunInvokeOptions): Promise<void> {
|
||||
const parsed = await parseSystemRunPhase(opts);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
const policyPhase = await evaluateSystemRunPolicyPhase(opts, parsed);
|
||||
if (!policyPhase) {
|
||||
return;
|
||||
}
|
||||
await executeSystemRunPhase(opts, policyPhase);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user