refactor(exec): split host flows and harden safe-bin trust

This commit is contained in:
Peter Steinberger
2026-02-19 14:21:07 +01:00
parent b45bb6801c
commit fec48a5006
10 changed files with 834 additions and 616 deletions

View File

@@ -357,6 +357,7 @@ function evaluateSegments(
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
cwd?: string;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: Set<string>;
autoAllowSkills?: boolean;
},
@@ -384,6 +385,7 @@ function evaluateSegments(
resolution: segment.resolution,
safeBins: params.safeBins,
cwd: params.cwd,
trustedSafeBinDirs: params.trustedSafeBinDirs,
});
const skillAllow =
allowSkills && segment.resolution?.executableName
@@ -408,6 +410,7 @@ export function evaluateExecAllowlist(params: {
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
cwd?: string;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: Set<string>;
autoAllowSkills?: boolean;
}): ExecAllowlistEvaluation {
@@ -424,6 +427,7 @@ export function evaluateExecAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
});
@@ -441,6 +445,7 @@ export function evaluateExecAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
});
@@ -468,6 +473,7 @@ export function evaluateShellAllowlist(params: {
safeBins: Set<string>;
cwd?: string;
env?: NodeJS.ProcessEnv;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: Set<string>;
autoAllowSkills?: boolean;
platform?: string | null;
@@ -496,6 +502,7 @@ export function evaluateShellAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
});
@@ -529,6 +536,7 @@ export function evaluateShellAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
});

View File

@@ -517,7 +517,6 @@ describe("exec approvals safe bins", () => {
});
expect(ok).toBe(true);
});
it("does not include sort/grep in default safeBins", () => {
const defaults = resolveSafeBins(undefined);
expect(defaults.has("jq")).toBe(true);
@@ -582,6 +581,43 @@ describe("exec approvals safe bins", () => {
expect(ok).toBe(false);
expect(checkedExists).toBe(false);
});
it("threads trusted safe-bin dirs through allowlist evaluation", () => {
if (process.platform === "win32") {
return;
}
const analysis = {
ok: true as const,
segments: [
{
raw: "jq .foo",
argv: ["jq", ".foo"],
resolution: {
rawExecutable: "jq",
resolvedPath: "/custom/bin/jq",
executableName: "jq",
},
},
],
};
const denied = evaluateExecAllowlist({
analysis,
allowlist: [],
safeBins: normalizeSafeBins(["jq"]),
trustedSafeBinDirs: new Set(["/usr/bin"]),
cwd: "/tmp",
});
expect(denied.allowlistSatisfied).toBe(false);
const allowed = evaluateExecAllowlist({
analysis,
allowlist: [],
safeBins: normalizeSafeBins(["jq"]),
trustedSafeBinDirs: new Set(["/custom/bin"]),
cwd: "/tmp",
});
expect(allowed.allowlistSatisfied).toBe(true);
});
});
describe("exec approvals allowlist evaluation", () => {

View File

@@ -54,4 +54,18 @@ describe("exec safe bin trust", () => {
}),
).toBe(false);
});
it("uses startup PATH snapshot when pathEnv is omitted", () => {
const originalPath = process.env.PATH;
const injected = `/tmp/openclaw-path-injected-${Date.now()}`;
const initial = getTrustedSafeBinDirs({ refresh: true });
try {
process.env.PATH = `${injected}${path.delimiter}${originalPath ?? ""}`;
const refreshed = getTrustedSafeBinDirs({ refresh: true });
expect(refreshed.has(path.resolve(injected))).toBe(false);
expect([...refreshed].toSorted()).toEqual([...initial].toSorted());
} finally {
process.env.PATH = originalPath;
}
});
});

View File

@@ -29,6 +29,7 @@ type TrustedSafeBinCache = {
};
let trustedSafeBinCache: TrustedSafeBinCache | null = null;
const STARTUP_PATH_ENV = process.env.PATH ?? process.env.Path ?? "";
function normalizeTrustedDir(value: string): string | null {
const trimmed = value.trim();
@@ -74,7 +75,7 @@ export function getTrustedSafeBinDirs(
} = {},
): Set<string> {
const delimiter = params.delimiter ?? path.delimiter;
const pathEnv = params.pathEnv ?? process.env.PATH ?? process.env.Path ?? "";
const pathEnv = params.pathEnv ?? STARTUP_PATH_ENV;
const key = buildTrustedSafeBinCacheKey(pathEnv, delimiter);
if (!params.refresh && trustedSafeBinCache?.key === key) {