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:
parent
fc0fca5108
commit
51decc6535
12
Dockerfile
12
Dockerfile
@ -4,6 +4,18 @@ FROM node:22-bookworm
|
|||||||
RUN curl -fsSL https://bun.sh/install | bash
|
RUN curl -fsSL https://bun.sh/install | bash
|
||||||
ENV PATH="/root/.bun/bin:${PATH}"
|
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
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@ -9,6 +9,9 @@ services:
|
|||||||
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY}
|
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY}
|
||||||
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY}
|
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY}
|
||||||
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE}
|
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:
|
volumes:
|
||||||
- ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot
|
- ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot
|
||||||
- ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd
|
- ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
dockerContainerState,
|
dockerContainerState,
|
||||||
execDocker,
|
execDocker,
|
||||||
readDockerPort,
|
readDockerPort,
|
||||||
|
remapPathForDinD,
|
||||||
} from "./docker.js";
|
} from "./docker.js";
|
||||||
import { updateBrowserRegistry } from "./registry.js";
|
import { updateBrowserRegistry } from "./registry.js";
|
||||||
import { slugifySessionKey } from "./shared.js";
|
import { slugifySessionKey } from "./shared.js";
|
||||||
@ -134,12 +135,15 @@ export async function ensureSandboxBrowser(params: {
|
|||||||
params.cfg.workspaceAccess === "ro" && params.workspaceDir === params.agentWorkspaceDir
|
params.cfg.workspaceAccess === "ro" && params.workspaceDir === params.agentWorkspaceDir
|
||||||
? ":ro"
|
? ":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) {
|
if (params.cfg.workspaceAccess !== "none" && params.workspaceDir !== params.agentWorkspaceDir) {
|
||||||
const agentMountSuffix = params.cfg.workspaceAccess === "ro" ? ":ro" : "";
|
const agentMountSuffix = params.cfg.workspaceAccess === "ro" ? ":ro" : "";
|
||||||
|
const hostAgentWorkspaceDir = remapPathForDinD(params.agentWorkspaceDir);
|
||||||
args.push(
|
args.push(
|
||||||
"-v",
|
"-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}`);
|
args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import os from "node:os";
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
@ -8,6 +9,41 @@ import { computeSandboxConfigHash } from "./config-hash.js";
|
|||||||
import { resolveSandboxAgentId, resolveSandboxScopeKey, slugifySessionKey } from "./shared.js";
|
import { resolveSandboxAgentId, resolveSandboxScopeKey, slugifySessionKey } from "./shared.js";
|
||||||
import type { SandboxConfig, SandboxDockerConfig, SandboxWorkspaceAccess } from "./types.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;
|
const HOT_CONTAINER_WINDOW_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
export function execDocker(args: string[], opts?: { allowFailure?: boolean }) {
|
export function execDocker(args: string[], opts?: { allowFailure?: boolean }) {
|
||||||
@ -189,12 +225,15 @@ async function createSandboxContainer(params: {
|
|||||||
args.push("--workdir", cfg.workdir);
|
args.push("--workdir", cfg.workdir);
|
||||||
const mainMountSuffix =
|
const mainMountSuffix =
|
||||||
params.workspaceAccess === "ro" && workspaceDir === params.agentWorkspaceDir ? ":ro" : "";
|
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) {
|
if (params.workspaceAccess !== "none" && workspaceDir !== params.agentWorkspaceDir) {
|
||||||
const agentMountSuffix = params.workspaceAccess === "ro" ? ":ro" : "";
|
const agentMountSuffix = params.workspaceAccess === "ro" ? ":ro" : "";
|
||||||
|
const hostAgentWorkspaceDir = remapPathForDinD(params.agentWorkspaceDir);
|
||||||
args.push(
|
args.push(
|
||||||
"-v",
|
"-v",
|
||||||
`${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`,
|
`${hostAgentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
args.push(cfg.image, "sleep", "infinity");
|
args.push(cfg.image, "sleep", "infinity");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user