From 05d7f39720f1d7e9b81b574927bb519805f76b32 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Fri, 15 May 2026 12:19:43 -0700 Subject: [PATCH 1/3] fix(onboard): preserve --add-host on GPU sandbox recreate (#3562) The Docker GPU patch (#3434) recreates the OpenShell-managed sandbox container with GPU flags after OpenShell creates it without --gpu. The recreation gated --add-host injection on networkMode !== host, but the patch's default network mode IS host, so the OpenShell-injected host.openshell.internal entry was silently dropped on every GPU sandbox. Result on DGX Spark and other GPU hosts: the recreated sandbox cannot resolve host.openshell.internal, so OpenClaw inference cannot reach the Ollama auth proxy and falls back to embedded which hits the same broken path. Surfaces as 'LLM request failed: network connection error' (#3562, #3568). --add-host writes to the container's /etc/hosts in the mount namespace, not the network stack, so it is safe to preserve even with --network=host. Also flips local.test.ts:618 to match the current validator output ('failed the local probe' instead of the legacy 'did not answer the local probe in time'). Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Charan Jagwani --- src/lib/inference/local.test.ts | 2 +- src/lib/onboard/docker-gpu-patch.test.ts | 5 ++++- src/lib/onboard/docker-gpu-patch.ts | 9 ++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/lib/inference/local.test.ts b/src/lib/inference/local.test.ts index 3dac656c7b7..2fe07c6f7c0 100644 --- a/src/lib/inference/local.test.ts +++ b/src/lib/inference/local.test.ts @@ -615,7 +615,7 @@ describe("local inference helpers", () => { it("fails ollama model validation when the probe times out or returns nothing", () => { const result = validateOllamaModel("nemotron-3-nano:30b", () => ""); expect(result.ok).toBe(false); - expect(result.message).toMatch(/did not answer the local probe in time/); + expect(result.message).toMatch(/failed the local probe/); }); it("fails ollama model validation when Ollama returns an error payload", () => { diff --git a/src/lib/onboard/docker-gpu-patch.test.ts b/src/lib/onboard/docker-gpu-patch.test.ts index ea2ce9c27aa..2e637840704 100644 --- a/src/lib/onboard/docker-gpu-patch.test.ts +++ b/src/lib/onboard/docker-gpu-patch.test.ts @@ -214,7 +214,10 @@ describe("docker-gpu-patch", () => { expect(args).toEqual( expect.arrayContaining(["--env", "OPENSHELL_ENDPOINT=http://127.0.0.1:8080/"]), ); - expect(args).not.toEqual( + // --add-host writes to the container's /etc/hosts (mount namespace) not + // the network stack, so OpenShell's host.openshell.internal mapping must + // survive the GPU recreate even with --network=host (#3562, #3568). + expect(args).toEqual( expect.arrayContaining(["--add-host", "host.openshell.internal:172.17.0.1"]), ); expect(args).not.toEqual(expect.arrayContaining(["--network-alias", "openshell-alpha"])); diff --git a/src/lib/onboard/docker-gpu-patch.ts b/src/lib/onboard/docker-gpu-patch.ts index efc94d9c938..743f8e439b8 100644 --- a/src/lib/onboard/docker-gpu-patch.ts +++ b/src/lib/onboard/docker-gpu-patch.ts @@ -449,9 +449,12 @@ export function buildDockerGpuCloneRunArgs( securityOpt.add("apparmor=unconfined"); } for (const opt of securityOpt) args.push("--security-opt", opt); - if (networkMode !== "host") { - for (const hostEntry of stringArray(host.ExtraHosts)) args.push("--add-host", hostEntry); - } + // --add-host writes to the container's /etc/hosts (mount namespace), not + // the network stack, so it is safe to preserve even when networkMode is + // "host". Dropping it here breaks `host.openshell.internal` resolution on + // GPU sandbox recreates and the sandbox cannot reach the host Ollama auth + // proxy (#3562, #3568). + for (const hostEntry of stringArray(host.ExtraHosts)) args.push("--add-host", hostEntry); for (const group of stringArray(host.GroupAdd)) args.push("--group-add", group); if (networkMode !== "host") { for (const dns of stringArray(host.Dns)) args.push("--dns", dns); From 2067acdabd8d598a9b2047f9b3bf22b3493ff96c Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Fri, 15 May 2026 12:32:39 -0700 Subject: [PATCH 2/3] fix(onboard): switch to Zifu's verified preserve-network fix Replace the targeted ExtraHosts unconditional with the upstream root-cause fix verified on Spark by @Zifu Yang: flip the default of getDockerGpuPatchNetworkMode from 'host' to 'preserve' so the GPU recreate keeps the original openshell-docker bridge network (and its --add-host injection) instead of switching to host networking. NEMOCLAW_DOCKER_GPU_PATCH_NETWORK=host still opts into host networking for callers that explicitly want it. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Charan Jagwani --- src/lib/inference/local.test.ts | 2 +- src/lib/onboard/docker-gpu-patch.test.ts | 13 ++++++------- src/lib/onboard/docker-gpu-patch.ts | 13 +++++-------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/lib/inference/local.test.ts b/src/lib/inference/local.test.ts index 2fe07c6f7c0..3dac656c7b7 100644 --- a/src/lib/inference/local.test.ts +++ b/src/lib/inference/local.test.ts @@ -615,7 +615,7 @@ describe("local inference helpers", () => { it("fails ollama model validation when the probe times out or returns nothing", () => { const result = validateOllamaModel("nemotron-3-nano:30b", () => ""); expect(result.ok).toBe(false); - expect(result.message).toMatch(/failed the local probe/); + expect(result.message).toMatch(/did not answer the local probe in time/); }); it("fails ollama model validation when Ollama returns an error payload", () => { diff --git a/src/lib/onboard/docker-gpu-patch.test.ts b/src/lib/onboard/docker-gpu-patch.test.ts index 2e637840704..9e30050a0db 100644 --- a/src/lib/onboard/docker-gpu-patch.test.ts +++ b/src/lib/onboard/docker-gpu-patch.test.ts @@ -214,10 +214,7 @@ describe("docker-gpu-patch", () => { expect(args).toEqual( expect.arrayContaining(["--env", "OPENSHELL_ENDPOINT=http://127.0.0.1:8080/"]), ); - // --add-host writes to the container's /etc/hosts (mount namespace) not - // the network stack, so OpenShell's host.openshell.internal mapping must - // survive the GPU recreate even with --network=host (#3562, #3568). - expect(args).toEqual( + expect(args).not.toEqual( expect.arrayContaining(["--add-host", "host.openshell.internal:172.17.0.1"]), ); expect(args).not.toEqual(expect.arrayContaining(["--network-alias", "openshell-alpha"])); @@ -229,7 +226,7 @@ describe("docker-gpu-patch", () => { }); it("reports the Docker GPU patch network mode", () => { - expect(getDockerGpuPatchNetworkMode({})).toBe("host"); + expect(getDockerGpuPatchNetworkMode({})).toBe("preserve"); expect(getDockerGpuPatchNetworkMode({ NEMOCLAW_DOCKER_GPU_PATCH_NETWORK: "host" })).toBe( "host", ); @@ -397,9 +394,11 @@ describe("docker-gpu-patch", () => { "--security-opt", "apparmor=unconfined", "--network", - "host", + "openshell-docker", + "--add-host", + "host.openshell.internal:172.17.0.1", "--env", - "OPENSHELL_ENDPOINT=http://127.0.0.1:8080/", + "OPENSHELL_ENDPOINT=http://host.openshell.internal:8080/", ]), expect.objectContaining({ ignoreError: true }), ); diff --git a/src/lib/onboard/docker-gpu-patch.ts b/src/lib/onboard/docker-gpu-patch.ts index 743f8e439b8..e23cb581412 100644 --- a/src/lib/onboard/docker-gpu-patch.ts +++ b/src/lib/onboard/docker-gpu-patch.ts @@ -358,9 +358,9 @@ export function getDockerGpuPatchNetworkMode( env: Record = process.env, ): "host" | "preserve" { const networkOverride = String(env[DOCKER_GPU_PATCH_NETWORK_ENV] || "").trim().toLowerCase(); + if (networkOverride === "host") return "host"; if (networkOverride === "preserve" || networkOverride === "bridge") return "preserve"; - if (networkOverride && networkOverride !== "host") return "preserve"; - return "host"; + return "preserve"; } function dockerNetworkAliases( @@ -449,12 +449,9 @@ export function buildDockerGpuCloneRunArgs( securityOpt.add("apparmor=unconfined"); } for (const opt of securityOpt) args.push("--security-opt", opt); - // --add-host writes to the container's /etc/hosts (mount namespace), not - // the network stack, so it is safe to preserve even when networkMode is - // "host". Dropping it here breaks `host.openshell.internal` resolution on - // GPU sandbox recreates and the sandbox cannot reach the host Ollama auth - // proxy (#3562, #3568). - for (const hostEntry of stringArray(host.ExtraHosts)) args.push("--add-host", hostEntry); + if (networkMode !== "host") { + for (const hostEntry of stringArray(host.ExtraHosts)) args.push("--add-host", hostEntry); + } for (const group of stringArray(host.GroupAdd)) args.push("--group-add", group); if (networkMode !== "host") { for (const dns of stringArray(host.Dns)) args.push("--dns", dns); From 505d6c42f33c1ab69696d6c10a26875472b67726 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Fri, 15 May 2026 12:40:44 -0700 Subject: [PATCH 3/3] fix(onboard): also preserve --add-host on explicit --network=host opt-in CodeRabbit follow-up on #3623: when a caller explicitly sets NEMOCLAW_DOCKER_GPU_PATCH_NETWORK=host, the recreation still drops --add-host because the buildDockerGpuCloneRunArgs guard was gated on networkMode !== host. --add-host modifies /etc/hosts in the mount namespace, not the network stack, so it is safe to preserve even with --network=host. Drop the guard; keep the --dns guard since --dns genuinely conflicts with --network=host. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Charan Jagwani --- src/lib/onboard/docker-gpu-patch.test.ts | 5 ++++- src/lib/onboard/docker-gpu-patch.ts | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lib/onboard/docker-gpu-patch.test.ts b/src/lib/onboard/docker-gpu-patch.test.ts index 9e30050a0db..1807488fb53 100644 --- a/src/lib/onboard/docker-gpu-patch.test.ts +++ b/src/lib/onboard/docker-gpu-patch.test.ts @@ -214,7 +214,10 @@ describe("docker-gpu-patch", () => { expect(args).toEqual( expect.arrayContaining(["--env", "OPENSHELL_ENDPOINT=http://127.0.0.1:8080/"]), ); - expect(args).not.toEqual( + // --add-host writes to /etc/hosts (mount namespace), not the network + // stack, so it must survive even when --network=host is explicitly + // requested (#3562, #3568). + expect(args).toEqual( expect.arrayContaining(["--add-host", "host.openshell.internal:172.17.0.1"]), ); expect(args).not.toEqual(expect.arrayContaining(["--network-alias", "openshell-alpha"])); diff --git a/src/lib/onboard/docker-gpu-patch.ts b/src/lib/onboard/docker-gpu-patch.ts index e23cb581412..a04700143df 100644 --- a/src/lib/onboard/docker-gpu-patch.ts +++ b/src/lib/onboard/docker-gpu-patch.ts @@ -449,9 +449,11 @@ export function buildDockerGpuCloneRunArgs( securityOpt.add("apparmor=unconfined"); } for (const opt of securityOpt) args.push("--security-opt", opt); - if (networkMode !== "host") { - for (const hostEntry of stringArray(host.ExtraHosts)) args.push("--add-host", hostEntry); - } + // --add-host writes to the container's /etc/hosts (mount namespace), not + // the network stack, so OpenShell's host.openshell.internal mapping must + // survive even when the caller explicitly opts into --network=host via + // NEMOCLAW_DOCKER_GPU_PATCH_NETWORK=host (#3562, #3568). + for (const hostEntry of stringArray(host.ExtraHosts)) args.push("--add-host", hostEntry); for (const group of stringArray(host.GroupAdd)) args.push("--group-add", group); if (networkMode !== "host") { for (const dns of stringArray(host.Dns)) args.push("--dns", dns);