fix(sandbox): gracefully handle Docker daemon unavailability when sandbox mode is off (#73671)

Merged via squash.

Prepared head SHA: 378851cf40
Co-authored-by: kaseonedge <15183881+kaseonedge@users.noreply.github.com>
Co-authored-by: sallyom <11166065+sallyom@users.noreply.github.com>
Reviewed-by: @sallyom
This commit is contained in:
edge_kase
2026-04-29 09:23:30 -07:00
committed by GitHub
parent e46dccb353
commit 2dadc82cf4
5 changed files with 55 additions and 8 deletions

View File

@@ -18,7 +18,7 @@ export {
requireSandboxBackendFactory, requireSandboxBackendFactory,
} from "./sandbox/backend.js"; } from "./sandbox/backend.js";
export { buildSandboxCreateArgs } from "./sandbox/docker.js"; export { buildSandboxCreateArgs, isDockerDaemonUnavailable } from "./sandbox/docker.js";
export { export {
listSandboxBrowsers, listSandboxBrowsers,
listSandboxContainers, listSandboxContainers,

View File

@@ -26,6 +26,7 @@ import {
buildSandboxCreateArgs, buildSandboxCreateArgs,
dockerContainerState, dockerContainerState,
execDocker, execDocker,
isDockerDaemonUnavailable,
readDockerContainerEnvVar, readDockerContainerEnvVar,
readDockerContainerLabel, readDockerContainerLabel,
readDockerNetworkDriver, readDockerNetworkDriver,
@@ -130,6 +131,12 @@ async function ensureSandboxBrowserImage(image: string) {
if (result.code === 0) { if (result.code === 0) {
return; return;
} }
const stderr = result.stderr.trim();
// When Docker daemon is unavailable, silently return instead of throwing.
// This allows sandbox.mode="off" sessions to start without Docker errors.
if (isDockerDaemonUnavailable(stderr)) {
return;
}
throw new Error( throw new Error(
`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`, `Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`,
); );

View File

@@ -18,6 +18,7 @@ type MockDockerChild = EventEmitter & {
const spawnState = vi.hoisted(() => ({ const spawnState = vi.hoisted(() => ({
calls: [] as SpawnCall[], calls: [] as SpawnCall[],
imageExists: true, imageExists: true,
inspectError: "",
})); }));
function createMockDockerChild(): MockDockerChild { function createMockDockerChild(): MockDockerChild {
@@ -40,7 +41,9 @@ function spawnDockerProcess(command: string, args: string[]) {
stderr = `unexpected command: ${command}`; stderr = `unexpected command: ${command}`;
} else if (args[0] === "image" && args[1] === "inspect") { } else if (args[0] === "image" && args[1] === "inspect") {
code = spawnState.imageExists ? 0 : 1; 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") { } else if (args[0] === "pull" || args[0] === "tag") {
code = 0; code = 0;
} else { } else {
@@ -79,6 +82,7 @@ describe("ensureDockerImage", () => {
beforeEach(async () => { beforeEach(async () => {
spawnState.calls.length = 0; spawnState.calls.length = 0;
spawnState.imageExists = true; spawnState.imageExists = true;
spawnState.inspectError = "";
await loadFreshDockerModuleForTest(); 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],
},
]);
});
}); });

View File

@@ -272,23 +272,40 @@ export async function readDockerPort(containerName: string, port: number) {
return Number.isFinite(mapped) ? mapped : null; return Number.isFinite(mapped) ? mapped : null;
} }
async function dockerImageExists(image: string) { 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 {
return DOCKER_DAEMON_UNAVAILABLE_MARKERS.some((marker) => stderr.toLowerCase().includes(marker));
}
async function inspectDockerImage(image: string): Promise<"exists" | "missing" | "unavailable"> {
const result = await execDocker(["image", "inspect", image], { const result = await execDocker(["image", "inspect", image], {
allowFailure: true, allowFailure: true,
}); });
if (result.code === 0) { if (result.code === 0) {
return true; return "exists";
} }
const stderr = result.stderr.trim(); const stderr = result.stderr.trim();
if (stderr.includes("No such image")) { if (stderr.toLowerCase().includes("no such image")) {
return false; return "missing";
}
// 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 "unavailable";
} }
throw new Error(`Failed to inspect sandbox image: ${stderr}`); throw new Error(`Failed to inspect sandbox image: ${stderr}`);
} }
export async function ensureDockerImage(image: string) { export async function ensureDockerImage(image: string) {
const exists = await dockerImageExists(image); const imageState = await inspectDockerImage(image);
if (exists) { if (imageState === "exists" || imageState === "unavailable") {
return; return;
} }
if (image === DEFAULT_SANDBOX_IMAGE) { if (image === DEFAULT_SANDBOX_IMAGE) {

View File

@@ -4,6 +4,7 @@ import {
DEFAULT_SANDBOX_BROWSER_IMAGE, DEFAULT_SANDBOX_BROWSER_IMAGE,
DEFAULT_SANDBOX_COMMON_IMAGE, DEFAULT_SANDBOX_COMMON_IMAGE,
DEFAULT_SANDBOX_IMAGE, DEFAULT_SANDBOX_IMAGE,
isDockerDaemonUnavailable,
resolveSandboxScope, resolveSandboxScope,
} from "../agents/sandbox.js"; } from "../agents/sandbox.js";
import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -85,6 +86,9 @@ async function dockerImageExists(image: string): Promise<boolean> {
if (stderr.includes("No such image")) { if (stderr.includes("No such image")) {
return false; return false;
} }
if (isDockerDaemonUnavailable(stderr)) {
return false;
}
throw error; throw error;
} }
} }