mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:20:44 +00:00
fix: Found one reliability bug: the new Docker-daemon-unavailable bran (#74520)
Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
@@ -101,6 +101,58 @@ describe("resolveSandboxContext", () => {
|
||||
expect(result).toBeNull();
|
||||
}, 15_000);
|
||||
|
||||
it("does not touch sandbox backends for cron or sub-agent sessions when sandbox mode is off", async () => {
|
||||
const backendFactory = vi.fn(async () => ({
|
||||
id: "test-off-backend",
|
||||
runtimeId: "unexpected-runtime",
|
||||
runtimeLabel: "Unexpected Runtime",
|
||||
workdir: "/workspace",
|
||||
buildExecSpec: async () => ({
|
||||
argv: ["unexpected"],
|
||||
env: process.env,
|
||||
stdinMode: "pipe-closed" as const,
|
||||
}),
|
||||
runShellCommand: async () => ({
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.alloc(0),
|
||||
code: 0,
|
||||
}),
|
||||
}));
|
||||
const restore = registerSandboxBackend("test-off-backend", backendFactory);
|
||||
try {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "off",
|
||||
backend: "test-off-backend",
|
||||
scope: "session",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
resolveSandboxContext({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:cron:job:run:uuid",
|
||||
workspaceDir: "/tmp/openclaw-test",
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
resolveSandboxContext({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:subagent:child",
|
||||
workspaceDir: "/tmp/openclaw-test",
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
|
||||
expect(backendFactory).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
}, 15_000);
|
||||
|
||||
it("treats main session aliases as main in non-main mode", async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
session: { mainKey: "work" },
|
||||
|
||||
@@ -236,6 +236,34 @@ describe("ensureSandboxBrowser create args", () => {
|
||||
expect(result?.noVncUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails before creating a browser container when Docker daemon is unavailable", async () => {
|
||||
dockerMocks.execDocker.mockImplementation(async (args: string[]) => {
|
||||
if (args[0] === "network" && args[1] === "inspect") {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
}
|
||||
if (args[0] === "image" && args[1] === "inspect") {
|
||||
return {
|
||||
stdout: "",
|
||||
stderr:
|
||||
"Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?",
|
||||
code: 1,
|
||||
};
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
});
|
||||
|
||||
await expect(
|
||||
ensureTestSandboxBrowser({
|
||||
scopeKey: "session:test",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
cfg: buildConfig(false),
|
||||
}),
|
||||
).rejects.toThrow("Docker daemon is not available");
|
||||
|
||||
expect(findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes the browser SSRF policy to the sandbox bridge", async () => {
|
||||
await ensureTestSandboxBrowser({
|
||||
scopeKey: "session:test",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
buildSandboxCreateArgs,
|
||||
dockerContainerState,
|
||||
execDocker,
|
||||
formatDockerDaemonUnavailableError,
|
||||
isDockerDaemonUnavailable,
|
||||
readDockerContainerEnvVar,
|
||||
readDockerContainerLabel,
|
||||
@@ -132,10 +133,8 @@ async function ensureSandboxBrowserImage(image: string) {
|
||||
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(formatDockerDaemonUnavailableError(stderr));
|
||||
}
|
||||
throw new Error(
|
||||
`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`,
|
||||
|
||||
@@ -118,12 +118,14 @@ describe("ensureDockerImage", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns when the Docker daemon is unavailable during image inspection", async () => {
|
||||
it("throws 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);
|
||||
await expect(ensureDockerImage(DEFAULT_SANDBOX_IMAGE)).rejects.toThrow(
|
||||
"Docker daemon is not available",
|
||||
);
|
||||
|
||||
expect(spawnState.calls).toEqual([
|
||||
{
|
||||
|
||||
@@ -283,7 +283,18 @@ 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"> {
|
||||
export function formatDockerDaemonUnavailableError(stderr: string): string {
|
||||
const detail = stderr.trim();
|
||||
return [
|
||||
"Sandbox mode requires Docker, but the Docker daemon is not available.",
|
||||
"Start Docker, or set `agents.defaults.sandbox.mode=off` to disable sandboxing.",
|
||||
detail ? `Docker said: ${detail}` : undefined,
|
||||
]
|
||||
.filter((line): line is string => Boolean(line))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
async function inspectDockerImage(image: string): Promise<"exists" | "missing"> {
|
||||
const result = await execDocker(["image", "inspect", image], {
|
||||
allowFailure: true,
|
||||
});
|
||||
@@ -294,18 +305,15 @@ async function inspectDockerImage(image: string): Promise<"exists" | "missing" |
|
||||
if (stderr.toLowerCase().includes("no such image")) {
|
||||
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(formatDockerDaemonUnavailableError(stderr));
|
||||
}
|
||||
throw new Error(`Failed to inspect sandbox image: ${stderr}`);
|
||||
}
|
||||
|
||||
export async function ensureDockerImage(image: string) {
|
||||
const imageState = await inspectDockerImage(image);
|
||||
if (imageState === "exists" || imageState === "unavailable") {
|
||||
if (imageState === "exists") {
|
||||
return;
|
||||
}
|
||||
if (image === DEFAULT_SANDBOX_IMAGE) {
|
||||
|
||||
Reference in New Issue
Block a user