mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 04:57:09 +02:00
Fix default sandbox image fallback for python3-dependent mutations (#73362)
This commit is contained in:
@@ -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.
|
||||
|
||||
116
src/agents/sandbox/docker.test.ts
Normal file
116
src/agents/sandbox/docker.test.ts
Normal file
@@ -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<typeof import("node:child_process")>("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],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user