From 3fff9129d410b170355749c782a3a8639055fe50 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 14:10:23 -0700 Subject: [PATCH 1/4] fix(connect): isolate route probe output (cherry picked from commit 1504f4c0dce6e69f38c15322ff5745d285835c08) Signed-off-by: Apurv Kumaria --- src/lib/actions/sandbox/connect-flow.test.ts | 32 +++++++++++++ .../connect-inference-route-probe.test.ts | 46 +++++++++++++++++++ .../sandbox/connect-inference-route-probe.ts | 14 ++++-- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/lib/actions/sandbox/connect-flow.test.ts b/src/lib/actions/sandbox/connect-flow.test.ts index 63613749200..b0e1405564b 100644 --- a/src/lib/actions/sandbox/connect-flow.test.ts +++ b/src/lib/actions/sandbox/connect-flow.test.ts @@ -432,6 +432,38 @@ describe("connectSandbox flow", () => { expect(exitSpy).toHaveBeenCalledWith(1); }); + it.each([ + "OK 200\nBROKEN 000", + "BROKEN 503\nOK 200", + ])("rejects DCode login-shell preamble evidence without repair or SSH (%s) (#6192)", async (output) => { + const harness = createConnectHarness({ + agentName: "langchain-deepagents-code", + registryEntry: { + provider: "nvidia-prod", + model: "nvidia/nemotron-3-super-120b-a12b", + }, + inferenceGetOutput: + "Gateway inference:\n Provider: nvidia-prod\n Model: nvidia/nemotron-3-super-120b-a12b\n", + inferenceProbeResponses: [output], + sessionAgent: { name: "langchain-deepagents-code" }, + }); + + await expect(harness.connectSandbox("alpha")).rejects.toThrow("process.exit(1)"); + + expect(harness.applyVmDnsMonkeypatchSpy).not.toHaveBeenCalled(); + expect(harness.runSetupDnsProxySpy).not.toHaveBeenCalled(); + expect(harness.runOpenshellSpy).not.toHaveBeenCalled(); + expect(harness.spawnSyncSpy).not.toHaveBeenCalledWith( + "openshell", + ["sandbox", "connect", "alpha"], + expect.any(Object), + ); + expect(harness.errorSpy.mock.calls.flat().join("\n")).toContain( + "did not return a trusted result", + ); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + it("stops before opening SSH when the sandbox list reports a terminal failure phase", async () => { const harness = createConnectHarness({ listOutput: "alpha Error" }); diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts index 0db847992f1..d97e280d20a 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { @@ -20,7 +23,9 @@ describe("sandbox connect inference route probe argv", () => { expect(args.slice(0, 7)).toEqual(["sandbox", "exec", "--name", "deep-code", "--", "sh", "-c"]); expect(args.at(-3)).toContain('bash -lc "$1" "$CA_BUNDLE"'); expect(args.at(-3)).toContain("-u HTTPS_PROXY"); + expect(args.at(-3)).toContain("3>&1 1>/dev/null"); expect(args.at(-2)).toBe("nemoclaw-ca-capture"); + expect(args.at(-1)).toContain("exec 1>&3 3>&-"); expect(args.at(-1)).toContain('CA_BUNDLE="$0"'); expect(args.at(-1)).toContain("https://inference.local/v1/models"); expect(args.every((arg) => !/[\r\n]/.test(arg))).toBe(true); @@ -74,6 +79,36 @@ describe("sandbox connect inference route probe argv", () => { parseSandboxInferenceRouteProbeResult({ status: result.status, output: result.stdout }), ).toMatchObject({ healthy: false, broken: false, httpStatus: 0 }); }); + + it.each([ + "OK 200", + "BROKEN 503", + ])("isolates DCode login-shell startup output from a %s spoof (#6192)", (spoof) => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-probe-")); + const caBundle = path.join(home, "openshell-ca.pem"); + const profileMarker = path.join(home, "profile-ran"); + fs.writeFileSync(caBundle, "test CA boundary", "utf8"); + fs.writeFileSync( + path.join(home, ".bash_profile"), + `printf '%s\\n' ${JSON.stringify(spoof)}; printf ran > ${JSON.stringify(profileMarker)}`, + ); + const args = buildSandboxInferenceRouteProbeArgs("deep-code", { + name: "langchain-deepagents-code", + }); + const wrapper = String(args.at(-3)).replace("HOME=/sandbox", `HOME=${JSON.stringify(home)}`); + const trustedProbe = "exec 1>&3 3>&-; printf 'BROKEN 000'"; + + const result = spawnSync("sh", ["-c", wrapper, String(args.at(-2)), trustedProbe], { + encoding: "utf8", + env: { ...process.env, CURL_CA_BUNDLE: caBundle, SSL_CERT_FILE: "" }, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("BROKEN 000"); + expect(result.stdout).not.toContain(spoof); + expect(fs.readFileSync(profileMarker, "utf8")).toBe("ran"); + fs.rmSync(home, { force: true, recursive: true }); + }); }); describe("sandbox inference route probe result", () => { @@ -125,6 +160,17 @@ describe("sandbox inference route probe result", () => { ).toMatchObject({ healthy: false, broken: false, httpStatus: 0 }); }); + it.each([ + "OK 200\nBROKEN 000", + "BROKEN 503\nOK 200", + ])("does not trust login-shell preamble output (%s) (#6192)", (output) => { + expect(parseSandboxInferenceRouteProbeResult({ status: 0, output })).toMatchObject({ + healthy: false, + broken: false, + httpStatus: 0, + }); + }); + it("fails closed when malformed output claims an unhealthy status is OK (#6192)", () => { expect(parseSandboxInferenceRouteProbeResult({ status: 0, output: "OK 503" })).toMatchObject({ healthy: false, diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.ts b/src/lib/actions/sandbox/connect-inference-route-probe.ts index 6ac5d477225..ebc64493435 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.ts @@ -36,6 +36,10 @@ export const INFERENCE_ROUTE_PROBE_SCRIPT = [ INFERENCE_ROUTE_PROBE_CORE_SCRIPT, ].join("; "); const INFERENCE_ROUTE_PROBE_FROM_ARG0_SCRIPT = [ + // The outer DCode wrapper suppresses login-shell startup stdout. Restore the + // capture descriptor only after profile loading completes so profile output + // cannot impersonate trusted route evidence. + "exec 1>&3 3>&-", 'CA_BUNDLE="$0"', INFERENCE_ROUTE_PROBE_CORE_SCRIPT, ].join("; "); @@ -55,8 +59,10 @@ const DCODE_INFERENCE_ROUTE_PROBE_WRAPPER = [ INFERENCE_ROUTE_CA_FROM_ENV, INFERENCE_ROUTE_CA_VALIDATION, // bash -lc receives CA_BUNDLE as argv[0], so the inner script reads the - // exact OpenShell-injected CA path from $0 after the login shell loads. - `exec env ${PROXY_ENV_KEYS.map((key) => `-u ${key}`).join(" ")} HOME=/sandbox bash -lc "$1" "$CA_BUNDLE"`, + // exact OpenShell-injected CA path from $0 after the login shell loads. FD 3 + // preserves the capture stream while startup stdout is discarded; the inner + // probe restores it before emitting its result. + `exec env ${PROXY_ENV_KEYS.map((key) => `-u ${key}`).join(" ")} HOME=/sandbox bash -lc "$1" "$CA_BUNDLE" 3>&1 1>/dev/null`, ].join("; "); /** @@ -98,7 +104,9 @@ export function parseSandboxInferenceRouteProbeResult( // Some OpenShell releases frame child stdout for humans. Normalize only the // two known frame prefixes at the beginning of the captured output. const detail = rawDetail.replace(/^(?:\[stdout\]|stdout:)\s*/i, ""); - const match = /^(OK|BROKEN)\s+([0-9]{3})\b/.exec(detail); + // A trusted probe emits one result line. Reject preambles or extra lines so + // shell startup output can never be mistaken for the authoritative result. + const match = /^(OK|BROKEN)\s+([0-9]{3})\b[^\r\n]*$/.exec(detail); const httpStatus = match ? Number.parseInt(match[2], 10) : 0; const isReachableHttpStatus = httpStatus >= 200 && httpStatus < 500; const commandSucceeded = result.status === 0; From 9b996d958a4a8170b030d38698dde4ee29783493 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 14:39:12 -0700 Subject: [PATCH 2/4] test(connect): isolate DCode probe boundary coverage Signed-off-by: Apurv Kumaria --- .../connect-flow-dcode-probe-preamble.test.ts | 65 +++++++++++++++++++ src/lib/actions/sandbox/connect-flow.test.ts | 32 --------- .../connect-inference-route-probe.test.ts | 2 + 3 files changed, 67 insertions(+), 32 deletions(-) create mode 100644 src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts diff --git a/src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts b/src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts new file mode 100644 index 00000000000..e869bf57d6e --- /dev/null +++ b/src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest"; + +import { + connectModulePath, + createConnectHarness, + requireDist, +} from "../../../../test/support/connect-flow-test-harness"; + +describe("connectSandbox DCode probe preamble boundary", () => { + let exitSpy: MockInstance; + const originalStdoutIsTty = process.stdout.isTTY; + + beforeEach(() => { + process.env.NEMOCLAW_TEST_NO_SLEEP = "1"; + Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true }); + exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number | string | null) => { + throw new Error(`process.exit(${code ?? 0})`); + }) as never); + }); + + afterEach(() => { + vi.restoreAllMocks(); + Object.defineProperty(process.stdout, "isTTY", { + configurable: true, + value: originalStdoutIsTty, + }); + delete process.env.NEMOCLAW_TEST_NO_SLEEP; + delete require.cache[requireDist.resolve(connectModulePath)]; + }); + + it.each([ + "OK 200\nBROKEN 000", + "BROKEN 503\nOK 200", + ])("rejects login-shell preamble evidence without repair or SSH (%s) (#6192)", async (output) => { + const harness = createConnectHarness({ + agentName: "langchain-deepagents-code", + registryEntry: { + provider: "nvidia-prod", + model: "nvidia/nemotron-3-super-120b-a12b", + }, + inferenceGetOutput: + "Gateway inference:\n Provider: nvidia-prod\n Model: nvidia/nemotron-3-super-120b-a12b\n", + inferenceProbeResponses: [output], + sessionAgent: { name: "langchain-deepagents-code" }, + }); + + await expect(harness.connectSandbox("alpha")).rejects.toThrow("process.exit(1)"); + + expect(harness.applyVmDnsMonkeypatchSpy).not.toHaveBeenCalled(); + expect(harness.runSetupDnsProxySpy).not.toHaveBeenCalled(); + expect(harness.runOpenshellSpy).not.toHaveBeenCalled(); + expect(harness.spawnSyncSpy).not.toHaveBeenCalledWith( + "openshell", + ["sandbox", "connect", "alpha"], + expect.any(Object), + ); + expect(harness.errorSpy.mock.calls.flat().join("\n")).toContain( + "did not return a trusted result", + ); + expect(exitSpy).toHaveBeenCalledWith(1); + }); +}); diff --git a/src/lib/actions/sandbox/connect-flow.test.ts b/src/lib/actions/sandbox/connect-flow.test.ts index b0e1405564b..63613749200 100644 --- a/src/lib/actions/sandbox/connect-flow.test.ts +++ b/src/lib/actions/sandbox/connect-flow.test.ts @@ -432,38 +432,6 @@ describe("connectSandbox flow", () => { expect(exitSpy).toHaveBeenCalledWith(1); }); - it.each([ - "OK 200\nBROKEN 000", - "BROKEN 503\nOK 200", - ])("rejects DCode login-shell preamble evidence without repair or SSH (%s) (#6192)", async (output) => { - const harness = createConnectHarness({ - agentName: "langchain-deepagents-code", - registryEntry: { - provider: "nvidia-prod", - model: "nvidia/nemotron-3-super-120b-a12b", - }, - inferenceGetOutput: - "Gateway inference:\n Provider: nvidia-prod\n Model: nvidia/nemotron-3-super-120b-a12b\n", - inferenceProbeResponses: [output], - sessionAgent: { name: "langchain-deepagents-code" }, - }); - - await expect(harness.connectSandbox("alpha")).rejects.toThrow("process.exit(1)"); - - expect(harness.applyVmDnsMonkeypatchSpy).not.toHaveBeenCalled(); - expect(harness.runSetupDnsProxySpy).not.toHaveBeenCalled(); - expect(harness.runOpenshellSpy).not.toHaveBeenCalled(); - expect(harness.spawnSyncSpy).not.toHaveBeenCalledWith( - "openshell", - ["sandbox", "connect", "alpha"], - expect.any(Object), - ); - expect(harness.errorSpy.mock.calls.flat().join("\n")).toContain( - "did not return a trusted result", - ); - expect(exitSpy).toHaveBeenCalledWith(1); - }); - it("stops before opening SSH when the sandbox list reports a terminal failure phase", async () => { const harness = createConnectHarness({ listOutput: "alpha Error" }); diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts index d97e280d20a..49280cb7f42 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts @@ -163,6 +163,8 @@ describe("sandbox inference route probe result", () => { it.each([ "OK 200\nBROKEN 000", "BROKEN 503\nOK 200", + "[stdout] OK 200\nBROKEN 000", + "[stdout] BROKEN 503\nOK 200", ])("does not trust login-shell preamble output (%s) (#6192)", (output) => { expect(parseSandboxInferenceRouteProbeResult({ status: 0, output })).toMatchObject({ healthy: false, From 71dc79ea2635edf16a61175cd2411010808938c6 Mon Sep 17 00:00:00 2001 From: cjagwani Date: Wed, 8 Jul 2026 21:47:59 +0000 Subject: [PATCH 3/4] fix(connect): remove DCode probe trust channels Signed-off-by: cjagwani --- src/lib/actions/sandbox/connect-flow.test.ts | 6 +- .../connect-inference-route-probe.test.ts | 83 ++++++++++++------- .../sandbox/connect-inference-route-probe.ts | 57 +++++-------- 3 files changed, 76 insertions(+), 70 deletions(-) diff --git a/src/lib/actions/sandbox/connect-flow.test.ts b/src/lib/actions/sandbox/connect-flow.test.ts index 63613749200..7fb0925646f 100644 --- a/src/lib/actions/sandbox/connect-flow.test.ts +++ b/src/lib/actions/sandbox/connect-flow.test.ts @@ -237,7 +237,7 @@ describe("connectSandbox flow", () => { expect(exitSpy).toHaveBeenCalledWith(0); }); - it("runs the dcode inference route probe through its login-shell proxy contract (#6191)", async () => { + it("runs the dcode inference route probe through its immutable proxy contract (#6191)", async () => { const harness = createConnectHarness({ agentName: "langchain-deepagents-code", sessionAgent: { @@ -282,9 +282,7 @@ describe("connectSandbox flow", () => { "--", "sh", "-c", - expect.stringContaining('bash -lc "$1" "$CA_BUNDLE"'), - "nemoclaw-ca-capture", - expect.stringContaining("/usr/bin/curl"), + expect.stringContaining("/usr/local/share/nemoclaw/dcode-proxy-host"), ], expect.objectContaining({ ignoreError: true }), ); diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts index 49280cb7f42..893c58d8dbb 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts @@ -15,19 +15,21 @@ import { } from "./connect-inference-route-probe"; describe("sandbox connect inference route probe argv", () => { - it("uses the dcode login-shell proxy contract without inherited proxy variables (#6191)", () => { + it("uses root-owned DCode proxy files without login-shell startup code (#6191)", () => { const args = buildSandboxInferenceRouteProbeArgs("deep-code", { name: "langchain-deepagents-code", }); expect(args.slice(0, 7)).toEqual(["sandbox", "exec", "--name", "deep-code", "--", "sh", "-c"]); - expect(args.at(-3)).toContain('bash -lc "$1" "$CA_BUNDLE"'); - expect(args.at(-3)).toContain("-u HTTPS_PROXY"); - expect(args.at(-3)).toContain("3>&1 1>/dev/null"); - expect(args.at(-2)).toBe("nemoclaw-ca-capture"); - expect(args.at(-1)).toContain("exec 1>&3 3>&-"); - expect(args.at(-1)).toContain('CA_BUNDLE="$0"'); + expect(args).toHaveLength(8); + expect(args.at(-1)).toContain("/usr/local/share/nemoclaw/dcode-proxy-host"); + expect(args.at(-1)).toContain("/usr/local/share/nemoclaw/dcode-proxy-port"); + expect(args.at(-1)).toContain("0:444"); + expect(args.at(-1)).toContain('HTTPS_PROXY="$PROXY_URL"'); expect(args.at(-1)).toContain("https://inference.local/v1/models"); + expect(args.at(-1)).not.toContain("bash -lc"); + expect(args.at(-1)).not.toContain("3>&1"); + expect(args.at(-1)).not.toContain("/tmp/nemoclaw-proxy-env.sh"); expect(args.every((arg) => !/[\r\n]/.test(arg))).toBe(true); }); @@ -52,7 +54,7 @@ describe("sandbox connect inference route probe argv", () => { const args = buildSandboxInferenceRouteProbeArgs("alpha", { name: "openclaw" }); const script = args.at(-1) ?? ""; - expect(script).toContain("/usr/bin/curl -s -o /dev/null"); + expect(script).toContain("/usr/bin/curl -q -s -o /dev/null"); expect(script).toContain('CA_BUNDLE="${CURL_CA_BUNDLE:-${SSL_CERT_FILE:-}}"'); expect(script).toContain('--cacert "$CA_BUNDLE"'); expect(script).toContain("printf 'UNAVAILABLE OpenShell CA bundle missing or unreadable'"); @@ -83,31 +85,48 @@ describe("sandbox connect inference route probe argv", () => { it.each([ "OK 200", "BROKEN 503", - ])("isolates DCode login-shell startup output from a %s spoof (#6192)", (spoof) => { + ])("does not execute DCode login-shell startup code containing a %s spoof (#6192)", (spoof) => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-probe-")); - const caBundle = path.join(home, "openshell-ca.pem"); - const profileMarker = path.join(home, "profile-ran"); - fs.writeFileSync(caBundle, "test CA boundary", "utf8"); - fs.writeFileSync( - path.join(home, ".bash_profile"), - `printf '%s\\n' ${JSON.stringify(spoof)}; printf ran > ${JSON.stringify(profileMarker)}`, - ); - const args = buildSandboxInferenceRouteProbeArgs("deep-code", { - name: "langchain-deepagents-code", - }); - const wrapper = String(args.at(-3)).replace("HOME=/sandbox", `HOME=${JSON.stringify(home)}`); - const trustedProbe = "exec 1>&3 3>&-; printf 'BROKEN 000'"; - - const result = spawnSync("sh", ["-c", wrapper, String(args.at(-2)), trustedProbe], { - encoding: "utf8", - env: { ...process.env, CURL_CA_BUNDLE: caBundle, SSL_CERT_FILE: "" }, - }); - - expect(result.status).toBe(0); - expect(result.stdout).toBe("BROKEN 000"); - expect(result.stdout).not.toContain(spoof); - expect(fs.readFileSync(profileMarker, "utf8")).toBe("ran"); - fs.rmSync(home, { force: true, recursive: true }); + try { + const caBundle = path.join(home, "openshell-ca.pem"); + const hostFile = path.join(home, "dcode-proxy-host"); + const portFile = path.join(home, "dcode-proxy-port"); + const profileMarker = path.join(home, "profile-ran"); + const curlConfigMarker = path.join(home, "curl-config-ran"); + fs.writeFileSync(caBundle, "test CA boundary", "utf8"); + fs.writeFileSync(hostFile, "127.0.0.1\n", { mode: 0o444 }); + fs.writeFileSync(portFile, "9\n", { mode: 0o444 }); + fs.chmodSync(hostFile, 0o444); + fs.chmodSync(portFile, 0o444); + fs.writeFileSync( + path.join(home, ".bash_profile"), + `printf '%s\\n' ${JSON.stringify(spoof)} >&3; printf ran > ${JSON.stringify(profileMarker)}`, + ); + fs.writeFileSync( + path.join(home, ".curlrc"), + `trace-ascii = ${JSON.stringify(curlConfigMarker)}\n`, + ); + const args = buildSandboxInferenceRouteProbeArgs("deep-code", { + name: "langchain-deepagents-code", + }); + const script = String(args.at(-1)) + .replace("/usr/local/share/nemoclaw/dcode-proxy-host", hostFile) + .replace("/usr/local/share/nemoclaw/dcode-proxy-port", portFile) + .replaceAll("0:444", `${process.getuid?.() ?? 0}:444`); + + const result = spawnSync("sh", ["-c", script], { + encoding: "utf8", + env: { ...process.env, HOME: home, CURL_CA_BUNDLE: caBundle, SSL_CERT_FILE: "" }, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("BROKEN 000"); + expect(result.stdout).not.toContain(spoof); + expect(fs.existsSync(profileMarker)).toBe(false); + expect(fs.existsSync(curlConfigMarker)).toBe(false); + } finally { + fs.rmSync(home, { force: true, recursive: true }); + } }); }); diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.ts b/src/lib/actions/sandbox/connect-inference-route-probe.ts index ebc64493435..71ac7ff1399 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.ts @@ -27,7 +27,7 @@ const INFERENCE_ROUTE_CA_FROM_ENV = 'CA_BUNDLE="${CURL_CA_BUNDLE:-${SSL_CERT_FIL const INFERENCE_ROUTE_CA_VALIDATION = '[ -n "$CA_BUNDLE" ] && [ -f "$CA_BUNDLE" ] && [ -r "$CA_BUNDLE" ] || { printf \'UNAVAILABLE OpenShell CA bundle missing or unreadable\'; exit 1; }'; const INFERENCE_ROUTE_PROBE_CORE_SCRIPT = [ - "HTTP_CODE=$(/usr/bin/curl -s -o /dev/null -w '%{http_code}' --cacert \"$CA_BUNDLE\" --connect-timeout 3 --max-time 8 https://inference.local/v1/models 2>/dev/null) || HTTP_CODE=000", + "HTTP_CODE=$(/usr/bin/curl -q -s -o /dev/null -w '%{http_code}' --cacert \"$CA_BUNDLE\" --connect-timeout 3 --max-time 8 https://inference.local/v1/models 2>/dev/null) || HTTP_CODE=000", 'case "$HTTP_CODE" in [2-4][0-9][0-9]) printf \'OK %s\' "$HTTP_CODE" ;; *) printf \'BROKEN %s\' "$HTTP_CODE" ;; esac', ].join("; "); export const INFERENCE_ROUTE_PROBE_SCRIPT = [ @@ -35,34 +35,26 @@ export const INFERENCE_ROUTE_PROBE_SCRIPT = [ INFERENCE_ROUTE_CA_VALIDATION, INFERENCE_ROUTE_PROBE_CORE_SCRIPT, ].join("; "); -const INFERENCE_ROUTE_PROBE_FROM_ARG0_SCRIPT = [ - // The outer DCode wrapper suppresses login-shell startup stdout. Restore the - // capture descriptor only after profile loading completes so profile output - // cannot impersonate trusted route evidence. - "exec 1>&3 3>&-", - 'CA_BUNDLE="$0"', - INFERENCE_ROUTE_PROBE_CORE_SCRIPT, -].join("; "); - -const PROXY_ENV_KEYS = [ - "HTTP_PROXY", - "HTTPS_PROXY", - "http_proxy", - "https_proxy", - "NO_PROXY", - "no_proxy", - "ALL_PROXY", - "all_proxy", -] as const; - -const DCODE_INFERENCE_ROUTE_PROBE_WRAPPER = [ +const DCODE_PROXY_HOST_FILE = "/usr/local/share/nemoclaw/dcode-proxy-host"; +const DCODE_PROXY_PORT_FILE = "/usr/local/share/nemoclaw/dcode-proxy-port"; +const DCODE_PROXY_UNAVAILABLE = + "UNAVAILABLE managed DCode proxy files are missing, unsafe, or invalid"; +const DCODE_INFERENCE_ROUTE_PROBE_SCRIPT = [ INFERENCE_ROUTE_CA_FROM_ENV, INFERENCE_ROUTE_CA_VALIDATION, - // bash -lc receives CA_BUNDLE as argv[0], so the inner script reads the - // exact OpenShell-injected CA path from $0 after the login shell loads. FD 3 - // preserves the capture stream while startup stdout is discarded; the inner - // probe restores it before emitting its result. - `exec env ${PROXY_ENV_KEYS.map((key) => `-u ${key}`).join(" ")} HOME=/sandbox bash -lc "$1" "$CA_BUNDLE" 3>&1 1>/dev/null`, + `PROXY_HOST_FILE="${DCODE_PROXY_HOST_FILE}"`, + `PROXY_PORT_FILE="${DCODE_PROXY_PORT_FILE}"`, + `[ -f "$PROXY_HOST_FILE" ] && [ ! -L "$PROXY_HOST_FILE" ] && [ "$(/usr/bin/stat -c '%u:%a' "$PROXY_HOST_FILE" 2>/dev/null)" = "0:444" ] && [ -f "$PROXY_PORT_FILE" ] && [ ! -L "$PROXY_PORT_FILE" ] && [ "$(/usr/bin/stat -c '%u:%a' "$PROXY_PORT_FILE" 2>/dev/null)" = "0:444" ] || { printf '${DCODE_PROXY_UNAVAILABLE}'; exit 1; }`, + `PROXY_HOST=$(/usr/bin/cat "$PROXY_HOST_FILE") || { printf '${DCODE_PROXY_UNAVAILABLE}'; exit 1; }`, + `PROXY_PORT=$(/usr/bin/cat "$PROXY_PORT_FILE") || { printf '${DCODE_PROXY_UNAVAILABLE}'; exit 1; }`, + `case "$PROXY_HOST" in ""|*[!A-Za-z0-9._-]*) printf '${DCODE_PROXY_UNAVAILABLE}'; exit 1 ;; esac`, + `case "$PROXY_PORT" in ""|*[!0-9]*) printf '${DCODE_PROXY_UNAVAILABLE}'; exit 1 ;; esac`, + `[ "$PROXY_PORT" -ge 1 ] 2>/dev/null && [ "$PROXY_PORT" -le 65535 ] 2>/dev/null || { printf '${DCODE_PROXY_UNAVAILABLE}'; exit 1; }`, + 'PROXY_URL="http://${PROXY_HOST}:${PROXY_PORT}"', + 'export HTTP_PROXY="$PROXY_URL" HTTPS_PROXY="$PROXY_URL" http_proxy="$PROXY_URL" https_proxy="$PROXY_URL"', + 'export NO_PROXY="localhost,127.0.0.1,::1,${PROXY_HOST}" no_proxy="localhost,127.0.0.1,::1,${PROXY_HOST}"', + "unset ALL_PROXY all_proxy OPENAI_PROXY", + INFERENCE_ROUTE_PROBE_CORE_SCRIPT, ].join("; "); /** @@ -81,15 +73,12 @@ export function buildSandboxInferenceRouteProbeArgs( const command = agent?.name === "langchain-deepagents-code" ? [ - // Capture OpenShell's trusted CA before the login shell sources the - // DCode runtime environment. The login shell still reconstructs the - // proxy contract from /tmp/nemoclaw-proxy-env.sh after inherited - // proxy variables are cleared. + // Do not run a login shell: sandbox-writable startup files can write + // to every inherited descriptor. Reconstruct only the required proxy + // route from immutable image files before running the fixed probe. "sh", "-c", - DCODE_INFERENCE_ROUTE_PROBE_WRAPPER, - "nemoclaw-ca-capture", - INFERENCE_ROUTE_PROBE_FROM_ARG0_SCRIPT, + DCODE_INFERENCE_ROUTE_PROBE_SCRIPT, ] : ["sh", "-c", INFERENCE_ROUTE_PROBE_SCRIPT]; From 1510b70da8af7723a3589189f90759f12ad9d24a Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 14:49:40 -0700 Subject: [PATCH 4/4] fix(connect): remove spoofable DCode probe descriptor Signed-off-by: Apurv Kumaria --- .../connect-flow-dcode-probe-preamble.test.ts | 6 +- src/lib/actions/sandbox/connect-flow.test.ts | 7 +- .../connect-inference-route-probe.test.ts | 75 +++++++++++-------- .../sandbox/connect-inference-route-probe.ts | 53 ++++--------- 4 files changed, 64 insertions(+), 77 deletions(-) diff --git a/src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts b/src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts index e869bf57d6e..2bd1a2d963c 100644 --- a/src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts +++ b/src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts @@ -52,11 +52,7 @@ describe("connectSandbox DCode probe preamble boundary", () => { expect(harness.applyVmDnsMonkeypatchSpy).not.toHaveBeenCalled(); expect(harness.runSetupDnsProxySpy).not.toHaveBeenCalled(); expect(harness.runOpenshellSpy).not.toHaveBeenCalled(); - expect(harness.spawnSyncSpy).not.toHaveBeenCalledWith( - "openshell", - ["sandbox", "connect", "alpha"], - expect.any(Object), - ); + expect(harness.spawnSyncSpy).not.toHaveBeenCalled(); expect(harness.errorSpy.mock.calls.flat().join("\n")).toContain( "did not return a trusted result", ); diff --git a/src/lib/actions/sandbox/connect-flow.test.ts b/src/lib/actions/sandbox/connect-flow.test.ts index 63613749200..71f2200d2d8 100644 --- a/src/lib/actions/sandbox/connect-flow.test.ts +++ b/src/lib/actions/sandbox/connect-flow.test.ts @@ -237,7 +237,7 @@ describe("connectSandbox flow", () => { expect(exitSpy).toHaveBeenCalledWith(0); }); - it("runs the dcode inference route probe through its login-shell proxy contract (#6191)", async () => { + it("runs the DCode route probe through its managed runtime boundary (#6191)", async () => { const harness = createConnectHarness({ agentName: "langchain-deepagents-code", sessionAgent: { @@ -280,10 +280,9 @@ describe("connectSandbox flow", () => { "--name", "alpha", "--", - "sh", + "/usr/local/bin/nemoclaw-start", + "/bin/sh", "-c", - expect.stringContaining('bash -lc "$1" "$CA_BUNDLE"'), - "nemoclaw-ca-capture", expect.stringContaining("/usr/bin/curl"), ], expect.objectContaining({ ignoreError: true }), diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts index 49280cb7f42..ea3e4bf4f79 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts @@ -15,19 +15,25 @@ import { } from "./connect-inference-route-probe"; describe("sandbox connect inference route probe argv", () => { - it("uses the dcode login-shell proxy contract without inherited proxy variables (#6191)", () => { + it("uses the managed DCode proxy boundary without a login shell (#6191)", () => { const args = buildSandboxInferenceRouteProbeArgs("deep-code", { name: "langchain-deepagents-code", }); - expect(args.slice(0, 7)).toEqual(["sandbox", "exec", "--name", "deep-code", "--", "sh", "-c"]); - expect(args.at(-3)).toContain('bash -lc "$1" "$CA_BUNDLE"'); - expect(args.at(-3)).toContain("-u HTTPS_PROXY"); - expect(args.at(-3)).toContain("3>&1 1>/dev/null"); - expect(args.at(-2)).toBe("nemoclaw-ca-capture"); - expect(args.at(-1)).toContain("exec 1>&3 3>&-"); - expect(args.at(-1)).toContain('CA_BUNDLE="$0"'); + expect(args.slice(0, 8)).toEqual([ + "sandbox", + "exec", + "--name", + "deep-code", + "--", + "/usr/local/bin/nemoclaw-start", + "/bin/sh", + "-c", + ]); expect(args.at(-1)).toContain("https://inference.local/v1/models"); + expect(args).not.toContain("bash"); + expect(args.join(" ")).not.toContain("3>&1"); + expect(args.join(" ")).not.toContain("/tmp/nemoclaw-proxy-env.sh"); expect(args.every((arg) => !/[\r\n]/.test(arg))).toBe(true); }); @@ -83,31 +89,38 @@ describe("sandbox connect inference route probe argv", () => { it.each([ "OK 200", "BROKEN 503", - ])("isolates DCode login-shell startup output from a %s spoof (#6192)", (spoof) => { + ])("does not run a hostile DCode profile that writes %s to fd 3 (#6192)", (spoof) => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-probe-")); - const caBundle = path.join(home, "openshell-ca.pem"); const profileMarker = path.join(home, "profile-ran"); - fs.writeFileSync(caBundle, "test CA boundary", "utf8"); - fs.writeFileSync( - path.join(home, ".bash_profile"), - `printf '%s\\n' ${JSON.stringify(spoof)}; printf ran > ${JSON.stringify(profileMarker)}`, - ); - const args = buildSandboxInferenceRouteProbeArgs("deep-code", { - name: "langchain-deepagents-code", - }); - const wrapper = String(args.at(-3)).replace("HOME=/sandbox", `HOME=${JSON.stringify(home)}`); - const trustedProbe = "exec 1>&3 3>&-; printf 'BROKEN 000'"; - - const result = spawnSync("sh", ["-c", wrapper, String(args.at(-2)), trustedProbe], { - encoding: "utf8", - env: { ...process.env, CURL_CA_BUNDLE: caBundle, SSL_CERT_FILE: "" }, - }); - - expect(result.status).toBe(0); - expect(result.stdout).toBe("BROKEN 000"); - expect(result.stdout).not.toContain(spoof); - expect(fs.readFileSync(profileMarker, "utf8")).toBe("ran"); - fs.rmSync(home, { force: true, recursive: true }); + try { + const profile = path.join(home, ".bash_profile"); + const launcher = path.join(home, "nemoclaw-start"); + fs.writeFileSync( + profile, + `printf '%s' ${JSON.stringify(spoof)} >&3; printf ran > ${JSON.stringify(profileMarker)}; exit 0`, + ); + fs.writeFileSync(launcher, '#!/bin/bash -p\nset -eu\nunset BASH_ENV ENV\nexec "$@"\n', { + mode: 0o755, + }); + const args = buildSandboxInferenceRouteProbeArgs("deep-code", { + name: "langchain-deepagents-code", + }); + const command = args.slice(5); + command[0] = launcher; + command[command.length - 1] = "printf 'BROKEN 000'"; + + const result = spawnSync(command[0], command.slice(1), { + encoding: "utf8", + env: { ...process.env, BASH_ENV: profile, ENV: profile, HOME: home }, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("BROKEN 000"); + expect(result.stdout).not.toContain(spoof); + expect(fs.existsSync(profileMarker)).toBe(false); + } finally { + fs.rmSync(home, { force: true, recursive: true }); + } }); }); diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.ts b/src/lib/actions/sandbox/connect-inference-route-probe.ts index ebc64493435..9591f48bbd9 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.ts @@ -35,35 +35,17 @@ export const INFERENCE_ROUTE_PROBE_SCRIPT = [ INFERENCE_ROUTE_CA_VALIDATION, INFERENCE_ROUTE_PROBE_CORE_SCRIPT, ].join("; "); -const INFERENCE_ROUTE_PROBE_FROM_ARG0_SCRIPT = [ - // The outer DCode wrapper suppresses login-shell startup stdout. Restore the - // capture descriptor only after profile loading completes so profile output - // cannot impersonate trusted route evidence. - "exec 1>&3 3>&-", - 'CA_BUNDLE="$0"', - INFERENCE_ROUTE_PROBE_CORE_SCRIPT, -].join("; "); - -const PROXY_ENV_KEYS = [ - "HTTP_PROXY", - "HTTPS_PROXY", - "http_proxy", - "https_proxy", - "NO_PROXY", - "no_proxy", - "ALL_PROXY", - "all_proxy", -] as const; - -const DCODE_INFERENCE_ROUTE_PROBE_WRAPPER = [ - INFERENCE_ROUTE_CA_FROM_ENV, - INFERENCE_ROUTE_CA_VALIDATION, - // bash -lc receives CA_BUNDLE as argv[0], so the inner script reads the - // exact OpenShell-injected CA path from $0 after the login shell loads. FD 3 - // preserves the capture stream while startup stdout is discarded; the inner - // probe restores it before emitting its result. - `exec env ${PROXY_ENV_KEYS.map((key) => `-u ${key}`).join(" ")} HOME=/sandbox bash -lc "$1" "$CA_BUNDLE" 3>&1 1>/dev/null`, -].join("; "); +// Invalid state: a DCode login shell runs sandbox-user startup files before the +// probe, so every inherited output descriptor is attacker-writable evidence. +// Source boundary: the image-baked launcher reconstructs the managed proxy from +// root-owned, mode-0444 files and execs a command without loading user profiles. +// Source-fix constraint: raw OpenShell exec does not inherit the entrypoint's +// trusted proxy contract, while a login shell cannot provide an output trust +// boundary. Regression: hostile-profile tests assert that no startup file or +// inherited descriptor can emit probe evidence. Removal condition: use a raw +// probe only when OpenShell provides the same trusted proxy environment to every +// sandbox exec process without shell startup. +const DCODE_MANAGED_RUNTIME_LAUNCHER = "/usr/local/bin/nemoclaw-start"; /** * Classify a route result that is already known not to be healthy. @@ -81,15 +63,12 @@ export function buildSandboxInferenceRouteProbeArgs( const command = agent?.name === "langchain-deepagents-code" ? [ - // Capture OpenShell's trusted CA before the login shell sources the - // DCode runtime environment. The login shell still reconstructs the - // proxy contract from /tmp/nemoclaw-proxy-env.sh after inherited - // proxy variables are cleared. - "sh", + // The trusted launcher ignores ambient proxy overrides and does not + // source sandbox-user startup files before executing this probe. + DCODE_MANAGED_RUNTIME_LAUNCHER, + "/bin/sh", "-c", - DCODE_INFERENCE_ROUTE_PROBE_WRAPPER, - "nemoclaw-ca-capture", - INFERENCE_ROUTE_PROBE_FROM_ARG0_SCRIPT, + INFERENCE_ROUTE_PROBE_SCRIPT, ] : ["sh", "-c", INFERENCE_ROUTE_PROBE_SCRIPT];