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 <noreply@anthropic.com>
This commit is contained in:
Muhsinun Chowdhury 2026-01-29 18:38:02 -05:00
parent fc0fca5108
commit 51decc6535
4 changed files with 62 additions and 4 deletions

View File

@ -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

View File

@ -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

View File

@ -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}`);

View File

@ -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");