Skip to content

Commit 8219dbc

Browse files
committed
Sandbox: improve missing-python3 write/edit error guidance
1 parent 6883f68 commit 8219dbc

4 files changed

Lines changed: 78 additions & 18 deletions

File tree

src/agents/sandbox/fs-bridge.shell.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createSandbox,
66
createSandboxFsBridge,
77
createSeededSandboxFsBridge,
8+
getDockerScript,
89
getScriptsFromCalls,
910
installFsBridgeTestHarness,
1011
mockedExecDockerRaw,
@@ -140,6 +141,35 @@ describe("sandbox fs bridge shell compatibility", () => {
140141
expect(scripts.some((script) => script.includes("os.replace("))).toBe(true);
141142
});
142143

144+
it("surfaces actionable guidance when sandbox python3 is missing", async () => {
145+
const bridge = createSandboxFsBridge({ sandbox: createSandbox() });
146+
const baseImpl = mockedExecDockerRaw.getMockImplementation();
147+
mockedExecDockerRaw.mockImplementation(async (...callArgs) => {
148+
const args = callArgs[0] as string[];
149+
if (getDockerScript(args).includes("operation = sys.argv[1]")) {
150+
const message = "moltbot-sandbox-fs: 2: python3: not found";
151+
const err = Object.assign(new Error(message), {
152+
code: 127,
153+
stdout: Buffer.alloc(0),
154+
stderr: Buffer.from(message),
155+
});
156+
throw err;
157+
}
158+
if (baseImpl) {
159+
return await baseImpl(...callArgs);
160+
}
161+
return {
162+
stdout: Buffer.alloc(0),
163+
stderr: Buffer.alloc(0),
164+
code: 0,
165+
};
166+
});
167+
168+
await expect(bridge.writeFile({ filePath: "b.txt", data: "hello" })).rejects.toThrow(
169+
/requires `python3` inside the sandbox container/i,
170+
);
171+
});
172+
143173
it("routes mkdirp, remove, and rename through the pinned mutation helper", async () => {
144174
await withTempDir("openclaw-fs-bridge-shell-write-", async (stateDir) => {
145175
const { bridge } = await createSeededSandboxFsBridge(stateDir, {

src/agents/sandbox/fs-bridge.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
resolveSandboxFsPathWithMounts,
1515
type SandboxResolvedFsPath,
1616
} from "./fs-paths.js";
17+
import { toFriendlySandboxMutationError } from "./mutation-errors.js";
1718
import type { SandboxContext, SandboxWorkspaceAccess } from "./types.js";
1819

1920
type RunCommandOptions = {
@@ -286,12 +287,19 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
286287
if (plan.recheckBeforeCommand) {
287288
await this.pathGuard.assertPathChecks(plan.checks);
288289
}
289-
return await this.runCommand(plan.script, {
290-
args: plan.args,
291-
stdin: plan.stdin,
292-
allowFailure: plan.allowFailure,
293-
signal: plan.signal,
294-
});
290+
try {
291+
return await this.runCommand(plan.script, {
292+
args: plan.args,
293+
stdin: plan.stdin,
294+
allowFailure: plan.allowFailure,
295+
signal: plan.signal,
296+
});
297+
} catch (error) {
298+
if (plan.script.includes("python3 /dev/fd/3")) {
299+
throw toFriendlySandboxMutationError(error);
300+
}
301+
throw error;
302+
}
295303
}
296304

297305
private async runPlannedCommand(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const PYTHON3_MISSING_RE = /\bpython3:\s*not found\b/i;
2+
3+
/**
4+
* Provide actionable guidance when mutation helpers fail because the sandbox image
5+
* is missing Python. This keeps write/edit errors from surfacing as opaque shell
6+
* stderr blobs.
7+
*/
8+
export function toFriendlySandboxMutationError(error: unknown): Error {
9+
const rawMessage = error instanceof Error ? error.message : String(error);
10+
if (!PYTHON3_MISSING_RE.test(rawMessage)) {
11+
return error instanceof Error ? error : new Error(rawMessage);
12+
}
13+
return new Error(
14+
"Sandbox write/edit requires `python3` inside the sandbox container, but it is missing in the active image. Rebuild or update the sandbox image (or configure a custom sandbox image that includes `python3`). Original error: " +
15+
rawMessage,
16+
);
17+
}

src/agents/sandbox/remote-fs-bridge.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { SandboxBackendCommandParams, SandboxBackendCommandResult } from ".
44
import { SANDBOX_PINNED_MUTATION_PYTHON } from "./fs-bridge-mutation-helper.js";
55
import { createWritableRenameTargetResolver } from "./fs-bridge-rename-targets.js";
66
import type { SandboxFsBridge, SandboxFsStat, SandboxResolvedPath } from "./fs-bridge.js";
7+
import { toFriendlySandboxMutationError } from "./mutation-errors.js";
78
import {
89
isPathInsideContainerRoot,
910
normalizeContainerPath as normalizeSandboxContainerPath,
@@ -472,18 +473,22 @@ class RemoteShellSandboxFsBridge implements SandboxFsBridge {
472473
signal?: AbortSignal;
473474
allowFailure?: boolean;
474475
}) {
475-
await this.runRemoteScript({
476-
script: [
477-
"set -eu",
478-
"python3 /dev/fd/3 \"$@\" 3<<'PY'",
479-
SANDBOX_PINNED_MUTATION_PYTHON,
480-
"PY",
481-
].join("\n"),
482-
args: params.args,
483-
stdin: params.stdin,
484-
signal: params.signal,
485-
allowFailure: params.allowFailure,
486-
});
476+
try {
477+
await this.runRemoteScript({
478+
script: [
479+
"set -eu",
480+
"python3 /dev/fd/3 \"$@\" 3<<'PY'",
481+
SANDBOX_PINNED_MUTATION_PYTHON,
482+
"PY",
483+
].join("\n"),
484+
args: params.args,
485+
stdin: params.stdin,
486+
signal: params.signal,
487+
allowFailure: params.allowFailure,
488+
});
489+
} catch (error) {
490+
throw toFriendlySandboxMutationError(error);
491+
}
487492
}
488493

489494
private async runRemoteScript(params: {

0 commit comments

Comments
 (0)