Skip to content

Commit 378851c

Browse files
committed
fix: repair sandbox Docker daemon fallback
1 parent b64e266 commit 378851c

5 files changed

Lines changed: 39 additions & 24 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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ import {
2626
buildSandboxCreateArgs,
2727
dockerContainerState,
2828
execDocker,
29-
execDockerRaw,
3029
isDockerDaemonUnavailable,
3130
readDockerContainerEnvVar,
3231
readDockerContainerLabel,
3332
readDockerNetworkDriver,
3433
readDockerNetworkGateway,
3534
readDockerPort,
3635
} from "./docker.js";
37-
import type { ExecDockerRawOptions } from "./docker.js";
3836
import {
3937
buildNoVncObserverTokenUrl,
4038
consumeNoVncObserverToken,
@@ -46,7 +44,7 @@ import {
4644
import { readBrowserRegistry, updateBrowserRegistry } from "./registry.js";
4745
import { resolveSandboxAgentId, slugifySessionKey } from "./shared.js";
4846
import { isToolAllowed } from "./tool-policy.js";
49-
import type { SandboxBrowserConfig, SandboxBrowserContext, SandboxConfig } from "./types.js";
47+
import type { SandboxBrowserContext, SandboxConfig } from "./types.js";
5048
import { validateNetworkMode } from "./validate-sandbox-security.js";
5149
import { appendWorkspaceMountArgs, SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js";
5250

@@ -126,6 +124,7 @@ function buildSandboxBrowserResolvedConfig(params: {
126124
};
127125
}
128126

127+
async function ensureSandboxBrowserImage(image: string) {
129128
const result = await execDocker(["image", "inspect", image], {
130129
allowFailure: true,
131130
});

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: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -272,44 +272,42 @@ export async function readDockerPort(containerName: string, port: number) {
272272
return Number.isFinite(mapped) ? mapped : null;
273273
}
274274

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+
275282
export function isDockerDaemonUnavailable(stderr: string): boolean {
276-
const lower = stderr.toLowerCase();
277-
return (
278-
lower.includes("cannot connect to the docker daemon") ||
279-
lower.includes("dial unix") ||
280-
lower.includes("docker daemon is not running") ||
281-
lower.includes("connection refused")
282-
);
283+
return DOCKER_DAEMON_UNAVAILABLE_MARKERS.some((marker) => stderr.toLowerCase().includes(marker));
283284
}
284285

285-
async function dockerImageExists(image: string) {
286+
async function inspectDockerImage(image: string): Promise<"exists" | "missing" | "unavailable"> {
286287
const result = await execDocker(["image", "inspect", image], {
287288
allowFailure: true,
288289
});
289290
if (result.code === 0) {
290-
return true;
291+
return "exists";
291292
}
292293
const stderr = result.stderr.trim();
293-
if (stderr.includes("No such image")) {
294-
return false;
294+
if (stderr.toLowerCase().includes("no such image")) {
295+
return "missing";
295296
}
296-
// When Docker daemon is unavailable, treat the image as non-existent
297+
// When Docker daemon is unavailable, treat the image as unavailable
297298
// rather than throwing. This allows sandbox.mode="off" sessions to
298299
// start without a running Docker daemon.
299300
if (isDockerDaemonUnavailable(stderr)) {
300-
return false;
301+
return "unavailable";
301302
}
302303
throw new Error(`Failed to inspect sandbox image: ${stderr}`);
303304
}
304305

305306
export async function ensureDockerImage(image: string) {
306-
const exists = await dockerImageExists(image);
307-
if (exists) {
307+
const imageState = await inspectDockerImage(image);
308+
if (imageState === "exists" || imageState === "unavailable") {
308309
return;
309310
}
310-
// When Docker daemon is unavailable, silently return — the caller will
311-
// fail later if it actually needs Docker, but for sandbox.mode="off"
312-
// sessions this prevents unnecessary probe errors.
313311
if (image === DEFAULT_SANDBOX_IMAGE) {
314312
throw new Error(
315313
`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.`,

src/commands/doctor-sandbox.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ async function dockerImageExists(image: string): Promise<boolean> {
8686
if (stderr.includes("No such image")) {
8787
return false;
8888
}
89-
const lower = stderr.toLowerCase();
9089
if (isDockerDaemonUnavailable(stderr)) {
9190
return false;
9291
}

0 commit comments

Comments
 (0)