Files
openclaw/src/infra/exec-wrapper-trust-plan.ts
2026-06-22 19:11:46 +08:00

154 lines
4.5 KiB
TypeScript

// Builds the trust plan for exec wrappers before commands are launched.
import {
MAX_DISPATCH_WRAPPER_DEPTH,
resolveDispatchWrapperTrustPlan,
unwrapKnownDispatchWrapperInvocation,
} from "./dispatch-wrapper-resolution.js";
import {
extractBindableShellWrapperInlineCommand,
isShellWrapperExecutable,
unwrapKnownShellMultiplexerInvocation,
} from "./shell-wrapper-resolution.js";
type ExecWrapperTrustPlan = {
argv: string[];
policyArgv: string[];
wrapperChain: string[];
policyBlocked: boolean;
blockedWrapper?: string;
shellWrapperExecutable: boolean;
shellInlineCommand: string | null;
};
function blockedExecWrapperTrustPlan(params: {
argv: string[];
policyArgv?: string[];
wrapperChain: string[];
blockedWrapper: string;
}): ExecWrapperTrustPlan {
return {
argv: params.argv,
policyArgv: params.policyArgv ?? params.argv,
wrapperChain: params.wrapperChain,
policyBlocked: true,
blockedWrapper: params.blockedWrapper,
shellWrapperExecutable: false,
shellInlineCommand: null,
};
}
function finalizeExecWrapperTrustPlan(
argv: string[],
policyArgv: string[],
wrapperChain: string[],
policyBlocked: boolean,
): ExecWrapperTrustPlan {
const rawExecutable = argv[0]?.trim() ?? "";
const shellWrapperExecutable =
!policyBlocked && rawExecutable.length > 0 && isShellWrapperExecutable(rawExecutable);
const plan: ExecWrapperTrustPlan = {
argv,
policyArgv,
wrapperChain,
policyBlocked,
shellWrapperExecutable,
shellInlineCommand: shellWrapperExecutable
? extractBindableShellWrapperInlineCommand(argv)
: null,
};
return plan;
}
/**
* Resolves transparent dispatch wrappers into the executable that policy should inspect.
* Shell multiplexers keep their original argv as the trust target while exposing the
* nested shell command for shell-specific approval checks.
*/
export function resolveExecWrapperTrustPlan(
argv: string[],
maxDepth = MAX_DISPATCH_WRAPPER_DEPTH,
platform: NodeJS.Platform = process.platform,
): ExecWrapperTrustPlan {
let current = argv;
let policyArgv = argv;
let sawShellMultiplexer = false;
const wrapperChain: string[] = [];
for (let depth = 0; depth < maxDepth; depth += 1) {
const dispatchPlan = resolveDispatchWrapperTrustPlan(
current,
maxDepth - wrapperChain.length,
platform,
);
if (dispatchPlan.policyBlocked) {
return blockedExecWrapperTrustPlan({
argv: dispatchPlan.argv,
policyArgv: dispatchPlan.argv,
wrapperChain,
blockedWrapper: dispatchPlan.blockedWrapper ?? current[0] ?? "unknown",
});
}
if (dispatchPlan.wrappers.length > 0) {
wrapperChain.push(...dispatchPlan.wrappers);
current = dispatchPlan.argv;
if (!sawShellMultiplexer) {
policyArgv = current;
}
if (wrapperChain.length >= maxDepth) {
break;
}
continue;
}
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(current);
if (shellMultiplexerUnwrap.kind === "blocked") {
return blockedExecWrapperTrustPlan({
argv: current,
policyArgv,
wrapperChain,
blockedWrapper: shellMultiplexerUnwrap.wrapper,
});
}
if (shellMultiplexerUnwrap.kind === "unwrapped") {
wrapperChain.push(shellMultiplexerUnwrap.wrapper);
if (!sawShellMultiplexer) {
// Trust policy must see the multiplexer applet, not only the shell it launches.
policyArgv = current;
sawShellMultiplexer = true;
}
current = shellMultiplexerUnwrap.argv;
if (wrapperChain.length >= maxDepth) {
break;
}
continue;
}
break;
}
if (wrapperChain.length >= maxDepth) {
const dispatchOverflow = unwrapKnownDispatchWrapperInvocation(current, platform);
if (dispatchOverflow.kind === "blocked" || dispatchOverflow.kind === "unwrapped") {
return blockedExecWrapperTrustPlan({
argv: current,
policyArgv,
wrapperChain,
blockedWrapper: dispatchOverflow.wrapper,
});
}
const shellMultiplexerOverflow = unwrapKnownShellMultiplexerInvocation(current);
if (
shellMultiplexerOverflow.kind === "blocked" ||
shellMultiplexerOverflow.kind === "unwrapped"
) {
return blockedExecWrapperTrustPlan({
argv: current,
policyArgv,
wrapperChain,
blockedWrapper: shellMultiplexerOverflow.wrapper,
});
}
}
return finalizeExecWrapperTrustPlan(current, policyArgv, wrapperChain, false);
}