Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/lib/actions/sandbox/connect-flow-dcode-probe-preamble.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
48 changes: 48 additions & 0 deletions src/lib/actions/sandbox/connect-inference-route-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -125,6 +160,19 @@ describe("sandbox inference route probe result", () => {
).toMatchObject({ healthy: false, broken: false, httpStatus: 0 });
});

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,
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,
Expand Down
14 changes: 11 additions & 3 deletions src/lib/actions/sandbox/connect-inference-route-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>&-",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
'CA_BUNDLE="$0"',
INFERENCE_ROUTE_PROBE_CORE_SCRIPT,
].join("; ");
Expand All @@ -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("; ");

/**
Expand Down Expand Up @@ -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;
Expand Down
Loading