diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f0da5ec..a23fca541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.clawd.bot - Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS. - Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo. - Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo. +- Sandbox: log docker image inspect failures during doctor checks. (#1548) Thanks @sweepies. - TUI: forward unknown slash commands (for example, `/context`) to the Gateway. - TUI: include Gateway slash commands in autocomplete and `/help`. - CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable. diff --git a/src/agents/sandbox-docker-image-check-errors.test.ts b/src/agents/sandbox-docker-image-check-errors.test.ts new file mode 100644 index 000000000..0201cc682 --- /dev/null +++ b/src/agents/sandbox-docker-image-check-errors.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { resolveDockerImageInspectResult } from "./sandbox/docker.js"; + +describe("ensureDockerImage", () => { + it("surfaces inspect failures with detail", async () => { + const result = resolveDockerImageInspectResult("custom-sandbox:latest", { + stdout: "", + stderr: "permission denied while trying to connect to the Docker daemon socket", + code: 1, + }); + + expect(result).toEqual({ + exists: false, + error: + "Failed to inspect sandbox image: permission denied while trying to connect to the Docker daemon socket", + }); + }); +}); diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index 3bc2cc5a0..5644fb4f7 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -46,16 +46,25 @@ export async function readDockerPort(containerName: string, port: number) { return Number.isFinite(mapped) ? mapped : null; } +export function resolveDockerImageInspectResult( + image: string, + result: { stdout: string; stderr: string; code: number }, +): { exists: boolean; error?: string } { + if (result.code === 0) return { exists: true }; + const stderr = result.stderr.trim(); + if (/no such image/i.test(stderr)) return { exists: false }; + const stdout = result.stdout.trim(); + const detail = stderr || stdout || `docker image inspect ${image} failed (exit ${result.code})`; + return { exists: false, error: `Failed to inspect sandbox image: ${detail}` }; +} + async function dockerImageExists(image: string) { const result = await execDocker(["image", "inspect", image], { allowFailure: true, }); - if (result.code === 0) return true; - const stderr = result.stderr.trim(); - if (stderr.includes("No such image")) { - return false; - } - throw new Error(`Failed to inspect sandbox image: ${stderr}`); + const resolved = resolveDockerImageInspectResult(image, result); + if (resolved.error) throw new Error(resolved.error); + return resolved.exists; } export async function ensureDockerImage(image: string) { diff --git a/src/commands/doctor-sandbox-image-inspect-errors.test.ts b/src/commands/doctor-sandbox-image-inspect-errors.test.ts new file mode 100644 index 000000000..0b05290b0 --- /dev/null +++ b/src/commands/doctor-sandbox-image-inspect-errors.test.ts @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { ClawdbotConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; +import type { DoctorPrompter } from "./doctor-prompter.js"; + +const runExec = vi.fn(); +const runCommandWithTimeout = vi.fn(); +const note = vi.fn(); + +vi.mock("../process/exec.js", () => ({ + runExec, + runCommandWithTimeout, +})); +vi.mock("../terminal/note.js", () => ({ + note, +})); + +describe("maybeRepairSandboxImages", () => { + beforeEach(() => { + runExec.mockReset(); + runCommandWithTimeout.mockReset(); + note.mockReset(); + }); + + it("logs docker inspect errors and continues", async () => { + runExec.mockImplementation(async (_command: string, args: string[]) => { + if (args[0] === "version") return { stdout: "26.0.0", stderr: "" }; + if (args[0] === "image" && args[1] === "inspect") { + const err = new Error( + "permission denied while trying to connect to the Docker daemon socket", + ) as Error & { stderr?: string }; + err.stderr = "permission denied while trying to connect to the Docker daemon socket"; + throw err; + } + return { stdout: "", stderr: "" }; + }); + + const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js"); + const cfg = { + agents: { + defaults: { + sandbox: { + mode: "all", + docker: { image: "custom-sandbox:latest" }, + }, + }, + }, + } satisfies ClawdbotConfig; + + const runtime = { log: vi.fn(), error: vi.fn() } as RuntimeEnv; + const prompter = { confirmSkipInNonInteractive: vi.fn() } as DoctorPrompter; + + await expect(maybeRepairSandboxImages(cfg, runtime, prompter)).resolves.toBe(cfg); + expect(note).toHaveBeenCalledWith( + expect.stringContaining("Unable to inspect sandbox base image (custom-sandbox:latest)"), + "Sandbox", + ); + expect(runCommandWithTimeout).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/doctor-sandbox.ts b/src/commands/doctor-sandbox.ts index d46af4ca4..2b34ffb55 100644 --- a/src/commands/doctor-sandbox.ts +++ b/src/commands/doctor-sandbox.ts @@ -79,11 +79,14 @@ async function dockerImageExists(image: string): Promise { await runExec("docker", ["image", "inspect", image], { timeoutMs: 5_000 }); return true; } catch (error: any) { - const stderr = error?.stderr || error?.message || ""; - if (String(stderr).includes("No such image")) { + const stderr = typeof error?.stderr === "string" ? error.stderr.trim() : ""; + const stdout = typeof error?.stdout === "string" ? error.stdout.trim() : ""; + const message = typeof error?.message === "string" ? error.message.trim() : ""; + const detail = stderr || stdout || message; + if (/no such image/i.test(detail)) { return false; } - throw error; + throw new Error(`Failed to inspect sandbox image: ${detail || "unknown error"}`); } } @@ -147,7 +150,17 @@ async function handleMissingSandboxImage( runtime: RuntimeEnv, prompter: DoctorPrompter, ) { - const exists = await dockerImageExists(params.image); + let exists = false; + try { + exists = await dockerImageExists(params.image); + } catch (error: any) { + const stderr = typeof error?.stderr === "string" ? error.stderr.trim() : ""; + const stdout = typeof error?.stdout === "string" ? error.stdout.trim() : ""; + const message = typeof error?.message === "string" ? error.message.trim() : ""; + const detail = stderr || stdout || message || "unknown error"; + note(`Unable to inspect sandbox ${params.label} image (${params.image}): ${detail}`, "Sandbox"); + return; + } if (exists) return; const buildHint = params.buildScript