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 <noreply@anthropic.com>
This commit is contained in:
parent
4583f88626
commit
174bac87cb
@ -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"
|
||||
|
||||
@ -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<boolean> {
|
||||
/**
|
||||
* 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<string> {
|
||||
// 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<boolean> {
|
||||
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 {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -31,6 +31,7 @@ export type SandboxBrowserConfig = {
|
||||
enabled: boolean;
|
||||
image: string;
|
||||
containerPrefix: string;
|
||||
cdpHost: string;
|
||||
cdpPort: number;
|
||||
vncPort: number;
|
||||
noVncPort: number;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user