Skip to content

Commit 47dc9f7

Browse files
authored
Fix default sandbox image fallback for python3-dependent mutations (#73362)
1 parent 6f3b5f8 commit 47dc9f7

3 files changed

Lines changed: 120 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
2828
- Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger `RangeError: Maximum call stack size exceeded`. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.
2929
- Agents/Anthropic: cancel stalled Anthropic Messages SSE body reads when abort signals fire, so active-memory timeouts release transport resources instead of leaving hidden recall runs parked on `reader.read()`. Refs #72965 and #73120. Thanks @wdeveloper16.
3030
- Control UI/WebChat: keep pending run and typing state attached to the active client run, so unowned inject/announce/side-result finals no longer unlock unrelated active runs while completed owned runs still clear promptly. Fixes #57795; carries forward the narrow diagnosis from #57887. Thanks @haoyu-haoyu.
31+
- Sandbox/Docker: stop satisfying a missing default sandbox image by tagging plain Debian as `openclaw-sandbox:bookworm-slim`, preserving the Python tooling required by sandbox write/edit helpers and directing users to build the default image. Fixes #51185; refs #45108, #51099, #51609, and #57713. Thanks @dpalis, @Tin55FoilDev, @jbcohen2-coder, @macminihal-cyber, and @PraxoOnline.
3132
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
3233
- Gateway/models: add `models.pricing.enabled` so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.
3334
- Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale `OPENCLAW_GATEWAY_TOKEN` or `OPENCLAW_GATEWAY_PASSWORD` values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.

src/agents/sandbox/docker.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { EventEmitter } from "node:events";
2+
import { Readable } from "node:stream";
3+
import { beforeEach, describe, expect, it, vi } from "vitest";
4+
import { DEFAULT_SANDBOX_IMAGE } from "./constants.js";
5+
6+
type SpawnCall = {
7+
command: string;
8+
args: string[];
9+
};
10+
11+
type MockDockerChild = EventEmitter & {
12+
stdout: Readable;
13+
stderr: Readable;
14+
stdin: { end: (input?: string | Buffer) => void };
15+
kill: (signal?: NodeJS.Signals) => void;
16+
};
17+
18+
const spawnState = vi.hoisted(() => ({
19+
calls: [] as SpawnCall[],
20+
imageExists: true,
21+
}));
22+
23+
function createMockDockerChild(): MockDockerChild {
24+
const child = new EventEmitter() as MockDockerChild;
25+
child.stdout = new Readable({ read() {} });
26+
child.stderr = new Readable({ read() {} });
27+
child.stdin = { end: () => undefined };
28+
child.kill = () => undefined;
29+
return child;
30+
}
31+
32+
function spawnDockerProcess(command: string, args: string[]) {
33+
spawnState.calls.push({ command, args });
34+
const child = createMockDockerChild();
35+
36+
let code = 0;
37+
let stderr = "";
38+
if (command !== "docker") {
39+
code = 1;
40+
stderr = `unexpected command: ${command}`;
41+
} else if (args[0] === "image" && args[1] === "inspect") {
42+
code = spawnState.imageExists ? 0 : 1;
43+
stderr = spawnState.imageExists ? "" : `Error response from daemon: No such image: ${args[2]}`;
44+
} else if (args[0] === "pull" || args[0] === "tag") {
45+
code = 0;
46+
} else {
47+
code = 1;
48+
stderr = `unexpected docker args: ${args.join(" ")}`;
49+
}
50+
51+
queueMicrotask(() => {
52+
if (stderr) {
53+
child.stderr.emit("data", Buffer.from(stderr));
54+
}
55+
child.emit("close", code);
56+
});
57+
return child;
58+
}
59+
60+
async function createChildProcessMock() {
61+
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
62+
return {
63+
...actual,
64+
spawn: spawnDockerProcess,
65+
};
66+
}
67+
68+
vi.mock("node:child_process", async () => createChildProcessMock());
69+
70+
let ensureDockerImage: typeof import("./docker.js").ensureDockerImage;
71+
72+
async function loadFreshDockerModuleForTest() {
73+
vi.resetModules();
74+
vi.doMock("node:child_process", async () => createChildProcessMock());
75+
({ ensureDockerImage } = await import("./docker.js"));
76+
}
77+
78+
describe("ensureDockerImage", () => {
79+
beforeEach(async () => {
80+
spawnState.calls.length = 0;
81+
spawnState.imageExists = true;
82+
await loadFreshDockerModuleForTest();
83+
});
84+
85+
it("returns when the configured image already exists", async () => {
86+
await ensureDockerImage(DEFAULT_SANDBOX_IMAGE);
87+
88+
expect(spawnState.calls).toEqual([
89+
{
90+
command: "docker",
91+
args: ["image", "inspect", DEFAULT_SANDBOX_IMAGE],
92+
},
93+
]);
94+
});
95+
96+
it("does not satisfy the missing default sandbox image by tagging plain Debian", async () => {
97+
spawnState.imageExists = false;
98+
99+
let err: unknown;
100+
try {
101+
await ensureDockerImage(DEFAULT_SANDBOX_IMAGE);
102+
} catch (caught) {
103+
err = caught;
104+
}
105+
106+
expect(err).toBeInstanceOf(Error);
107+
expect((err as Error).message).toContain("scripts/sandbox-setup.sh");
108+
expect((err as Error).message).toContain("python3");
109+
expect(spawnState.calls).toEqual([
110+
{
111+
command: "docker",
112+
args: ["image", "inspect", DEFAULT_SANDBOX_IMAGE],
113+
},
114+
]);
115+
});
116+
});

src/agents/sandbox/docker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,9 @@ export async function ensureDockerImage(image: string) {
292292
return;
293293
}
294294
if (image === DEFAULT_SANDBOX_IMAGE) {
295-
await execDocker(["pull", "debian:bookworm-slim"]);
296-
await execDocker(["tag", "debian:bookworm-slim", DEFAULT_SANDBOX_IMAGE]);
297-
return;
295+
throw new Error(
296+
`Sandbox image not found: ${image}. Build it with scripts/sandbox-setup.sh before enabling Docker sandboxing. The default image includes python3 for sandbox write/edit helpers; OpenClaw will not substitute plain debian:bookworm-slim.`,
297+
);
298298
}
299299
throw new Error(`Sandbox image not found: ${image}. Build or pull it first.`);
300300
}

0 commit comments

Comments
 (0)