From 51decc6535ad3cb0bbc388bec97984da34d89fc0 Mon Sep 17 00:00:00 2001 From: Muhsinun Chowdhury <11369216+MuhsinunC@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:38:02 -0500 Subject: [PATCH] feat(sandbox): add Docker-in-Docker path remapping for browser sandboxes When the gateway runs inside a Docker container and creates sandbox containers, volume mount paths need to be host paths, not container paths. Changes: - Add remapPathForDinD() function to remap container paths to host paths - Add CLAWDBOT_SANDBOX_HOST_CONFIG_DIR and CLAWDBOT_SANDBOX_HOST_WORKSPACE_DIR environment variables to docker-compose.yml - Use path remapping in both sandbox container and browser sandbox creation - Add Docker CLI to gateway Dockerfile for Docker-in-Docker support The path remapping is a no-op when the environment variables are not set, so bare metal installations are unaffected. Co-Authored-By: Claude Opus 4.5 --- Dockerfile | 12 ++++++++++ docker-compose.yml | 3 +++ src/agents/sandbox/browser.ts | 8 +++++-- src/agents/sandbox/docker.ts | 43 +++++++++++++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9c6aa7036..a7a16bd70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,18 @@ FROM node:22-bookworm RUN curl -fsSL https://bun.sh/install | bash ENV PATH="/root/.bun/bin:${PATH}" +# Install Docker CLI from official Docker repo (Debian docker.io is too old for Docker Desktop) +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl && \ + install -m 0755 -d /etc/apt/keyrings && \ + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \ + chmod a+r /etc/apt/keyrings/docker.asc && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends docker-ce-cli && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* + RUN corepack enable WORKDIR /app diff --git a/docker-compose.yml b/docker-compose.yml index 9324c521d..1f5dd26e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,9 @@ services: CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} + # Docker-in-Docker: host paths for sandbox container volume mounts + CLAWDBOT_SANDBOX_HOST_CONFIG_DIR: ${CLAWDBOT_CONFIG_DIR} + CLAWDBOT_SANDBOX_HOST_WORKSPACE_DIR: ${CLAWDBOT_WORKSPACE_DIR} volumes: - ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot - ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index 92ecf1666..4a6648c7f 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -12,6 +12,7 @@ import { dockerContainerState, execDocker, readDockerPort, + remapPathForDinD, } from "./docker.js"; import { updateBrowserRegistry } from "./registry.js"; import { slugifySessionKey } from "./shared.js"; @@ -134,12 +135,15 @@ export async function ensureSandboxBrowser(params: { params.cfg.workspaceAccess === "ro" && params.workspaceDir === params.agentWorkspaceDir ? ":ro" : ""; - args.push("-v", `${params.workspaceDir}:${params.cfg.docker.workdir}${mainMountSuffix}`); + // Remap paths for Docker-in-Docker scenarios + const hostWorkspaceDir = remapPathForDinD(params.workspaceDir); + args.push("-v", `${hostWorkspaceDir}:${params.cfg.docker.workdir}${mainMountSuffix}`); if (params.cfg.workspaceAccess !== "none" && params.workspaceDir !== params.agentWorkspaceDir) { const agentMountSuffix = params.cfg.workspaceAccess === "ro" ? ":ro" : ""; + const hostAgentWorkspaceDir = remapPathForDinD(params.agentWorkspaceDir); args.push( "-v", - `${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`, + `${hostAgentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`, ); } args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`); diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index 21e8c67b5..6a0a5d524 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -1,3 +1,4 @@ +import os from "node:os"; import { spawn } from "node:child_process"; import { defaultRuntime } from "../../runtime.js"; @@ -8,6 +9,41 @@ import { computeSandboxConfigHash } from "./config-hash.js"; import { resolveSandboxAgentId, resolveSandboxScopeKey, slugifySessionKey } from "./shared.js"; import type { SandboxConfig, SandboxDockerConfig, SandboxWorkspaceAccess } from "./types.js"; +/** + * For Docker-in-Docker scenarios, remap container paths to host paths. + * When the gateway runs in a container and creates sandbox containers, + * the volume mount paths must be host paths, not container paths. + * + * Uses environment variables set by docker-compose: + * - CLAWDBOT_SANDBOX_HOST_CONFIG_DIR: host path for ~/.clawdbot + * - CLAWDBOT_SANDBOX_HOST_WORKSPACE_DIR: host path for ~/clawd + */ +export function remapPathForDinD(containerPath: string): string { + const hostConfigDir = process.env.CLAWDBOT_SANDBOX_HOST_CONFIG_DIR; + const hostWorkspaceDir = process.env.CLAWDBOT_SANDBOX_HOST_WORKSPACE_DIR; + + // If no host path mappings are set, we're not in Docker-in-Docker mode + if (!hostConfigDir && !hostWorkspaceDir) { + return containerPath; + } + + const home = os.homedir(); + const containerConfigDir = `${home}/.clawdbot`; + const containerWorkspaceDir = `${home}/clawd`; + + // Remap config directory paths + if (hostConfigDir && containerPath.startsWith(containerConfigDir)) { + return containerPath.replace(containerConfigDir, hostConfigDir); + } + + // Remap workspace directory paths + if (hostWorkspaceDir && containerPath.startsWith(containerWorkspaceDir)) { + return containerPath.replace(containerWorkspaceDir, hostWorkspaceDir); + } + + return containerPath; +} + const HOT_CONTAINER_WINDOW_MS = 5 * 60 * 1000; export function execDocker(args: string[], opts?: { allowFailure?: boolean }) { @@ -189,12 +225,15 @@ async function createSandboxContainer(params: { args.push("--workdir", cfg.workdir); const mainMountSuffix = params.workspaceAccess === "ro" && workspaceDir === params.agentWorkspaceDir ? ":ro" : ""; - args.push("-v", `${workspaceDir}:${cfg.workdir}${mainMountSuffix}`); + // Remap paths for Docker-in-Docker scenarios + const hostWorkspaceDir = remapPathForDinD(workspaceDir); + args.push("-v", `${hostWorkspaceDir}:${cfg.workdir}${mainMountSuffix}`); if (params.workspaceAccess !== "none" && workspaceDir !== params.agentWorkspaceDir) { const agentMountSuffix = params.workspaceAccess === "ro" ? ":ro" : ""; + const hostAgentWorkspaceDir = remapPathForDinD(params.agentWorkspaceDir); args.push( "-v", - `${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`, + `${hostAgentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`, ); } args.push(cfg.image, "sleep", "infinity");