diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c4a02be51..26e44ff4174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai - 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. - 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. - 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. +- 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. - 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. - 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. - 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. diff --git a/src/agents/sandbox/docker.test.ts b/src/agents/sandbox/docker.test.ts new file mode 100644 index 00000000000..2dca47efcd8 --- /dev/null +++ b/src/agents/sandbox/docker.test.ts @@ -0,0 +1,116 @@ +import { EventEmitter } from "node:events"; +import { Readable } from "node:stream"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_SANDBOX_IMAGE } from "./constants.js"; + +type SpawnCall = { + command: string; + args: string[]; +}; + +type MockDockerChild = EventEmitter & { + stdout: Readable; + stderr: Readable; + stdin: { end: (input?: string | Buffer) => void }; + kill: (signal?: NodeJS.Signals) => void; +}; + +const spawnState = vi.hoisted(() => ({ + calls: [] as SpawnCall[], + imageExists: true, +})); + +function createMockDockerChild(): MockDockerChild { + const child = new EventEmitter() as MockDockerChild; + child.stdout = new Readable({ read() {} }); + child.stderr = new Readable({ read() {} }); + child.stdin = { end: () => undefined }; + child.kill = () => undefined; + return child; +} + +function spawnDockerProcess(command: string, args: string[]) { + spawnState.calls.push({ command, args }); + const child = createMockDockerChild(); + + let code = 0; + let stderr = ""; + if (command !== "docker") { + code = 1; + stderr = `unexpected command: ${command}`; + } else if (args[0] === "image" && args[1] === "inspect") { + code = spawnState.imageExists ? 0 : 1; + stderr = spawnState.imageExists ? "" : `Error response from daemon: No such image: ${args[2]}`; + } else if (args[0] === "pull" || args[0] === "tag") { + code = 0; + } else { + code = 1; + stderr = `unexpected docker args: ${args.join(" ")}`; + } + + queueMicrotask(() => { + if (stderr) { + child.stderr.emit("data", Buffer.from(stderr)); + } + child.emit("close", code); + }); + return child; +} + +async function createChildProcessMock() { + const actual = await vi.importActual("node:child_process"); + return { + ...actual, + spawn: spawnDockerProcess, + }; +} + +vi.mock("node:child_process", async () => createChildProcessMock()); + +let ensureDockerImage: typeof import("./docker.js").ensureDockerImage; + +async function loadFreshDockerModuleForTest() { + vi.resetModules(); + vi.doMock("node:child_process", async () => createChildProcessMock()); + ({ ensureDockerImage } = await import("./docker.js")); +} + +describe("ensureDockerImage", () => { + beforeEach(async () => { + spawnState.calls.length = 0; + spawnState.imageExists = true; + await loadFreshDockerModuleForTest(); + }); + + it("returns when the configured image already exists", async () => { + await ensureDockerImage(DEFAULT_SANDBOX_IMAGE); + + expect(spawnState.calls).toEqual([ + { + command: "docker", + args: ["image", "inspect", DEFAULT_SANDBOX_IMAGE], + }, + ]); + }); + + it("does not satisfy the missing default sandbox image by tagging plain Debian", async () => { + spawnState.imageExists = false; + + let err: unknown; + try { + await ensureDockerImage(DEFAULT_SANDBOX_IMAGE); + } catch (caught) { + err = caught; + } + + expect(err).toBeInstanceOf(Error); + expect((err as Error).message).toContain("scripts/sandbox-setup.sh"); + expect((err as Error).message).toContain("python3"); + expect(spawnState.calls).toEqual([ + { + command: "docker", + args: ["image", "inspect", DEFAULT_SANDBOX_IMAGE], + }, + ]); + }); +}); diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index c9da9180493..d6c3eb38693 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -292,9 +292,9 @@ export async function ensureDockerImage(image: string) { return; } if (image === DEFAULT_SANDBOX_IMAGE) { - await execDocker(["pull", "debian:bookworm-slim"]); - await execDocker(["tag", "debian:bookworm-slim", DEFAULT_SANDBOX_IMAGE]); - return; + throw new Error( + `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.`, + ); } throw new Error(`Sandbox image not found: ${image}. Build or pull it first.`); }