Skip to content

Commit 2dadc82

Browse files
kaseonedgesallyom
andauthored
fix(sandbox): gracefully handle Docker daemon unavailability when sandbox mode is off (#73671)
Merged via squash. Prepared head SHA: 378851c Co-authored-by: kaseonedge <[email protected]> Co-authored-by: sallyom <[email protected]> Reviewed-by: @sallyom
1 parent e46dccb commit 2dadc82

5 files changed

Lines changed: 55 additions & 8 deletions

File tree

src/agents/sandbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export {
1818
requireSandboxBackendFactory,
1919
} from "./sandbox/backend.js";
2020

21-
export { buildSandboxCreateArgs } from "./sandbox/docker.js";
21+
export { buildSandboxCreateArgs, isDockerDaemonUnavailable } from "./sandbox/docker.js";
2222
export {
2323
listSandboxBrowsers,
2424
listSandboxContainers,

src/agents/sandbox/browser.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
buildSandboxCreateArgs,
2727
dockerContainerState,
2828
execDocker,
29+
isDockerDaemonUnavailable,
2930
readDockerContainerEnvVar,
3031
readDockerContainerLabel,
3132
readDockerNetworkDriver,
@@ -130,6 +131,12 @@ async function ensureSandboxBrowserImage(image: string) {
130131
if (result.code === 0) {
131132
return;
132133
}
134+
const stderr = result.stderr.trim();
135+
// When Docker daemon is unavailable, silently return instead of throwing.
136+
// This allows sandbox.mode="off" sessions to start without Docker errors.
137+
if (isDockerDaemonUnavailable(stderr)) {
138+
return;
139+
}
133140
throw new Error(
134141
`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`,
135142
);

src/agents/sandbox/docker.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type MockDockerChild = EventEmitter & {
1818
const spawnState = vi.hoisted(() => ({
1919
calls: [] as SpawnCall[],
2020
imageExists: true,
21+
inspectError: "",
2122
}));
2223

2324
function createMockDockerChild(): MockDockerChild {
@@ -40,7 +41,9 @@ function spawnDockerProcess(command: string, args: string[]) {
4041
stderr = `unexpected command: ${command}`;
4142
} else if (args[0] === "image" && args[1] === "inspect") {
4243
code = spawnState.imageExists ? 0 : 1;
43-
stderr = spawnState.imageExists ? "" : `Error response from daemon: No such image: ${args[2]}`;
44+
stderr = spawnState.imageExists
45+
? ""
46+
: spawnState.inspectError || `Error response from daemon: No such image: ${args[2]}`;
4447
} else if (args[0] === "pull" || args[0] === "tag") {
4548
code = 0;
4649
} else {
@@ -79,6 +82,7 @@ describe("ensureDockerImage", () => {
7982
beforeEach(async () => {
8083
spawnState.calls.length = 0;
8184
spawnState.imageExists = true;
85+
spawnState.inspectError = "";
8286
await loadFreshDockerModuleForTest();
8387
});
8488

@@ -113,4 +117,19 @@ describe("ensureDockerImage", () => {
113117
},
114118
]);
115119
});
120+
121+
it("returns when the Docker daemon is unavailable during image inspection", async () => {
122+
spawnState.imageExists = false;
123+
spawnState.inspectError =
124+
"Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?";
125+
126+
await ensureDockerImage(DEFAULT_SANDBOX_IMAGE);
127+
128+
expect(spawnState.calls).toEqual([
129+
{
130+
command: "docker",
131+
args: ["image", "inspect", DEFAULT_SANDBOX_IMAGE],
132+
},
133+
]);
134+
});
116135
});

src/agents/sandbox/docker.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,23 +272,40 @@ export async function readDockerPort(containerName: string, port: number) {
272272
return Number.isFinite(mapped) ? mapped : null;
273273
}
274274

275-
async function dockerImageExists(image: string) {
275+
const DOCKER_DAEMON_UNAVAILABLE_MARKERS = [
276+
"cannot connect to the docker daemon",
277+
"dial unix",
278+
"docker daemon is not running",
279+
"connection refused",
280+
];
281+
282+
export function isDockerDaemonUnavailable(stderr: string): boolean {
283+
return DOCKER_DAEMON_UNAVAILABLE_MARKERS.some((marker) => stderr.toLowerCase().includes(marker));
284+
}
285+
286+
async function inspectDockerImage(image: string): Promise<"exists" | "missing" | "unavailable"> {
276287
const result = await execDocker(["image", "inspect", image], {
277288
allowFailure: true,
278289
});
279290
if (result.code === 0) {
280-
return true;
291+
return "exists";
281292
}
282293
const stderr = result.stderr.trim();
283-
if (stderr.includes("No such image")) {
284-
return false;
294+
if (stderr.toLowerCase().includes("no such image")) {
295+
return "missing";
296+
}
297+
// When Docker daemon is unavailable, treat the image as unavailable
298+
// rather than throwing. This allows sandbox.mode="off" sessions to
299+
// start without a running Docker daemon.
300+
if (isDockerDaemonUnavailable(stderr)) {
301+
return "unavailable";
285302
}
286303
throw new Error(`Failed to inspect sandbox image: ${stderr}`);
287304
}
288305

289306
export async function ensureDockerImage(image: string) {
290-
const exists = await dockerImageExists(image);
291-
if (exists) {
307+
const imageState = await inspectDockerImage(image);
308+
if (imageState === "exists" || imageState === "unavailable") {
292309
return;
293310
}
294311
if (image === DEFAULT_SANDBOX_IMAGE) {

src/commands/doctor-sandbox.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
DEFAULT_SANDBOX_BROWSER_IMAGE,
55
DEFAULT_SANDBOX_COMMON_IMAGE,
66
DEFAULT_SANDBOX_IMAGE,
7+
isDockerDaemonUnavailable,
78
resolveSandboxScope,
89
} from "../agents/sandbox.js";
910
import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -85,6 +86,9 @@ async function dockerImageExists(image: string): Promise<boolean> {
8586
if (stderr.includes("No such image")) {
8687
return false;
8788
}
89+
if (isDockerDaemonUnavailable(stderr)) {
90+
return false;
91+
}
8892
throw error;
8993
}
9094
}

0 commit comments

Comments
 (0)