From 9f9e2b69a1d5578e672fc0c722a33d9ac27a3b3c Mon Sep 17 00:00:00 2001 From: leone Date: Thu, 6 Aug 2026 15:23:53 +0800 Subject: [PATCH] local-sandbox: allow reaching the exec daemon over a peer network The exec daemon URL is hard-coded to 127.0.0.1, which only works when core runs on the docker host itself. When core runs in a container (the shape deploy/core/Dockerfile produces), loopback is the core container, not the host publishing the sandbox port, so every exec, read, and write fails with fetch failed. With LOCAL_SANDBOX_PEER_NETWORK set, sandbox containers are connected to that docker network after run/start and core reaches the daemon by container name on AGENT_PORT. Unset keeps the loopback behavior used when core runs on the host. The daemon is unauthenticated, so this is only for a trusted internal network. --- src/config.ts | 2 ++ src/sandbox/local-sandbox.ts | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 16e85a7b..144db0ff 100644 --- a/src/config.ts +++ b/src/config.ts @@ -245,12 +245,14 @@ interface LocalSandboxEnv { cpus?: number; memoryMb?: number; defaultTimeoutSec?: number; + peerNetwork?: string; } function localSandboxEnv(env: NodeJS.ProcessEnv): LocalSandboxEnv { return { ...(env.LOCAL_SANDBOX_IMAGE ? { image: env.LOCAL_SANDBOX_IMAGE } : {}), ...(env.LOCAL_SANDBOX_DOCKER_BIN ? { dockerBin: env.LOCAL_SANDBOX_DOCKER_BIN } : {}), + ...(env.LOCAL_SANDBOX_PEER_NETWORK ? { peerNetwork: env.LOCAL_SANDBOX_PEER_NETWORK } : {}), ...(numEnvStrict("LOCAL_SANDBOX_CPUS", env.LOCAL_SANDBOX_CPUS) !== undefined ? { cpus: numEnvStrict("LOCAL_SANDBOX_CPUS", env.LOCAL_SANDBOX_CPUS) } : {}), diff --git a/src/sandbox/local-sandbox.ts b/src/sandbox/local-sandbox.ts index ce8c97b4..de15b234 100644 --- a/src/sandbox/local-sandbox.ts +++ b/src/sandbox/local-sandbox.ts @@ -48,6 +48,7 @@ export interface LocalSandboxOptions { repoRoot?: string; dockerExec?: DockerExec; fetchImpl?: typeof fetch; + peerNetwork?: string; onError?: (e: { category: string; code: string; message: string; scopeLabel?: string }) => void; } @@ -144,6 +145,7 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox } async function resolvePort(name: string): Promise { + if (opts.peerNetwork) return AGENT_PORT; const cached = portByName.get(name); if (cached) return cached; const r = await dexec(["port", name, `${AGENT_PORT}/tcp`]); @@ -167,7 +169,8 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox ): Promise<{ status: number; text: string }> { const port = await resolvePort(name); const signals = [AbortSignal.timeout(timeoutMs ?? 30_000), ...(signal ? [signal] : [])]; - const res = await fetchImpl(`http://127.0.0.1:${port}${path}`, { + const host = opts.peerNetwork ? name : "127.0.0.1"; + const res = await fetchImpl(`http://${host}:${port}${path}`, { method: body === undefined ? "GET" : "POST", ...(body === undefined ? {} : { body: JSON.stringify(body), headers: { "content-type": "application/json" } }), signal: AbortSignal.any(signals), @@ -195,6 +198,7 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox portByName.delete(name); const r = await dexec(["start", name]); if (r.code !== 0) throw new Error(`docker start ${name} failed: ${r.stderr.trim()}`); + await ensurePeerConnected(name); await waitDaemon(name); } @@ -236,6 +240,14 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox return net; } + async function ensurePeerConnected(name: string): Promise { + if (!opts.peerNetwork) return; + const r = await dexec(["network", "connect", opts.peerNetwork, name]); + if (r.code !== 0 && !/already exists/i.test(r.stderr)) { + throw new Error(`docker network connect ${opts.peerNetwork} ${name} failed: ${r.stderr.trim()}`); + } + } + async function runContainer(name: string, scope: string | undefined, withVolume: boolean): Promise { const net = await ensureNetwork(name); const args = [ @@ -262,6 +274,7 @@ export function createLocalSandbox(workspace: WorkspaceStore, opts: LocalSandbox ]; const r = await dexec(args, 120_000); if (r.code !== 0) throw new Error(`docker run ${name} failed: ${r.stderr.trim()}`); + await ensurePeerConnected(name); portByName.delete(name); await waitDaemon(name); }