fix(sandbox): resolve pinned fs helper python without PATH (#58573)

This commit is contained in:
Reed 2026-04-01 09:10:17 +08:00 committed by GitHub
parent a37c66906c
commit b86f5d5ea4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 53 additions and 5 deletions

View File

@ -1,4 +1,5 @@
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
@ -6,6 +7,7 @@ import { withTempDir } from "../../test-helpers/temp-dir.js";
import {
buildPinnedWritePlan,
SANDBOX_PINNED_MUTATION_PYTHON,
SANDBOX_PINNED_MUTATION_PYTHON_CANDIDATES,
} from "./fs-bridge-mutation-helper.js";
function runMutation(args: string[], input?: string) {
@ -16,7 +18,7 @@ function runMutation(args: string[], input?: string) {
});
}
function runWritePlan(args: string[], input?: string) {
function runWritePlan(args: string[], input?: string, env?: NodeJS.ProcessEnv) {
const plan = buildPinnedWritePlan({
check: {
target: {
@ -38,13 +40,18 @@ function runWritePlan(args: string[], input?: string) {
mkdir: args[4] === "1",
});
return spawnSync("sh", ["-c", plan.script, "openclaw-sandbox-fs", ...(plan.args ?? [])], {
return spawnSync("/bin/sh", ["-c", plan.script, "openclaw-sandbox-fs", ...(plan.args ?? [])], {
input,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
env,
});
}
const hasAbsolutePythonCandidate = SANDBOX_PINNED_MUTATION_PYTHON_CANDIDATES.some((candidate) =>
existsSync(candidate),
);
describe("sandbox pinned mutation helper", () => {
it("writes through a pinned directory fd", async () => {
await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
@ -116,6 +123,29 @@ describe("sandbox pinned mutation helper", () => {
},
);
it.runIf(process.platform !== "win32" && hasAbsolutePythonCandidate)(
"finds an absolute python when the write plan runs with an empty PATH",
async () => {
await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
const workspace = path.join(root, "workspace");
await fs.mkdir(workspace, { recursive: true });
const result = runWritePlan(
["write", workspace, "nested/deeper", "note.txt", "1"],
"hello",
{
PATH: "",
},
);
expect(result.status).toBe(0);
await expect(
fs.readFile(path.join(workspace, "nested", "deeper", "note.txt"), "utf8"),
).resolves.toBe("hello");
});
},
);
it.runIf(process.platform !== "win32")(
"rejects symlink-parent writes instead of materializing a temp file outside the mount",
async () => {

View File

@ -6,6 +6,13 @@ import type {
} from "./fs-bridge-path-safety.js";
import type { SandboxFsCommandPlan } from "./fs-bridge-shell-command-plans.js";
export const SANDBOX_PINNED_MUTATION_PYTHON_CANDIDATES = [
"/usr/bin/python3",
"/usr/local/bin/python3",
"/opt/homebrew/bin/python3",
"/bin/python3",
] as const;
export const SANDBOX_PINNED_MUTATION_PYTHON = [
"import errno",
"import os",
@ -276,6 +283,8 @@ export const SANDBOX_PINNED_MUTATION_PYTHON = [
" raise RuntimeError('unknown sandbox mutation operation: ' + operation)",
].join("\n");
const SANDBOX_PINNED_MUTATION_PYTHON_SHELL_LITERAL = `'${SANDBOX_PINNED_MUTATION_PYTHON.replaceAll("'", `'\\''`)}'`;
function buildPinnedMutationPlan(params: {
args: string[];
checks: PathSafetyCheck[];
@ -286,9 +295,18 @@ function buildPinnedMutationPlan(params: {
// Feed the helper source over fd 3 so stdin stays available for write payload bytes.
script: [
"set -eu",
"python3 /dev/fd/3 \"$@\" 3<<'PY'",
SANDBOX_PINNED_MUTATION_PYTHON,
"PY",
"python_cmd=''",
...SANDBOX_PINNED_MUTATION_PYTHON_CANDIDATES.map(
(candidate) =>
`if [ -z "$python_cmd" ] && [ -x '${candidate}' ]; then python_cmd='${candidate}'; fi`,
),
'if [ -z "$python_cmd" ]; then python_cmd=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true); fi',
'if [ -z "$python_cmd" ]; then',
" echo >&2 'sandbox pinned mutation helper requires python3 or python'",
" exit 127",
"fi",
`python_script=${SANDBOX_PINNED_MUTATION_PYTHON_SHELL_LITERAL}`,
'exec "$python_cmd" -c "$python_script" "$@"',
].join("\n"),
args: params.args,
};