From 378851cf405314137aa5d3c0d68193af7cf94f88 Mon Sep 17 00:00:00 2001 From: sallyom Date: Wed, 29 Apr 2026 00:51:44 -0400 Subject: [PATCH] fix: repair sandbox Docker daemon fallback --- src/agents/sandbox.ts | 2 +- src/agents/sandbox/browser.ts | 5 ++--- src/agents/sandbox/docker.test.ts | 21 ++++++++++++++++++- src/agents/sandbox/docker.ts | 34 +++++++++++++++---------------- src/commands/doctor-sandbox.ts | 1 - 5 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/agents/sandbox.ts b/src/agents/sandbox.ts index 745910e090f..683721c3b28 100644 --- a/src/agents/sandbox.ts +++ b/src/agents/sandbox.ts @@ -18,7 +18,7 @@ export { requireSandboxBackendFactory, } from "./sandbox/backend.js"; -export { buildSandboxCreateArgs } from "./sandbox/docker.js"; +export { buildSandboxCreateArgs, isDockerDaemonUnavailable } from "./sandbox/docker.js"; export { listSandboxBrowsers, listSandboxContainers, diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index c4d37512607..dac3f84d17c 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -26,7 +26,6 @@ import { buildSandboxCreateArgs, dockerContainerState, execDocker, - execDockerRaw, isDockerDaemonUnavailable, readDockerContainerEnvVar, readDockerContainerLabel, @@ -34,7 +33,6 @@ import { readDockerNetworkGateway, readDockerPort, } from "./docker.js"; -import type { ExecDockerRawOptions } from "./docker.js"; import { buildNoVncObserverTokenUrl, consumeNoVncObserverToken, @@ -46,7 +44,7 @@ import { import { readBrowserRegistry, updateBrowserRegistry } from "./registry.js"; import { resolveSandboxAgentId, slugifySessionKey } from "./shared.js"; import { isToolAllowed } from "./tool-policy.js"; -import type { SandboxBrowserConfig, SandboxBrowserContext, SandboxConfig } from "./types.js"; +import type { SandboxBrowserContext, SandboxConfig } from "./types.js"; import { validateNetworkMode } from "./validate-sandbox-security.js"; import { appendWorkspaceMountArgs, SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js"; @@ -126,6 +124,7 @@ function buildSandboxBrowserResolvedConfig(params: { }; } +async function ensureSandboxBrowserImage(image: string) { const result = await execDocker(["image", "inspect", image], { allowFailure: true, }); diff --git a/src/agents/sandbox/docker.test.ts b/src/agents/sandbox/docker.test.ts index 2dca47efcd8..2cae9429137 100644 --- a/src/agents/sandbox/docker.test.ts +++ b/src/agents/sandbox/docker.test.ts @@ -18,6 +18,7 @@ type MockDockerChild = EventEmitter & { const spawnState = vi.hoisted(() => ({ calls: [] as SpawnCall[], imageExists: true, + inspectError: "", })); function createMockDockerChild(): MockDockerChild { @@ -40,7 +41,9 @@ function spawnDockerProcess(command: string, args: string[]) { 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]}`; + stderr = spawnState.imageExists + ? "" + : spawnState.inspectError || `Error response from daemon: No such image: ${args[2]}`; } else if (args[0] === "pull" || args[0] === "tag") { code = 0; } else { @@ -79,6 +82,7 @@ describe("ensureDockerImage", () => { beforeEach(async () => { spawnState.calls.length = 0; spawnState.imageExists = true; + spawnState.inspectError = ""; await loadFreshDockerModuleForTest(); }); @@ -113,4 +117,19 @@ describe("ensureDockerImage", () => { }, ]); }); + + it("returns when the Docker daemon is unavailable during image inspection", async () => { + spawnState.imageExists = false; + spawnState.inspectError = + "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?"; + + await ensureDockerImage(DEFAULT_SANDBOX_IMAGE); + + 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 51910f460bf..6d9eca9865e 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -272,44 +272,42 @@ export async function readDockerPort(containerName: string, port: number) { return Number.isFinite(mapped) ? mapped : null; } +const DOCKER_DAEMON_UNAVAILABLE_MARKERS = [ + "cannot connect to the docker daemon", + "dial unix", + "docker daemon is not running", + "connection refused", +]; + export function isDockerDaemonUnavailable(stderr: string): boolean { - const lower = stderr.toLowerCase(); - return ( - lower.includes("cannot connect to the docker daemon") || - lower.includes("dial unix") || - lower.includes("docker daemon is not running") || - lower.includes("connection refused") - ); + return DOCKER_DAEMON_UNAVAILABLE_MARKERS.some((marker) => stderr.toLowerCase().includes(marker)); } -async function dockerImageExists(image: string) { +async function inspectDockerImage(image: string): Promise<"exists" | "missing" | "unavailable"> { const result = await execDocker(["image", "inspect", image], { allowFailure: true, }); if (result.code === 0) { - return true; + return "exists"; } const stderr = result.stderr.trim(); - if (stderr.includes("No such image")) { - return false; + if (stderr.toLowerCase().includes("no such image")) { + return "missing"; } - // When Docker daemon is unavailable, treat the image as non-existent + // When Docker daemon is unavailable, treat the image as unavailable // rather than throwing. This allows sandbox.mode="off" sessions to // start without a running Docker daemon. if (isDockerDaemonUnavailable(stderr)) { - return false; + return "unavailable"; } throw new Error(`Failed to inspect sandbox image: ${stderr}`); } export async function ensureDockerImage(image: string) { - const exists = await dockerImageExists(image); - if (exists) { + const imageState = await inspectDockerImage(image); + if (imageState === "exists" || imageState === "unavailable") { return; } - // When Docker daemon is unavailable, silently return — the caller will - // fail later if it actually needs Docker, but for sandbox.mode="off" - // sessions this prevents unnecessary probe errors. if (image === DEFAULT_SANDBOX_IMAGE) { 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.`, diff --git a/src/commands/doctor-sandbox.ts b/src/commands/doctor-sandbox.ts index 0bf5c509dad..fda5fd5d849 100644 --- a/src/commands/doctor-sandbox.ts +++ b/src/commands/doctor-sandbox.ts @@ -86,7 +86,6 @@ async function dockerImageExists(image: string): Promise { if (stderr.includes("No such image")) { return false; } - const lower = stderr.toLowerCase(); if (isDockerDaemonUnavailable(stderr)) { return false; }