From 174bac87cbeb9d60a7218d93b479c6e519c1bd2d Mon Sep 17 00:00:00 2001 From: Muhsinun Chowdhury <11369216+MuhsinunC@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:42:15 -0500 Subject: [PATCH] feat(sandbox): add cdpHost config for Docker gateway deployments When the gateway runs inside a Docker container, it needs to connect to sandbox browser containers via the host network. Chrome's CDP HTTP endpoints reject non-IP Host headers, so this change: 1. Adds `cdpHost` config option (default: "127.0.0.1") 2. Adds DNS resolution helper to convert hostnames to IPs 3. Updates sandbox browser entrypoint with --remote-allow-origins=* For Docker deployments, set `cdpHost: "host.docker.internal"` and the gateway will resolve it to the host's IP for Chrome compatibility. Co-Authored-By: Claude Opus 4.5 --- scripts/sandbox-browser-entrypoint.sh | 1 + src/agents/sandbox/browser.ts | 51 ++++++++++++++++++++++---- src/agents/sandbox/config.ts | 2 + src/agents/sandbox/constants.ts | 1 + src/agents/sandbox/types.ts | 1 + src/config/types.sandbox.ts | 6 +++ src/config/zod-schema.agent-runtime.ts | 1 + 7 files changed, 56 insertions(+), 7 deletions(-) diff --git a/scripts/sandbox-browser-entrypoint.sh b/scripts/sandbox-browser-entrypoint.sh index 7a1d6fdf7..660850aba 100755 --- a/scripts/sandbox-browser-entrypoint.sh +++ b/scripts/sandbox-browser-entrypoint.sh @@ -34,6 +34,7 @@ fi CHROME_ARGS+=( "--remote-debugging-address=127.0.0.1" "--remote-debugging-port=${CHROME_CDP_PORT}" + "--remote-allow-origins=*" "--user-data-dir=${HOME}/.chrome" "--no-first-run" "--no-default-browser-check" diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index e085030b5..92ecf1666 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -1,3 +1,4 @@ +import dns from "node:dns/promises"; import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js"; import { type ResolvedBrowserConfig, resolveProfile } from "../../browser/config.js"; import { @@ -17,9 +18,38 @@ import { slugifySessionKey } from "./shared.js"; import { isToolAllowed } from "./tool-policy.js"; import type { SandboxBrowserContext, SandboxConfig } from "./types.js"; -async function waitForSandboxCdp(params: { cdpPort: number; timeoutMs: number }): Promise { +/** + * Resolve a hostname to an IPv4 address for CDP connections. + * Chrome's CDP HTTP endpoints reject non-IP Host headers, so we resolve + * hostnames like "host.docker.internal" to their IP addresses. + */ +async function resolveHostToIp(host: string): Promise { + // If already an IP address (v4 or v6), return as-is + if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(host) || host.includes(":")) { + return host; + } + // localhost is special-cased by Chrome + if (host === "localhost") { + return host; + } + try { + const result = await dns.lookup(host, { family: 4 }); + return result.address; + } catch { + // If DNS resolution fails, return original host and let caller handle the error + return host; + } +} + +async function waitForSandboxCdp(params: { + cdpHost: string; + cdpPort: number; + timeoutMs: number; +}): Promise { const deadline = Date.now() + Math.max(0, params.timeoutMs); - const url = `http://127.0.0.1:${params.cdpPort}/json/version`; + // Resolve hostname to IP for Chrome CDP compatibility + const resolvedHost = await resolveHostToIp(params.cdpHost); + const url = `http://${resolvedHost}:${params.cdpPort}/json/version`; while (Date.now() < deadline) { try { const ctrl = new AbortController(); @@ -40,18 +70,20 @@ async function waitForSandboxCdp(params: { cdpPort: number; timeoutMs: number }) function buildSandboxBrowserResolvedConfig(params: { controlPort: number; + cdpHost: string; cdpPort: number; headless: boolean; evaluateEnabled: boolean; }): ResolvedBrowserConfig { - const cdpHost = "127.0.0.1"; + const isLoopback = + params.cdpHost === "127.0.0.1" || params.cdpHost === "localhost" || params.cdpHost === "::1"; return { enabled: true, evaluateEnabled: params.evaluateEnabled, controlPort: params.controlPort, cdpProtocol: "http", - cdpHost, - cdpIsLoopback: true, + cdpHost: params.cdpHost, + cdpIsLoopback: isLoopback, remoteCdpTimeoutMs: 1500, remoteCdpHandshakeTimeoutMs: 3000, color: DEFAULT_CLAWD_BROWSER_COLOR, @@ -153,6 +185,9 @@ export async function ensureSandboxBrowser(params: { const ensureBridge = async () => { if (bridge) return bridge; + // Resolve hostname to IP for Chrome CDP compatibility + const resolvedCdpHost = await resolveHostToIp(params.cfg.browser.cdpHost); + const onEnsureAttachTarget = params.cfg.browser.autoStart ? async () => { const state = await dockerContainerState(containerName); @@ -160,12 +195,13 @@ export async function ensureSandboxBrowser(params: { await execDocker(["start", containerName]); } const ok = await waitForSandboxCdp({ + cdpHost: resolvedCdpHost, cdpPort: mappedCdp, timeoutMs: params.cfg.browser.autoStartTimeoutMs, }); if (!ok) { throw new Error( - `Sandbox browser CDP did not become reachable on 127.0.0.1:${mappedCdp} within ${params.cfg.browser.autoStartTimeoutMs}ms.`, + `Sandbox browser CDP did not become reachable on ${resolvedCdpHost}:${mappedCdp} within ${params.cfg.browser.autoStartTimeoutMs}ms.`, ); } } @@ -174,6 +210,7 @@ export async function ensureSandboxBrowser(params: { return await startBrowserBridgeServer({ resolved: buildSandboxBrowserResolvedConfig({ controlPort: 0, + cdpHost: resolvedCdpHost, cdpPort: mappedCdp, headless: params.cfg.browser.headless, evaluateEnabled: params.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED, @@ -203,7 +240,7 @@ export async function ensureSandboxBrowser(params: { const noVncUrl = mappedNoVnc && params.cfg.browser.enableNoVnc && !params.cfg.browser.headless - ? `http://127.0.0.1:${mappedNoVnc}/vnc.html?autoconnect=1&resize=remote` + ? `http://${params.cfg.browser.cdpHost}:${mappedNoVnc}/vnc.html?autoconnect=1&resize=remote` : undefined; return { diff --git a/src/agents/sandbox/config.ts b/src/agents/sandbox/config.ts index aa848c54b..5eb2976a1 100644 --- a/src/agents/sandbox/config.ts +++ b/src/agents/sandbox/config.ts @@ -2,6 +2,7 @@ import type { MoltbotConfig } from "../../config/config.js"; import { resolveAgentConfig } from "../agent-scope.js"; import { DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS, + DEFAULT_SANDBOX_BROWSER_CDP_HOST, DEFAULT_SANDBOX_BROWSER_CDP_PORT, DEFAULT_SANDBOX_BROWSER_IMAGE, DEFAULT_SANDBOX_BROWSER_NOVNC_PORT, @@ -93,6 +94,7 @@ export function resolveSandboxBrowserConfig(params: { agentBrowser?.containerPrefix ?? globalBrowser?.containerPrefix ?? DEFAULT_SANDBOX_BROWSER_PREFIX, + cdpHost: agentBrowser?.cdpHost ?? globalBrowser?.cdpHost ?? DEFAULT_SANDBOX_BROWSER_CDP_HOST, cdpPort: agentBrowser?.cdpPort ?? globalBrowser?.cdpPort ?? DEFAULT_SANDBOX_BROWSER_CDP_PORT, vncPort: agentBrowser?.vncPort ?? globalBrowser?.vncPort ?? DEFAULT_SANDBOX_BROWSER_VNC_PORT, noVncPort: diff --git a/src/agents/sandbox/constants.ts b/src/agents/sandbox/constants.ts index 6a844ecc7..c227ea65e 100644 --- a/src/agents/sandbox/constants.ts +++ b/src/agents/sandbox/constants.ts @@ -41,6 +41,7 @@ export const DEFAULT_SANDBOX_BROWSER_IMAGE = "moltbot-sandbox-browser:bookworm-s export const DEFAULT_SANDBOX_COMMON_IMAGE = "moltbot-sandbox-common:bookworm-slim"; export const DEFAULT_SANDBOX_BROWSER_PREFIX = "moltbot-sbx-browser-"; +export const DEFAULT_SANDBOX_BROWSER_CDP_HOST = "127.0.0.1"; export const DEFAULT_SANDBOX_BROWSER_CDP_PORT = 9222; export const DEFAULT_SANDBOX_BROWSER_VNC_PORT = 5900; export const DEFAULT_SANDBOX_BROWSER_NOVNC_PORT = 6080; diff --git a/src/agents/sandbox/types.ts b/src/agents/sandbox/types.ts index f27dfd715..f13922111 100644 --- a/src/agents/sandbox/types.ts +++ b/src/agents/sandbox/types.ts @@ -31,6 +31,7 @@ export type SandboxBrowserConfig = { enabled: boolean; image: string; containerPrefix: string; + cdpHost: string; cdpPort: number; vncPort: number; noVncPort: number; diff --git a/src/config/types.sandbox.ts b/src/config/types.sandbox.ts index 4f5f83810..a896c5171 100644 --- a/src/config/types.sandbox.ts +++ b/src/config/types.sandbox.ts @@ -48,6 +48,12 @@ export type SandboxBrowserSettings = { enabled?: boolean; image?: string; containerPrefix?: string; + /** + * Host to connect to for CDP (Chrome DevTools Protocol). + * Default: "127.0.0.1". + * Set to "host.docker.internal" when running gateway inside Docker. + */ + cdpHost?: string; cdpPort?: number; vncPort?: number; noVncPort?: number; diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 7e95c3538..18b8884e0 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -124,6 +124,7 @@ export const SandboxBrowserSchema = z enabled: z.boolean().optional(), image: z.string().optional(), containerPrefix: z.string().optional(), + cdpHost: z.string().optional(), cdpPort: z.number().int().positive().optional(), vncPort: z.number().int().positive().optional(), noVncPort: z.number().int().positive().optional(),