From 9188073df064b5408d64803110a250114ecadeda Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Mon, 10 Aug 2026 10:43:48 +0800 Subject: [PATCH 01/13] fix(sandbox): run Deep Agents Code smoke commands without a login shell The terminal smoke runner executed every agent's smoke commands through `sh -lc`, so a sandbox-user `.bash_profile` or `.profile` ran before the managed command. For Deep Agents Code that startup file can emit output into the connect probe's evidence and create persistent side effects, which is the state the managed boundary is meant to bypass. Deep Agents Code smoke commands now run through the image-baked dcode-managed-exec launcher with BASH_ENV and ENV cleared and no login shell, matching how the managed inference route probe already invokes that launcher. Every other terminal agent keeps `sh -lc`. Their smoke commands depend on profile-provided PATH entries, and this defect is reported only against the Deep Agents Code managed boundary. Refs #8624 Signed-off-by: Dongni Yang --- src/lib/agent/terminal-smoke.test.ts | 54 +++++++++++++++++++++ src/lib/agent/terminal-smoke.ts | 72 +++++++++++++++++++++------- 2 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 src/lib/agent/terminal-smoke.test.ts diff --git a/src/lib/agent/terminal-smoke.test.ts b/src/lib/agent/terminal-smoke.test.ts new file mode 100644 index 00000000000..064a7ce40a6 --- /dev/null +++ b/src/lib/agent/terminal-smoke.test.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { DCODE_MANAGED_EXEC_LAUNCHER } from "../actions/sandbox/connect-inference-route-probe"; +import type { AgentDefinition } from "./defs"; +import { buildAgentSmokeArgs, runAgentSmokeCommands } from "./terminal-smoke"; + +function agent(name: string): AgentDefinition { + return { name, runtime: { smoke_commands: ["dcode --version"] } } as unknown as AgentDefinition; +} + +describe("terminal agent smoke command invocation", () => { + it("runs Deep Agents Code smoke commands without a login shell (#8624)", () => { + const args = buildAgentSmokeArgs( + "probe-box", + agent("langchain-deepagents-code"), + "dcode --version", + ); + + expect(args).not.toContain("-lc"); + expect(args.join(" ")).not.toContain("sh -lc"); + expect(args).toContain(DCODE_MANAGED_EXEC_LAUNCHER); + expect(args).toContain("BASH_ENV="); + expect(args).toContain("ENV="); + expect(args.at(-1)).toBe("dcode --version"); + }); + + it("keeps the login shell for other terminal agents (#8624)", () => { + const args = buildAgentSmokeArgs("probe-box", agent("hermes"), "hermes --version"); + + expect(args).toContain("-lc"); + expect(args).not.toContain(DCODE_MANAGED_EXEC_LAUNCHER); + expect(args.at(-1)).toBe("hermes --version"); + }); + + it("never issues a Deep Agents Code smoke exec through a login shell (#8624)", () => { + const issued: string[][] = []; + const result = runAgentSmokeCommands( + "probe-box", + agent("langchain-deepagents-code"), + (args) => { + issued.push(args); + return `NEMOCLAW_AGENT_SMOKE_EXIT:0\n`; + }, + ); + + expect(result).toEqual({ ok: true }); + expect(issued).toHaveLength(1); + expect(issued[0]).not.toContain("-lc"); + expect(issued[0]!.join(" ")).not.toContain("sh -lc"); + }); +}); diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index 47966deeff2..f9d3a35fb66 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { DCODE_MANAGED_EXEC_LAUNCHER } from "../actions/sandbox/connect-inference-route-probe"; import type { AgentDefinition } from "./defs"; type RunCaptureOpenshell = ( @@ -20,6 +21,58 @@ function getSmokeExitCode(output: string | null): number | null { return match ? Number.parseInt(match[1], 10) : null; } +function smokeRunner(loginShell: boolean): string { + const shell = loginShell ? "sh -lc" : "sh -c"; + return `${shell} "$1"; rc=$?; printf '\\n${SMOKE_EXIT_MARKER}%s\\n' "$rc"; exit 0`; +} + +/** + * Deep Agents Code smoke commands run through the same image-baked launcher the + * managed route probe uses, without a login shell (#8624). A login shell runs + * sandbox-user startup files, which can emit output into the probe's evidence + * and create persistent side effects before the managed command runs. Every + * other terminal agent keeps the login shell, which their smoke commands rely + * on for profile-provided PATH entries. + */ +export function buildAgentSmokeArgs( + sandboxName: string, + agent: AgentDefinition, + command: string, +): string[] { + if (agent.name === "langchain-deepagents-code") { + return [ + "sandbox", + "exec", + "-n", + sandboxName, + "--no-tty", + "--env", + "BASH_ENV=", + "--env", + "ENV=", + "--", + DCODE_MANAGED_EXEC_LAUNCHER, + "/bin/sh", + "-c", + smokeRunner(false), + "nemoclaw-agent-smoke", + command, + ]; + } + return [ + "sandbox", + "exec", + "-n", + sandboxName, + "--", + "sh", + "-lc", + smokeRunner(true), + "nemoclaw-agent-smoke", + command, + ]; +} + export function runAgentSmokeCommands( sandboxName: string, agent: AgentDefinition, @@ -28,23 +81,10 @@ export function runAgentSmokeCommands( // smoke_commands are shell-form commands from repository-shipped agents/*/manifest.yaml files. // Switch to argv-form commands before accepting custom or user-provided manifests here. const commands = agent.runtime?.smoke_commands ?? []; - const smokeRunner = `sh -lc "$1"; rc=$?; printf '\\n${SMOKE_EXIT_MARKER}%s\\n' "$rc"; exit 0`; for (const command of commands) { - const result = runCaptureOpenshell( - [ - "sandbox", - "exec", - "-n", - sandboxName, - "--", - "sh", - "-lc", - smokeRunner, - "nemoclaw-agent-smoke", - command, - ], - { ignoreError: true }, - ); + const result = runCaptureOpenshell(buildAgentSmokeArgs(sandboxName, agent, command), { + ignoreError: true, + }); const output = typeof result === "string" ? result : (result?.output ?? null); const exitCode = getSmokeExitCode(output); if (exitCode !== 0) { From 245da7c547e25ad768a1608bdf4deef2d8c71640 Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Mon, 10 Aug 2026 11:16:36 +0800 Subject: [PATCH 02/13] test(sandbox): read the smoke command from the end of the stub argv The CLI dispatch stub for terminal agents extracted the smoke command from a fixed argv position, so it stopped matching once the Deep Agents Code smoke invocation gained its launcher and environment flags. The stub then produced no smoke output and the probe reported failure. The smoke command is always the final argument, so read it from the end. The stub no longer depends on how many flags precede it. Refs #8624 Signed-off-by: Dongni Yang --- test/cli/connect-terminal-agent.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/cli/connect-terminal-agent.test.ts b/test/cli/connect-terminal-agent.test.ts index f5490da33fc..41fcfa29039 100644 --- a/test/cli/connect-terminal-agent.test.ts +++ b/test/cli/connect-terminal-agent.test.ts @@ -39,7 +39,9 @@ describe("CLI dispatch for terminal agents", () => { " exit 0", "fi", 'if [ "$1" = "sandbox" ] && [ "$2" = "exec" ] && [ "$3" = "-n" ] && [ "$4" = "alpha" ]; then', - ' cmd="${10}"', + // The smoke command is always the final argument. Read it from the end + // so the stub does not depend on how many flags precede it (#8624). + ' cmd="${*: -1}"', ' case "$cmd" in', ' *"dcode --version"*) echo "dcode 0.1.34"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', ' *"config.toml"*) echo "NEMOCLAW_DEEPAGENTS_CONFIG_OK"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', From 9410fe01beee56e58cd835f27f25fb61ab63d457 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 10 Aug 2026 15:00:25 -0700 Subject: [PATCH 03/13] docs(sandbox): clarify nested login shell boundary Signed-off-by: Apurv Kumaria --- .../connect-inference-route-probe.test.ts | 2 +- .../sandbox/connect-inference-route-probe.ts | 27 ++++++++++--------- src/lib/agent/terminal-smoke.test.ts | 4 +-- src/lib/agent/terminal-smoke.ts | 11 ++++---- 4 files changed, 23 insertions(+), 21 deletions(-) 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 208edaf2a27..9e0caa88e66 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts @@ -18,7 +18,7 @@ import { } from "./connect-inference-route-probe"; describe("sandbox connect inference route probe argv", () => { - it("uses the managed DCode proxy boundary without a login shell (#6191)", () => { + it("uses the managed DCode proxy boundary without adding a login shell (#6191)", () => { const args = buildSandboxInferenceRouteProbeArgs("deep-code", { name: "langchain-deepagents-code", }); diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.ts b/src/lib/actions/sandbox/connect-inference-route-probe.ts index 29e97d7da17..406bb985aaa 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.ts @@ -38,16 +38,17 @@ export const INFERENCE_ROUTE_PROBE_SCRIPT = [ INFERENCE_ROUTE_CA_VALIDATION, INFERENCE_ROUTE_PROBE_CORE_SCRIPT, ].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. +// Invalid state: OpenShell currently starts sandbox exec through a login shell +// before the requested command, so sandbox-user startup files can emit output +// and create side effects before this probe begins (#8624; OpenShell#2668). +// NemoClaw cannot prevent that transport behavior. The image-baked launcher +// reconstructs the managed proxy from root-owned, mode-0444 files without +// adding another profile-sourcing shell, and the parser rejects inherited +// stderr or extra stdout so startup output cannot become accepted probe +// evidence. Regression: hostile-profile tests cover contaminated output and +// inherited descriptors. Removal condition: use a raw probe only when OpenShell +// provides both a non-login exec path and the trusted proxy environment to every +// sandbox exec process. // This separate regular-file install is intentionally absent from older images: // a newer CLI probing one fails before the stateful entrypoint or dcode wrapper // can run, so version skew cannot mutate observability state. @@ -97,9 +98,9 @@ export function buildSandboxInferenceRouteProbeArgs( "--env", "ENV=", "--", - // The trusted launcher ignores ambient proxy overrides and does not - // source sandbox-user startup files or rewrite persistent runtime - // state before executing this probe. + // The trusted launcher ignores ambient proxy overrides and does not add + // another startup-file read or rewrite persistent runtime state. The + // OpenShell transport-level login shell remains tracked in OpenShell#2668. DCODE_MANAGED_EXEC_LAUNCHER, "/bin/sh", "-c", diff --git a/src/lib/agent/terminal-smoke.test.ts b/src/lib/agent/terminal-smoke.test.ts index 064a7ce40a6..df39df5c4dd 100644 --- a/src/lib/agent/terminal-smoke.test.ts +++ b/src/lib/agent/terminal-smoke.test.ts @@ -12,7 +12,7 @@ function agent(name: string): AgentDefinition { } describe("terminal agent smoke command invocation", () => { - it("runs Deep Agents Code smoke commands without a login shell (#8624)", () => { + it("runs Deep Agents Code smoke commands without adding a login shell (#8624)", () => { const args = buildAgentSmokeArgs( "probe-box", agent("langchain-deepagents-code"), @@ -35,7 +35,7 @@ describe("terminal agent smoke command invocation", () => { expect(args.at(-1)).toBe("hermes --version"); }); - it("never issues a Deep Agents Code smoke exec through a login shell (#8624)", () => { + it("does not add a login shell to Deep Agents Code smoke exec (#8624)", () => { const issued: string[][] = []; const result = runAgentSmokeCommands( "probe-box", diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index f9d3a35fb66..dc68ddd0677 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -28,11 +28,12 @@ function smokeRunner(loginShell: boolean): string { /** * Deep Agents Code smoke commands run through the same image-baked launcher the - * managed route probe uses, without a login shell (#8624). A login shell runs - * sandbox-user startup files, which can emit output into the probe's evidence - * and create persistent side effects before the managed command runs. Every - * other terminal agent keeps the login shell, which their smoke commands rely - * on for profile-provided PATH entries. + * managed route probe uses, without adding another login shell (#8624). The + * OpenShell transport still starts its own login shell before this command; see + * NVIDIA/OpenShell#2668. Avoiding two nested login shells here prevents two + * additional reads of sandbox-user startup files. Every other terminal agent + * keeps the existing nested shells because its smoke commands rely on + * profile-provided PATH entries. */ export function buildAgentSmokeArgs( sandboxName: string, From d5039af8cd326bba6bbbebd3c1490957dd527d9f Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 11 Aug 2026 10:55:16 -0700 Subject: [PATCH 04/13] fix(agent): reject forged smoke exit markers Signed-off-by: Prekshi Vyas --- src/lib/agent/terminal-smoke.test.ts | 14 ++++++++++++++ src/lib/agent/terminal-smoke.ts | 7 +++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/agent/terminal-smoke.test.ts b/src/lib/agent/terminal-smoke.test.ts index df39df5c4dd..86390b65845 100644 --- a/src/lib/agent/terminal-smoke.test.ts +++ b/src/lib/agent/terminal-smoke.test.ts @@ -51,4 +51,18 @@ describe("terminal agent smoke command invocation", () => { expect(issued[0]).not.toContain("-lc"); expect(issued[0]!.join(" ")).not.toContain("sh -lc"); }); + + it("rejects a success marker forged before the smoke runner result", () => { + const result = runAgentSmokeCommands("probe-box", agent("langchain-deepagents-code"), () => + ["NEMOCLAW_AGENT_SMOKE_EXIT:0", "dcode failed to start", "NEMOCLAW_AGENT_SMOKE_EXIT:42"].join( + "\n", + ), + ); + + expect(result).toEqual({ + ok: false, + command: "dcode --version", + output: "NEMOCLAW_AGENT_SMOKE_EXIT:0\ndcode failed to start\nNEMOCLAW_AGENT_SMOKE_EXIT:42", + }); + }); }); diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index dc68ddd0677..99a3e0e2a21 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -17,8 +17,11 @@ export type AgentSmokeCommandResult = function getSmokeExitCode(output: string | null): number | null { if (!output) return null; - const match = output.match(/(?:^|\n)NEMOCLAW_AGENT_SMOKE_EXIT:(\d+)(?:\n|$)/); - return match ? Number.parseInt(match[1], 10) : null; + const matches = [...output.matchAll(/(?:^|\n)NEMOCLAW_AGENT_SMOKE_EXIT:(\d+)(?=\n|$)/g)]; + // The managed runner emits exactly one result marker. Reject additional + // markers from transport login-shell startup output or the smoke command + // itself instead of allowing earlier output to forge success. + return matches.length === 1 ? Number.parseInt(matches[0]![1], 10) : null; } function smokeRunner(loginShell: boolean): string { From 7540048e5e791daca1d53967a87b738b9a38a65a Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 00:44:06 -0700 Subject: [PATCH 05/13] fix(agent): narrow smoke hardening scope Refs #8624 Signed-off-by: Apurv Kumaria --- src/lib/agent/terminal-smoke.test.ts | 14 -------------- src/lib/agent/terminal-smoke.ts | 10 ++++------ 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/lib/agent/terminal-smoke.test.ts b/src/lib/agent/terminal-smoke.test.ts index 86390b65845..df39df5c4dd 100644 --- a/src/lib/agent/terminal-smoke.test.ts +++ b/src/lib/agent/terminal-smoke.test.ts @@ -51,18 +51,4 @@ describe("terminal agent smoke command invocation", () => { expect(issued[0]).not.toContain("-lc"); expect(issued[0]!.join(" ")).not.toContain("sh -lc"); }); - - it("rejects a success marker forged before the smoke runner result", () => { - const result = runAgentSmokeCommands("probe-box", agent("langchain-deepagents-code"), () => - ["NEMOCLAW_AGENT_SMOKE_EXIT:0", "dcode failed to start", "NEMOCLAW_AGENT_SMOKE_EXIT:42"].join( - "\n", - ), - ); - - expect(result).toEqual({ - ok: false, - command: "dcode --version", - output: "NEMOCLAW_AGENT_SMOKE_EXIT:0\ndcode failed to start\nNEMOCLAW_AGENT_SMOKE_EXIT:42", - }); - }); }); diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index 99a3e0e2a21..e22c99d269c 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -17,11 +17,8 @@ export type AgentSmokeCommandResult = function getSmokeExitCode(output: string | null): number | null { if (!output) return null; - const matches = [...output.matchAll(/(?:^|\n)NEMOCLAW_AGENT_SMOKE_EXIT:(\d+)(?=\n|$)/g)]; - // The managed runner emits exactly one result marker. Reject additional - // markers from transport login-shell startup output or the smoke command - // itself instead of allowing earlier output to forge success. - return matches.length === 1 ? Number.parseInt(matches[0]![1], 10) : null; + const match = output.match(/(?:^|\n)NEMOCLAW_AGENT_SMOKE_EXIT:(\d+)(?:\n|$)/); + return match ? Number.parseInt(match[1], 10) : null; } function smokeRunner(loginShell: boolean): string { @@ -36,7 +33,8 @@ function smokeRunner(loginShell: boolean): string { * NVIDIA/OpenShell#2668. Avoiding two nested login shells here prevents two * additional reads of sandbox-user startup files. Every other terminal agent * keeps the existing nested shells because its smoke commands rely on - * profile-provided PATH entries. + * profile-provided PATH entries. The smoke marker is diagnostic evidence only: + * the upstream transport shell can emit it before this managed command starts. */ export function buildAgentSmokeArgs( sandboxName: string, From b63f6e2d6506e760096a6d57254b92b3b456038c Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 02:38:07 -0700 Subject: [PATCH 06/13] fix(agent): bind DCode smoke evidence to managed launch Signed-off-by: Apurv Kumaria --- .../sandbox/terminal-connect-probe.test.ts | 4 ++- .../agent/onboard-terminal-fixtures.test.ts | 4 ++- src/lib/agent/onboard-terminal-fixtures.ts | 8 ++--- src/lib/agent/terminal-smoke.test.ts | 24 ++++++++++++- src/lib/agent/terminal-smoke.ts | 34 ++++++++++++++----- test/cli/connect-terminal-agent.test.ts | 6 ++-- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/lib/actions/sandbox/terminal-connect-probe.test.ts b/src/lib/actions/sandbox/terminal-connect-probe.test.ts index 694fb5820f3..1ac11f6826a 100644 --- a/src/lib/actions/sandbox/terminal-connect-probe.test.ts +++ b/src/lib/actions/sandbox/terminal-connect-probe.test.ts @@ -87,7 +87,9 @@ describe("terminal-agent connect inference route", () => { }); it("lets dcode continue to terminal smoke checks when its route probe is inconclusive (#6191)", () => { - const capture = vi.fn(() => "dcode 0.1.12\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n"); + const capture = vi.fn( + () => "NEMOCLAW_AGENT_SMOKE_BEGIN\ndcode 0.1.12\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n", + ); const ensureInferenceRoute = vi.fn(() => ({ routeHealthy: null })); expect(() => diff --git a/src/lib/agent/onboard-terminal-fixtures.test.ts b/src/lib/agent/onboard-terminal-fixtures.test.ts index 4153ee9f0af..70d7800a4ca 100644 --- a/src/lib/agent/onboard-terminal-fixtures.test.ts +++ b/src/lib/agent/onboard-terminal-fixtures.test.ts @@ -152,7 +152,9 @@ describe("Deep Agents Code terminal onboard fixtures", () => { calls, ); - expect(output).toBe("NEMOCLAW_DEEPAGENTS_CONFIG_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"); + expect(output).toBe( + "NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_DEEPAGENTS_CONFIG_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0", + ); }); it("can model a nonzero terminal smoke command", () => { diff --git a/src/lib/agent/onboard-terminal-fixtures.ts b/src/lib/agent/onboard-terminal-fixtures.ts index 200131d3b2b..5a5115f0ebf 100644 --- a/src/lib/agent/onboard-terminal-fixtures.ts +++ b/src/lib/agent/onboard-terminal-fixtures.ts @@ -22,13 +22,13 @@ function recordDeepAgentsRuntimeCall( return probeOutput; } if (command.includes("dcode --version")) { - return `dcode ${smokeVersion}\nNEMOCLAW_AGENT_SMOKE_EXIT:0`; + return `NEMOCLAW_AGENT_SMOKE_BEGIN\ndcode ${smokeVersion}\nNEMOCLAW_AGENT_SMOKE_EXIT:0`; } if (command.includes("NEMOCLAW_DCODE_EMPTY_PROMPT_OK")) { - return "NEMOCLAW_DCODE_EMPTY_PROMPT_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"; + return "NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_DCODE_EMPTY_PROMPT_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"; } if (command.includes("/sandbox/.deepagents/config.toml")) { - return "NEMOCLAW_DEEPAGENTS_CONFIG_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"; + return "NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_DEEPAGENTS_CONFIG_OK\nNEMOCLAW_AGENT_SMOKE_EXIT:0"; } return ""; } @@ -62,5 +62,5 @@ export function recordFailingDeepAgentsSmokeCall(args: string[]): string { const command = args.slice(args.indexOf("--") + 1).at(-1) ?? ""; return command.includes("NEMOCLAW_AGENT_BINARY_CHECK") ? "NEMOCLAW_AGENT_BINARY_CHECK:ok" - : "dcode provider route failed\nNEMOCLAW_AGENT_SMOKE_EXIT:42"; + : "NEMOCLAW_AGENT_SMOKE_BEGIN\ndcode provider route failed\nNEMOCLAW_AGENT_SMOKE_EXIT:42"; } diff --git a/src/lib/agent/terminal-smoke.test.ts b/src/lib/agent/terminal-smoke.test.ts index df39df5c4dd..09ed240542c 100644 --- a/src/lib/agent/terminal-smoke.test.ts +++ b/src/lib/agent/terminal-smoke.test.ts @@ -22,6 +22,7 @@ describe("terminal agent smoke command invocation", () => { expect(args).not.toContain("-lc"); expect(args.join(" ")).not.toContain("sh -lc"); expect(args).toContain(DCODE_MANAGED_EXEC_LAUNCHER); + expect(args).toContain("HOME=/usr/local/lib/nemoclaw"); expect(args).toContain("BASH_ENV="); expect(args).toContain("ENV="); expect(args.at(-1)).toBe("dcode --version"); @@ -42,7 +43,7 @@ describe("terminal agent smoke command invocation", () => { agent("langchain-deepagents-code"), (args) => { issued.push(args); - return `NEMOCLAW_AGENT_SMOKE_EXIT:0\n`; + return `NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n`; }, ); @@ -51,4 +52,25 @@ describe("terminal agent smoke command invocation", () => { expect(issued[0]).not.toContain("-lc"); expect(issued[0]!.join(" ")).not.toContain("sh -lc"); }); + + it("rejects a forged success marker emitted before the managed runner starts (#8624)", () => { + const result = runAgentSmokeCommands( + "probe-box", + agent("langchain-deepagents-code"), + () => "NEMOCLAW_AGENT_SMOKE_EXIT:0\n", + ); + + expect(result).toMatchObject({ ok: false, command: "dcode --version" }); + }); + + it("rejects extra marker evidence around the managed runner boundary (#8624)", () => { + const result = runAgentSmokeCommands( + "probe-box", + agent("langchain-deepagents-code"), + () => + "NEMOCLAW_AGENT_SMOKE_EXIT:0\nNEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:42\n", + ); + + expect(result).toMatchObject({ ok: false, command: "dcode --version" }); + }); }); diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index e22c99d269c..c72bd06305a 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -10,20 +10,33 @@ type RunCaptureOpenshell = ( ) => string | { output?: string | null } | null; const SMOKE_EXIT_MARKER = "NEMOCLAW_AGENT_SMOKE_EXIT:"; +const SMOKE_BEGIN_MARKER = "NEMOCLAW_AGENT_SMOKE_BEGIN"; export type AgentSmokeCommandResult = | { ok: true } | { ok: false; command: string; output: string | null }; -function getSmokeExitCode(output: string | null): number | null { +function getSmokeExitCode(output: string | null, requireManagedBoundary: boolean): number | null { if (!output) return null; - const match = output.match(/(?:^|\n)NEMOCLAW_AGENT_SMOKE_EXIT:(\d+)(?:\n|$)/); - return match ? Number.parseInt(match[1], 10) : null; + const exitMatches = [...output.matchAll(/(?:^|\n)NEMOCLAW_AGENT_SMOKE_EXIT:(\d+)(?=\n|$)/g)]; + if (!requireManagedBoundary) { + const match = exitMatches[0]; + return match ? Number.parseInt(match[1]!, 10) : null; + } + const beginMatches = [...output.matchAll(/(?:^|\n)NEMOCLAW_AGENT_SMOKE_BEGIN(?=\n|$)/g)]; + if ( + beginMatches.length !== 1 || + exitMatches.length !== 1 || + beginMatches[0]!.index >= exitMatches[0]!.index + ) { + return null; + } + return Number.parseInt(exitMatches[0]![1]!, 10); } function smokeRunner(loginShell: boolean): string { const shell = loginShell ? "sh -lc" : "sh -c"; - return `${shell} "$1"; rc=$?; printf '\\n${SMOKE_EXIT_MARKER}%s\\n' "$rc"; exit 0`; + return `printf '${SMOKE_BEGIN_MARKER}\\n'; ${shell} "$1"; rc=$?; printf '\\n${SMOKE_EXIT_MARKER}%s\\n' "$rc"; exit 0`; } /** @@ -31,10 +44,11 @@ function smokeRunner(loginShell: boolean): string { * managed route probe uses, without adding another login shell (#8624). The * OpenShell transport still starts its own login shell before this command; see * NVIDIA/OpenShell#2668. Avoiding two nested login shells here prevents two - * additional reads of sandbox-user startup files. Every other terminal agent - * keeps the existing nested shells because its smoke commands rely on - * profile-provided PATH entries. The smoke marker is diagnostic evidence only: - * the upstream transport shell can emit it before this managed command starts. + * additional reads of sandbox-user startup files. The transport HOME is the + * image-baked root-owned NemoClaw directory, and the managed runner emits one + * ordered begin/exit evidence pair. Every other terminal agent keeps the + * existing nested shells because its smoke commands rely on profile-provided + * PATH entries and retains its legacy diagnostic marker. */ export function buildAgentSmokeArgs( sandboxName: string, @@ -49,6 +63,8 @@ export function buildAgentSmokeArgs( sandboxName, "--no-tty", "--env", + "HOME=/usr/local/lib/nemoclaw", + "--env", "BASH_ENV=", "--env", "ENV=", @@ -88,7 +104,7 @@ export function runAgentSmokeCommands( ignoreError: true, }); const output = typeof result === "string" ? result : (result?.output ?? null); - const exitCode = getSmokeExitCode(output); + const exitCode = getSmokeExitCode(output, agent.name === "langchain-deepagents-code"); if (exitCode !== 0) { return { ok: false, command, output }; } diff --git a/test/cli/connect-terminal-agent.test.ts b/test/cli/connect-terminal-agent.test.ts index 41fcfa29039..cfb1fc69bd2 100644 --- a/test/cli/connect-terminal-agent.test.ts +++ b/test/cli/connect-terminal-agent.test.ts @@ -43,9 +43,9 @@ describe("CLI dispatch for terminal agents", () => { // so the stub does not depend on how many flags precede it (#8624). ' cmd="${*: -1}"', ' case "$cmd" in', - ' *"dcode --version"*) echo "dcode 0.1.34"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', - ' *"config.toml"*) echo "NEMOCLAW_DEEPAGENTS_CONFIG_OK"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', - ' *"NEMOCLAW_DCODE_EMPTY_PROMPT_OK"*) echo "NEMOCLAW_DCODE_EMPTY_PROMPT_OK"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', + ' *"dcode --version"*) echo "NEMOCLAW_AGENT_SMOKE_BEGIN"; echo "dcode 0.1.34"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', + ' *"config.toml"*) echo "NEMOCLAW_AGENT_SMOKE_BEGIN"; echo "NEMOCLAW_DEEPAGENTS_CONFIG_OK"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', + ' *"NEMOCLAW_DCODE_EMPTY_PROMPT_OK"*) echo "NEMOCLAW_AGENT_SMOKE_BEGIN"; echo "NEMOCLAW_DCODE_EMPTY_PROMPT_OK"; echo "NEMOCLAW_AGENT_SMOKE_EXIT:0"; exit 0 ;;', " esac", "fi", "exit 0", From 6926d02309b01f55c7cde6dafe3855a9ebc2b299 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 11:07:06 -0700 Subject: [PATCH 07/13] test(dcode): cover hostile login profile boundary Signed-off-by: Apurv Kumaria --- .../04-deepagents-code-fresh-reonboard.sh | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh index 667ed687a5e..7ccde6c0cbc 100755 --- a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh +++ b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh @@ -19,6 +19,8 @@ PRIMARY_TARGET_MODEL="openai/openai/gpt-5.5" FALLBACK_TARGET_MODEL="nvidia/nvidia/nemotron-3-ultra" HOSTED_ENDPOINT="${NEMOCLAW_ENDPOINT_URL:-https://inference-api.nvidia.com/v1}" CREDENTIAL_CANARY="nemoclaw-dcode-config-get-canary" +HOSTILE_LOGIN_PROFILE="/sandbox/.bash_profile" +HOSTILE_PROFILE_MARKER="/sandbox/.nemoclaw-dcode-hostile-profile-loaded" fail() { printf '%s: FAIL: %s\n' "$PREFIX" "$1" >&2 @@ -33,6 +35,13 @@ sandbox_exec() { openshell sandbox exec --name "$SANDBOX_NAME" -- bash -c "$1" 2>&1 } +cleanup_hostile_login_profile() { + openshell sandbox exec --name "$SANDBOX_NAME" \ + --env HOME=/usr/local/lib/nemoclaw -- \ + /bin/sh -c "rm -f '$HOSTILE_LOGIN_PROFILE' '$HOSTILE_PROFILE_MARKER'" \ + >/dev/null 2>&1 || true +} + dcode_identity() { # Invoke dcode by absolute path: `openshell sandbox exec -- dcode ...` runs # without a login shell, so /usr/local/bin is not on PATH and a bare `dcode` @@ -192,6 +201,34 @@ model_a="$(identity_field "$identity_before" Model)" model_a="${model_a#openai:}" [ -n "$model_a" ] || fail "initial dcode identity did not report a model" assert_identity "$identity_before" "$model_a" "initial" +pass "initial live identity reports model A" + +# OpenShell starts sandbox exec through a login shell. Plant a writable startup +# file that would leave a side effect, forge the legacy success marker, and stop +# the requested command if the DCode connect probe did not assign its root-owned +# transport HOME before shell startup. The supported probe must still reach the +# managed runner and the hostile profile must remain unexecuted (#8624). +cleanup_hostile_login_profile +trap cleanup_hostile_login_profile EXIT +sandbox_exec "umask 077; printf '%s\n' 'touch $HOSTILE_PROFILE_MARKER' 'printf \"%s\\n\" NEMOCLAW_AGENT_SMOKE_EXIT:0' 'exit 97' > '$HOSTILE_LOGIN_PROFILE'" \ + >/dev/null || fail "could not install the hostile DCode login profile" + +set +e +hostile_profile_connect_output="$("$CLI" "$SANDBOX_NAME" connect --probe-only 2>&1)" +hostile_profile_connect_status=$? +set -e +marker_state="$({ + openshell sandbox exec --name "$SANDBOX_NAME" \ + --env HOME=/usr/local/lib/nemoclaw -- \ + /bin/sh -c "if [ -e '$HOSTILE_PROFILE_MARKER' ]; then printf PROFILE_LOADED; else printf PROFILE_NOT_LOADED; fi" +} 2>&1)" || fail "could not inspect the hostile DCode profile marker" +cleanup_hostile_login_profile +trap - EXIT + +[ "$hostile_profile_connect_status" -eq 0 ] || fail "probe-only connect failed with a hostile login profile: $hostile_profile_connect_output" +printf '%s\n' "$hostile_profile_connect_output" | grep -Fq "terminal smoke checks passed" || fail "probe-only connect did not reach the DCode smoke boundary" +[ "$marker_state" = "PROFILE_NOT_LOADED" ] || fail "DCode transport executed the hostile login profile: $marker_state" +pass "probe-only connect bypasses a hostile sandbox-user login profile" if [ "$model_a" = "$PRIMARY_TARGET_MODEL" ]; then model_b="$FALLBACK_TARGET_MODEL" @@ -199,7 +236,6 @@ else model_b="$PRIMARY_TARGET_MODEL" fi [ "$model_a" != "$model_b" ] || fail "model A and model B must differ" -pass "initial live identity reports model A" seed_source="$(seed_config_source)" seed_output="$( @@ -338,4 +374,4 @@ verify_output="$( printf '%s\n' "$verify_output" | grep -Fq "NEMOCLAW_DCODE_FRESH_CONFIG_VERIFIED" || fail "fresh config verification marker is missing" pass "config keeps model B and only the allowlisted preferences" -printf '%s: 11 passed, 0 failed\n' "$PREFIX" +printf '%s: 12 passed, 0 failed\n' "$PREFIX" From c00b0f09b635938823b6d49efe907c8e187566e6 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 11:36:17 -0700 Subject: [PATCH 08/13] test(dcode): verify hostile profile fails closed Signed-off-by: Apurv Kumaria --- .../connect-inference-route-probe.test.ts | 2 +- src/lib/agent/terminal-smoke.ts | 14 +++++---- .../04-deepagents-code-fresh-reonboard.sh | 29 ++++++++++++------- 3 files changed, 27 insertions(+), 18 deletions(-) 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 9e0caa88e66..0e3e7e45703 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.test.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.test.ts @@ -99,7 +99,7 @@ describe("sandbox connect inference route probe argv", () => { it.each([ "OK 200", "BROKEN 503", - ])("does not run hostile DCode startup or curl config for a %s spoof (#6192)", (spoof) => { + ])("managed launcher does not run hostile DCode startup or curl config for a %s spoof (#6192)", (spoof) => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-probe-")); const profileMarker = path.join(home, "profile-ran"); try { diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index c72bd06305a..59cdf944834 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -43,12 +43,14 @@ function smokeRunner(loginShell: boolean): string { * Deep Agents Code smoke commands run through the same image-baked launcher the * managed route probe uses, without adding another login shell (#8624). The * OpenShell transport still starts its own login shell before this command; see - * NVIDIA/OpenShell#2668. Avoiding two nested login shells here prevents two - * additional reads of sandbox-user startup files. The transport HOME is the - * image-baked root-owned NemoClaw directory, and the managed runner emits one - * ordered begin/exit evidence pair. Every other terminal agent keeps the - * existing nested shells because its smoke commands rely on profile-provided - * PATH entries and retains its legacy diagnostic marker. + * NVIDIA/OpenShell#2668. That transport shell can read the sandbox-user profile + * before these requested-command environment assignments apply. Using the + * image-baked root-owned NemoClaw HOME and avoiding two nested login shells + * prevents additional startup-file reads, while the managed runner's single + * ordered begin/exit evidence pair ensures transport output cannot become an + * accepted smoke result. Every other terminal agent keeps the existing nested + * shells because its smoke commands rely on profile-provided PATH entries and + * retains its legacy diagnostic marker. */ export function buildAgentSmokeArgs( sandboxName: string, diff --git a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh index 7ccde6c0cbc..6b1052addf7 100755 --- a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh +++ b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh @@ -203,14 +203,15 @@ model_a="${model_a#openai:}" assert_identity "$identity_before" "$model_a" "initial" pass "initial live identity reports model A" -# OpenShell starts sandbox exec through a login shell. Plant a writable startup -# file that would leave a side effect, forge the legacy success marker, and stop -# the requested command if the DCode connect probe did not assign its root-owned -# transport HOME before shell startup. The supported probe must still reach the -# managed runner and the hostile profile must remain unexecuted (#8624). +# OpenShell starts sandbox exec through a login shell before applying the +# requested command's environment (OpenShell#2668). Plant a writable startup +# file that attacks only a DCode managed-exec command, leaving inspection and +# cleanup commands available. It leaves a side effect, forges the legacy success +# marker, and stops the requested command. The supported probe must fail closed; +# startup output must not become accepted managed-runner evidence (#8624). cleanup_hostile_login_profile trap cleanup_hostile_login_profile EXIT -sandbox_exec "umask 077; printf '%s\n' 'touch $HOSTILE_PROFILE_MARKER' 'printf \"%s\\n\" NEMOCLAW_AGENT_SMOKE_EXIT:0' 'exit 97' > '$HOSTILE_LOGIN_PROFILE'" \ +sandbox_exec "umask 077; printf '%s\n' 'case \"\${BASH_EXECUTION_STRING:-}\" in' ' *dcode-managed-exec*)' ' touch $HOSTILE_PROFILE_MARKER' ' printf \"%s\\n\" NEMOCLAW_AGENT_SMOKE_EXIT:0' ' exit 97' ' ;;' 'esac' > '$HOSTILE_LOGIN_PROFILE'" \ >/dev/null || fail "could not install the hostile DCode login profile" set +e @@ -225,10 +226,16 @@ marker_state="$({ cleanup_hostile_login_profile trap - EXIT -[ "$hostile_profile_connect_status" -eq 0 ] || fail "probe-only connect failed with a hostile login profile: $hostile_profile_connect_output" -printf '%s\n' "$hostile_profile_connect_output" | grep -Fq "terminal smoke checks passed" || fail "probe-only connect did not reach the DCode smoke boundary" -[ "$marker_state" = "PROFILE_NOT_LOADED" ] || fail "DCode transport executed the hostile login profile: $marker_state" -pass "probe-only connect bypasses a hostile sandbox-user login profile" +[ "$hostile_profile_connect_status" -ne 0 ] || fail "probe-only connect accepted a hostile login-profile result" +if printf '%s\n' "$hostile_profile_connect_output" | grep -Fq "terminal smoke checks passed"; then + fail "probe-only connect accepted a forged hostile-profile success marker" +fi +[ "$marker_state" = "PROFILE_LOADED" ] || fail "hostile login profile did not exercise the OpenShell transport boundary: $marker_state" +pass "hostile login-profile output fails closed at the DCode smoke boundary" + +clean_profile_connect_output="$("$CLI" "$SANDBOX_NAME" connect --probe-only 2>&1)" || fail "probe-only connect did not recover after hostile-profile cleanup: $clean_profile_connect_output" +printf '%s\n' "$clean_profile_connect_output" | grep -Fq "terminal smoke checks passed" || fail "cleaned probe-only connect did not reach the DCode smoke boundary" +pass "probe-only connect succeeds after hostile-profile cleanup" if [ "$model_a" = "$PRIMARY_TARGET_MODEL" ]; then model_b="$FALLBACK_TARGET_MODEL" @@ -374,4 +381,4 @@ verify_output="$( printf '%s\n' "$verify_output" | grep -Fq "NEMOCLAW_DCODE_FRESH_CONFIG_VERIFIED" || fail "fresh config verification marker is missing" pass "config keeps model B and only the allowlisted preferences" -printf '%s: 12 passed, 0 failed\n' "$PREFIX" +printf '%s: 13 passed, 0 failed\n' "$PREFIX" From a34bbd3c5560d2355ab7c4a4e214d2bfdb7b7128 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 11:47:30 -0700 Subject: [PATCH 09/13] fix(dcode): honor smoke transport failures Signed-off-by: Apurv Kumaria --- .../sandbox/terminal-connect-probe.test.ts | 27 ++++++++++++++ src/lib/agent/terminal-smoke.test.ts | 25 +++++++------ src/lib/agent/terminal-smoke.ts | 20 ++++++---- .../04-deepagents-code-fresh-reonboard.sh | 37 ++++++++++++------- 4 files changed, 76 insertions(+), 33 deletions(-) diff --git a/src/lib/actions/sandbox/terminal-connect-probe.test.ts b/src/lib/actions/sandbox/terminal-connect-probe.test.ts index 1ac11f6826a..79fe96dd461 100644 --- a/src/lib/actions/sandbox/terminal-connect-probe.test.ts +++ b/src/lib/actions/sandbox/terminal-connect-probe.test.ts @@ -109,4 +109,31 @@ describe("terminal-agent connect inference route", () => { " Probe complete: LangChain Deep Agents Code terminal smoke checks passed (dcode).", ); }); + + it("fails dcode connect when a hostile profile forges markers before a nonzero exit (#8624)", () => { + const capture = vi.fn(() => ({ + status: 97, + output: "NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n", + })); + const ensureInferenceRoute = vi.fn(() => ({ routeHealthy: true })); + + expect(() => + runTerminalAgentConnectProbe({ + agent: dcodeAgent, + agentName: "LangChain Deep Agents Code", + capture: capture as never, + ensureInferenceRoute, + sandboxName: "deep-code", + }), + ).toThrow("process.exit(1)"); + + expect(capture).toHaveBeenCalledOnce(); + expect(errorSpy).toHaveBeenCalledWith( + " Probe failed: LangChain Deep Agents Code terminal smoke command failed: dcode --version", + ); + expect(logSpy).not.toHaveBeenCalledWith( + expect.stringContaining("terminal smoke checks passed"), + ); + expect(exitSpy).toHaveBeenCalledWith(1); + }); }); diff --git a/src/lib/agent/terminal-smoke.test.ts b/src/lib/agent/terminal-smoke.test.ts index 09ed240542c..7e706345b5e 100644 --- a/src/lib/agent/terminal-smoke.test.ts +++ b/src/lib/agent/terminal-smoke.test.ts @@ -43,7 +43,10 @@ describe("terminal agent smoke command invocation", () => { agent("langchain-deepagents-code"), (args) => { issued.push(args); - return `NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n`; + return { + status: 0, + output: `NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n`, + }; }, ); @@ -53,23 +56,21 @@ describe("terminal agent smoke command invocation", () => { expect(issued[0]!.join(" ")).not.toContain("sh -lc"); }); - it("rejects a forged success marker emitted before the managed runner starts (#8624)", () => { - const result = runAgentSmokeCommands( - "probe-box", - agent("langchain-deepagents-code"), - () => "NEMOCLAW_AGENT_SMOKE_EXIT:0\n", - ); + it("rejects forged managed markers when the transport exits before the runner (#8624)", () => { + const result = runAgentSmokeCommands("probe-box", agent("langchain-deepagents-code"), () => ({ + status: 97, + output: "NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n", + })); expect(result).toMatchObject({ ok: false, command: "dcode --version" }); }); it("rejects extra marker evidence around the managed runner boundary (#8624)", () => { - const result = runAgentSmokeCommands( - "probe-box", - agent("langchain-deepagents-code"), - () => + const result = runAgentSmokeCommands("probe-box", agent("langchain-deepagents-code"), () => ({ + status: 0, + output: "NEMOCLAW_AGENT_SMOKE_EXIT:0\nNEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:42\n", - ); + })); expect(result).toMatchObject({ ok: false, command: "dcode --version" }); }); diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index 59cdf944834..202d097c123 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -7,7 +7,7 @@ import type { AgentDefinition } from "./defs"; type RunCaptureOpenshell = ( args: string[], opts?: { ignoreError?: boolean; timeout?: number }, -) => string | { output?: string | null } | null; +) => string | { status?: number | null; output?: string | null } | null; const SMOKE_EXIT_MARKER = "NEMOCLAW_AGENT_SMOKE_EXIT:"; const SMOKE_BEGIN_MARKER = "NEMOCLAW_AGENT_SMOKE_BEGIN"; @@ -46,11 +46,12 @@ function smokeRunner(loginShell: boolean): string { * NVIDIA/OpenShell#2668. That transport shell can read the sandbox-user profile * before these requested-command environment assignments apply. Using the * image-baked root-owned NemoClaw HOME and avoiding two nested login shells - * prevents additional startup-file reads, while the managed runner's single - * ordered begin/exit evidence pair ensures transport output cannot become an - * accepted smoke result. Every other terminal agent keeps the existing nested - * shells because its smoke commands rely on profile-provided PATH entries and - * retains its legacy diagnostic marker. + * prevents additional startup-file reads. The managed runner's single ordered + * begin/exit pair remains diagnostic rather than a trust boundary; when the + * caller preserves OpenShell's process status, a nonzero transport exit cannot + * be hidden by forged marker output. Every other terminal agent keeps the + * existing nested shells because its smoke commands rely on profile-provided + * PATH entries and retains its legacy diagnostic marker. */ export function buildAgentSmokeArgs( sandboxName: string, @@ -106,8 +107,11 @@ export function runAgentSmokeCommands( ignoreError: true, }); const output = typeof result === "string" ? result : (result?.output ?? null); - const exitCode = getSmokeExitCode(output, agent.name === "langchain-deepagents-code"); - if (exitCode !== 0) { + const requireManagedBoundary = agent.name === "langchain-deepagents-code"; + const exitCode = getSmokeExitCode(output, requireManagedBoundary); + const transportFailed = + requireManagedBoundary && typeof result !== "string" && result?.status !== 0; + if (exitCode !== 0 || transportFailed) { return { ok: false, command, output }; } } diff --git a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh index 6b1052addf7..cc9aa0ca410 100755 --- a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh +++ b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh @@ -36,9 +36,15 @@ sandbox_exec() { } cleanup_hostile_login_profile() { - openshell sandbox exec --name "$SANDBOX_NAME" \ - --env HOME=/usr/local/lib/nemoclaw -- \ - /bin/sh -c "rm -f '$HOSTILE_LOGIN_PROFILE' '$HOSTILE_PROFILE_MARKER'" \ + local container_id + container_id="$( + docker ps \ + --filter "label=openshell.ai/sandbox-name=$SANDBOX_NAME" \ + --format '{{.ID}}' 2>/dev/null | head -n 1 + )" + [ -n "$container_id" ] || return 0 + docker exec --user 0 "$container_id" /bin/sh -c \ + "rm -f '$HOSTILE_LOGIN_PROFILE' '$HOSTILE_PROFILE_MARKER'" \ >/dev/null 2>&1 || true } @@ -205,24 +211,29 @@ pass "initial live identity reports model A" # OpenShell starts sandbox exec through a login shell before applying the # requested command's environment (OpenShell#2668). Plant a writable startup -# file that attacks only a DCode managed-exec command, leaving inspection and -# cleanup commands available. It leaves a side effect, forges the legacy success -# marker, and stops the requested command. The supported probe must fail closed; -# startup output must not become accepted managed-runner evidence (#8624). +# file that attacks only a DCode smoke command. It leaves a side effect, forges +# the exact ordered marker pair, and exits nonzero before the managed runner. +# The supported probe must fail closed, and direct container authority performs +# inspection and cleanup without traversing the hostile profile (#8624). cleanup_hostile_login_profile trap cleanup_hostile_login_profile EXIT -sandbox_exec "umask 077; printf '%s\n' 'case \"\${BASH_EXECUTION_STRING:-}\" in' ' *dcode-managed-exec*)' ' touch $HOSTILE_PROFILE_MARKER' ' printf \"%s\\n\" NEMOCLAW_AGENT_SMOKE_EXIT:0' ' exit 97' ' ;;' 'esac' > '$HOSTILE_LOGIN_PROFILE'" \ +sandbox_exec "umask 077; printf '%s\n' 'case \"\${BASH_EXECUTION_STRING:-}\" in' ' *NEMOCLAW_AGENT_SMOKE_BEGIN*)' ' touch $HOSTILE_PROFILE_MARKER' ' printf \"%s\\n\" NEMOCLAW_AGENT_SMOKE_BEGIN NEMOCLAW_AGENT_SMOKE_EXIT:0' ' exit 97' ' ;;' 'esac' > '$HOSTILE_LOGIN_PROFILE'" \ >/dev/null || fail "could not install the hostile DCode login profile" set +e hostile_profile_connect_output="$("$CLI" "$SANDBOX_NAME" connect --probe-only 2>&1)" hostile_profile_connect_status=$? set -e -marker_state="$({ - openshell sandbox exec --name "$SANDBOX_NAME" \ - --env HOME=/usr/local/lib/nemoclaw -- \ - /bin/sh -c "if [ -e '$HOSTILE_PROFILE_MARKER' ]; then printf PROFILE_LOADED; else printf PROFILE_NOT_LOADED; fi" -} 2>&1)" || fail "could not inspect the hostile DCode profile marker" +container_id="$( + docker ps \ + --filter "label=openshell.ai/sandbox-name=$SANDBOX_NAME" \ + --format '{{.ID}}' | head -n 1 +)" +[ -n "$container_id" ] || fail "could not resolve the DCode sandbox container" +marker_state="$( + docker exec --user 0 "$container_id" /bin/sh -c \ + "if [ -e '$HOSTILE_PROFILE_MARKER' ]; then printf PROFILE_LOADED; else printf PROFILE_NOT_LOADED; fi" +)" || fail "could not inspect the hostile DCode profile marker" cleanup_hostile_login_profile trap - EXIT From 27ceb4b6f103b74780a803dc75b8b977afe3c6fb Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 13:46:24 -0700 Subject: [PATCH 10/13] fix(dcode): protect managed login probes Signed-off-by: Apurv Kumaria --- agents/langchain-deepagents-code/Dockerfile | 13 ++- .../dcode-login-profile.sh | 20 ++++ agents/langchain-deepagents-code/start.sh | 47 ++++++++- .../manage-sandboxes/run-deep-agents-code.mdx | 17 ++++ .../sandbox/connect-inference-route-probe.ts | 17 ++-- src/lib/agent/terminal-smoke.ts | 19 ++-- test/dcode-login-profile.test.ts | 99 +++++++++++++++++++ .../04-deepagents-code-fresh-reonboard.sh | 65 ++++++------ test/langchain-deepagents-code-image.test.ts | 28 +++++- test/support/dcode-start-script-fixture.ts | 17 ++++ 10 files changed, 289 insertions(+), 53 deletions(-) create mode 100644 agents/langchain-deepagents-code/dcode-login-profile.sh create mode 100644 test/dcode-login-profile.test.ts diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index ce35f6f800e..294bfee4bd9 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -193,6 +193,7 @@ COPY agents/langchain-deepagents-code/validate-progressive-tool-disclosure.py /o COPY agents/langchain-deepagents-code/validate-observability.py /opt/nemoclaw-deepagents-code/validate-observability.py COPY agents/langchain-deepagents-code/dcode-wrapper.sh /usr/local/lib/nemoclaw/dcode-wrapper.sh COPY agents/langchain-deepagents-code/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-launcher.sh +COPY agents/langchain-deepagents-code/dcode-login-profile.sh /usr/local/lib/nemoclaw/dcode-login-profile.sh COPY agents/langchain-deepagents-code/dcode-session-supervisor.py /usr/local/lib/nemoclaw/dcode-session-supervisor.py COPY scripts/lib/entrypoint-env-wrapper.sh /usr/local/lib/nemoclaw/entrypoint-env-wrapper.sh COPY agents/langchain-deepagents-code/start.sh /usr/local/bin/nemoclaw-start @@ -217,7 +218,7 @@ RUN test -f /usr/local/bin/nemoclaw-managed-bootstrap \ && test -f /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ && test ! -L /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ && test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \ - && chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py /opt/nemoclaw-deepagents-code/validate-observability.py /usr/local/lib/nemoclaw/entrypoint-env-wrapper.sh \ + && chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py /opt/nemoclaw-deepagents-code/validate-observability.py /usr/local/lib/nemoclaw/entrypoint-env-wrapper.sh /usr/local/lib/nemoclaw/dcode-login-profile.sh \ && chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-managed-startup-hold /usr/local/bin/nemoclaw-managed-bootstrap /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-session-supervisor.py \ && test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/dcode-session-supervisor.py)" = "0:0:755" \ && install -o root -g root -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-managed-exec \ @@ -350,7 +351,13 @@ RUN mkdir -p /sandbox/.nemoclaw/blueprints/0.1.0 \ && chmod 660 /sandbox/.deepagents/config.toml USER root -RUN chown root:root /sandbox/.nemoclaw \ +RUN chown root:sandbox /sandbox \ + && chmod 1775 /sandbox \ + && install -o root -g root -m 0444 /usr/local/lib/nemoclaw/dcode-login-profile.sh /sandbox/.bash_profile \ + && test "$(stat -c '%U:%G:%a' /sandbox)" = 'root:sandbox:1775' \ + && test "$(stat -c '%U:%G:%a' /sandbox/.bash_profile)" = 'root:root:444' \ + && cmp -s /usr/local/lib/nemoclaw/dcode-login-profile.sh /sandbox/.bash_profile \ + && chown root:root /sandbox/.nemoclaw \ && chmod 1755 /sandbox/.nemoclaw \ && chown -R root:root /sandbox/.nemoclaw/blueprints \ && chmod -R 755 /sandbox/.nemoclaw/blueprints \ @@ -424,7 +431,7 @@ RUN set -eu; \ test -z "$(dpkg --audit)" # End completed-image security package verification. -ARG NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox +ARG NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root RUN case "$NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER" in \ root|sandbox) ;; \ *) echo "ERROR: NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER must be root or sandbox" >&2; exit 1 ;; \ diff --git a/agents/langchain-deepagents-code/dcode-login-profile.sh b/agents/langchain-deepagents-code/dcode-login-profile.sh new file mode 100644 index 00000000000..69f0bc53613 --- /dev/null +++ b/agents/langchain-deepagents-code/dcode-login-profile.sh @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# shellcheck shell=bash + +# OpenShell starts command-bearing sandbox sessions with `bash -lc` and sets +# HOME to the writable workspace before Bash reads its first login file. Keep +# this first-match profile root-owned so sandbox code cannot run before a +# NemoClaw-managed DCode probe. Ordinary login commands retain the established +# runtime environment; the managed launcher rebuilds that environment from +# image-owned inputs and must not source the sandbox-user-owned convenience +# file first. +unset BASH_ENV ENV +case "${BASH_EXECUTION_STRING:-}" in + *"/usr/local/lib/nemoclaw/dcode-managed-exec"*) ;; + *) + [ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh + export HOME=/sandbox + export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" + ;; +esac diff --git a/agents/langchain-deepagents-code/start.sh b/agents/langchain-deepagents-code/start.sh index 6dfac535c4f..aa4b368a97d 100755 --- a/agents/langchain-deepagents-code/start.sh +++ b/agents/langchain-deepagents-code/start.sh @@ -10,6 +10,41 @@ unset BASH_ENV ENV export HOME=/sandbox export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" +readonly NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE="/usr/local/lib/nemoclaw/dcode-login-profile.sh" + +verify_dcode_login_profile() { + [ -d /sandbox ] \ + && [ ! -L /sandbox ] \ + && [ -f "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" ] \ + && [ ! -L "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" ] \ + && [ "$(stat -c '%U:%G:%a' "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" 2>/dev/null || true)" = "root:root:444" ] \ + && [ ! -L /sandbox/.bash_profile ] \ + && [ "$(stat -c '%U:%G:%a' /sandbox 2>/dev/null || true)" = "root:sandbox:1775" ] \ + && [ "$(stat -c '%U:%G:%a' /sandbox/.bash_profile 2>/dev/null || true)" = "root:root:444" ] \ + && cmp -s "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" /sandbox/.bash_profile +} + +protect_dcode_login_profile() { + local source_metadata + source_metadata="$(stat -c '%U:%G:%a' "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" 2>/dev/null || true)" + if [ ! -f "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" ] \ + || [ -L "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" ] \ + || [ "$source_metadata" != "root:root:444" ]; then + printf '%s\n' '[SECURITY] Managed DCode login profile is missing or unsafe.' >&2 + exit 1 + fi + + chown root:sandbox /sandbox + chmod 1775 /sandbox + rm -f -- /sandbox/.bash_profile + install -o root -g root -m 0444 \ + "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" /sandbox/.bash_profile + if ! verify_dcode_login_profile; then + printf '%s\n' '[SECURITY] Could not protect the managed DCode login profile.' >&2 + exit 1 + fi +} + # managed-entrypoint-env-wrapper begin _NEMOCLAW_ENTRYPOINT_ENV_WRAPPER="/usr/local/lib/nemoclaw/entrypoint-env-wrapper.sh" if [ ! -f "$_NEMOCLAW_ENTRYPOINT_ENV_WRAPPER" ]; then @@ -34,13 +69,19 @@ unset NEMOCLAW_ENTRYPOINT_NORMALIZED_ARGC NEMOCLAW_ENTRYPOINT_NORMALIZED_ARGV \ unset -f nemoclaw_normalize_entrypoint_env_wrapper # managed-entrypoint-env-wrapper end -# The published managed image uses uid 0 as its OCI entry user so it can accept -# a future profile. Without one, drop immediately and execute the byte-for-byte -# legacy sandbox-user path. Ordinary non-managed builds still start as sandbox. +# The published managed image uses uid 0 as its OCI entry user so every start +# can repair the protected login-profile boundary before immediately dropping +# to the legacy sandbox-user path. A sandbox-user image still verifies the +# image-baked boundary before continuing. if [ "$(id -u)" -eq 0 ]; then + protect_dcode_login_profile exec /usr/bin/setpriv --reuid=sandbox --regid=sandbox --init-groups -- \ /usr/local/bin/nemoclaw-start "$@" fi +if ! verify_dcode_login_profile; then + printf '%s\n' '[SECURITY] DCode login profile is not protected; rebuild this sandbox.' >&2 + exit 1 +fi while IFS= read -r _nemoclaw_auto_approval_env; do unset "$_nemoclaw_auto_approval_env" diff --git a/docs/manage-sandboxes/run-deep-agents-code.mdx b/docs/manage-sandboxes/run-deep-agents-code.mdx index 30b4ef399e7..a3dacb21077 100644 --- a/docs/manage-sandboxes/run-deep-agents-code.mdx +++ b/docs/manage-sandboxes/run-deep-agents-code.mdx @@ -144,6 +144,23 @@ Stdio commands, extra headers, raw credentials, and unrelated top-level configur For authenticated MCP setup and credential rotation, refer to [Add an MCP Server](../mcp-servers/add-an-mcp-server) and [Manage MCP Servers](../mcp-servers/manage-mcp-servers). This isolated-mode guarantee applies to the managed launchers, not arbitrary Python commands in the sandbox. +### Protect the Managed Login Profile + +Managed Deep Agents Code images reserve `/sandbox/.bash_profile` as the first Bash login profile for OpenShell command sessions. +The file is `root:root` mode `0444`, and `/sandbox` is `root:sandbox` mode `1775`. +The sticky directory keeps normal workspace writes available while preventing the `sandbox` user from deleting or replacing the root-owned profile. + +At each container start, the root entrypoint restores and verifies the profile before it changes to the `sandbox` user. +If a sandbox-user start cannot verify the profile, it stops and tells you to rebuild the sandbox. + +For NemoClaw-managed route and terminal probes, the profile clears `BASH_ENV` and `ENV` and skips `/tmp/nemoclaw-proxy-env.sh`. +This prevents sandbox startup code from running before the managed probe. +Ordinary login commands continue to load the credential-free runtime environment, and interactive `.bashrc` behavior does not change. +Do not edit or replace `/sandbox/.bash_profile`. + +Existing Deep Agents Code sandboxes retain their previous image until you rebuild them. +After you update NemoClaw, finish active tasks and follow [Recover and Rebuild Sandboxes](recover-and-rebuild-sandboxes) to replace each image. + ## Choose an Approval Boundary Interactive shell execution and other destructive tools remain behind human-in-the-loop approval prompts by default. diff --git a/src/lib/actions/sandbox/connect-inference-route-probe.ts b/src/lib/actions/sandbox/connect-inference-route-probe.ts index 406bb985aaa..909c8806c3e 100644 --- a/src/lib/actions/sandbox/connect-inference-route-probe.ts +++ b/src/lib/actions/sandbox/connect-inference-route-probe.ts @@ -38,14 +38,15 @@ export const INFERENCE_ROUTE_PROBE_SCRIPT = [ INFERENCE_ROUTE_CA_VALIDATION, INFERENCE_ROUTE_PROBE_CORE_SCRIPT, ].join("; "); -// Invalid state: OpenShell currently starts sandbox exec through a login shell -// before the requested command, so sandbox-user startup files can emit output -// and create side effects before this probe begins (#8624; OpenShell#2668). -// NemoClaw cannot prevent that transport behavior. The image-baked launcher -// reconstructs the managed proxy from root-owned, mode-0444 files without -// adding another profile-sourcing shell, and the parser rejects inherited -// stderr or extra stdout so startup output cannot become accepted probe -// evidence. Regression: hostile-profile tests cover contaminated output and +// Invalid state: OpenShell starts sandbox exec through a login shell before the +// requested command (#8624; OpenShell#2668). Rebuilt DCode images reserve that +// shell's first-match profile as a root-owned file which skips sandbox startup +// state for the image-baked launcher. Older images can still emit output and +// create side effects before this probe begins. The launcher reconstructs the +// managed proxy from root-owned, mode-0444 files without adding another +// profile-sourcing shell, and the parser rejects inherited stderr or extra +// stdout so startup output cannot become accepted probe evidence. Regression: +// protected- and hostile-profile tests cover both image generations plus // inherited descriptors. Removal condition: use a raw probe only when OpenShell // provides both a non-login exec path and the trusted proxy environment to every // sandbox exec process. diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index 202d097c123..7121131a5c6 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -43,15 +43,16 @@ function smokeRunner(loginShell: boolean): string { * Deep Agents Code smoke commands run through the same image-baked launcher the * managed route probe uses, without adding another login shell (#8624). The * OpenShell transport still starts its own login shell before this command; see - * NVIDIA/OpenShell#2668. That transport shell can read the sandbox-user profile - * before these requested-command environment assignments apply. Using the - * image-baked root-owned NemoClaw HOME and avoiding two nested login shells - * prevents additional startup-file reads. The managed runner's single ordered - * begin/exit pair remains diagnostic rather than a trust boundary; when the - * caller preserves OpenShell's process status, a nonzero transport exit cannot - * be hidden by forged marker output. Every other terminal agent keeps the - * existing nested shells because its smoke commands rely on profile-provided - * PATH entries and retains its legacy diagnostic marker. + * NVIDIA/OpenShell#2668. Rebuilt managed DCode images reserve that shell's + * first-match profile as a root-owned file which skips sandbox startup state + * for the image-baked launcher. Older images can still read a sandbox-user + * profile before these requested-command environment assignments apply, so the + * managed runner's single ordered begin/exit pair remains diagnostic rather + * than a trust boundary. When the caller preserves OpenShell's process status, + * a nonzero transport exit cannot be hidden by forged marker output. Every + * other terminal agent keeps the existing nested shells because its smoke + * commands rely on profile-provided PATH entries and retain legacy diagnostic + * markers. */ export function buildAgentSmokeArgs( sandboxName: string, diff --git a/test/dcode-login-profile.test.ts b/test/dcode-login-profile.test.ts new file mode 100644 index 00000000000..e08376edc1b --- /dev/null +++ b/test/dcode-login-profile.test.ts @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// 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 { afterEach, describe, expect, it } from "vitest"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); +const sourcePath = path.join( + repoRoot, + "agents", + "langchain-deepagents-code", + "dcode-login-profile.sh", +); +const tempDirs: string[] = []; + +function fixture(): { + fallbackMarker: string; + hookMarker: string; + home: string; + runtimeEnv: string; +} { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-login-profile-")); + tempDirs.push(home); + const runtimeEnv = path.join(home, "runtime-env.sh"); + const hook = path.join(home, "hostile-bash-env.sh"); + const hookMarker = path.join(home, "hook-ran"); + const fallbackMarker = path.join(home, "fallback-ran"); + const source = fs + .readFileSync(sourcePath, "utf8") + .replaceAll("/tmp/nemoclaw-proxy-env.sh", runtimeEnv); + + fs.writeFileSync(path.join(home, ".bash_profile"), source, "utf8"); + fs.writeFileSync(path.join(home, ".bash_login"), `printf ran > ${fallbackMarker}\n`, "utf8"); + fs.writeFileSync(hook, `printf ran > ${hookMarker}\n`, "utf8"); + return { fallbackMarker, hookMarker, home, runtimeEnv }; +} + +describe("managed DCode login profile", () => { + afterEach(() => { + for (const directory of tempDirs.splice(0)) { + fs.rmSync(directory, { force: true, recursive: true }); + } + }); + + it("skips sandbox startup hooks before a managed exec command (#8624)", () => { + const { fallbackMarker, hookMarker, home, runtimeEnv } = fixture(); + const runtimeMarker = path.join(home, "runtime-env-ran"); + fs.writeFileSync(runtimeEnv, `printf ran > ${runtimeMarker}\n`, "utf8"); + + const result = spawnSync( + "/bin/bash", + ["-lc", ": /usr/local/lib/nemoclaw/dcode-managed-exec; printf '%s\\n' MANAGED_COMMAND_RAN"], + { + encoding: "utf8", + env: { + ...process.env, + BASH_ENV: path.join(home, "hostile-bash-env.sh"), + ENV: path.join(home, "hostile-bash-env.sh"), + HOME: home, + }, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("MANAGED_COMMAND_RAN\n"); + expect(result.stderr).toBe(""); + expect(fs.existsSync(runtimeMarker)).toBe(false); + expect(fs.existsSync(hookMarker)).toBe(false); + expect(fs.existsSync(fallbackMarker)).toBe(false); + }); + + it("preserves the managed runtime environment for ordinary login commands (#6191)", () => { + const { fallbackMarker, hookMarker, home, runtimeEnv } = fixture(); + fs.writeFileSync(runtimeEnv, "export NEMOCLAW_DCODE_LOGIN_TEST=preserved\n", "utf8"); + + const result = spawnSync( + "/bin/bash", + ["-lc", "printf '%s\\n' \"$NEMOCLAW_DCODE_LOGIN_TEST\""], + { + encoding: "utf8", + env: { + ...process.env, + BASH_ENV: path.join(home, "hostile-bash-env.sh"), + ENV: path.join(home, "hostile-bash-env.sh"), + HOME: home, + }, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("preserved\n"); + expect(result.stderr).toBe(""); + expect(fs.existsSync(hookMarker)).toBe(false); + expect(fs.existsSync(fallbackMarker)).toBe(false); + }); +}); diff --git a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh index cc9aa0ca410..19d03567ff2 100755 --- a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh +++ b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh @@ -19,7 +19,8 @@ PRIMARY_TARGET_MODEL="openai/openai/gpt-5.5" FALLBACK_TARGET_MODEL="nvidia/nvidia/nemotron-3-ultra" HOSTED_ENDPOINT="${NEMOCLAW_ENDPOINT_URL:-https://inference-api.nvidia.com/v1}" CREDENTIAL_CANARY="nemoclaw-dcode-config-get-canary" -HOSTILE_LOGIN_PROFILE="/sandbox/.bash_profile" +MANAGED_LOGIN_PROFILE="/sandbox/.bash_profile" +HOSTILE_LOGIN_FALLBACK="/sandbox/.bash_login" HOSTILE_PROFILE_MARKER="/sandbox/.nemoclaw-dcode-hostile-profile-loaded" fail() { @@ -35,7 +36,7 @@ sandbox_exec() { openshell sandbox exec --name "$SANDBOX_NAME" -- bash -c "$1" 2>&1 } -cleanup_hostile_login_profile() { +cleanup_hostile_login_fallback() { local container_id container_id="$( docker ps \ @@ -44,7 +45,7 @@ cleanup_hostile_login_profile() { )" [ -n "$container_id" ] || return 0 docker exec --user 0 "$container_id" /bin/sh -c \ - "rm -f '$HOSTILE_LOGIN_PROFILE' '$HOSTILE_PROFILE_MARKER'" \ + "rm -f '$HOSTILE_LOGIN_FALLBACK' '$HOSTILE_PROFILE_MARKER'" \ >/dev/null 2>&1 || true } @@ -209,44 +210,50 @@ model_a="${model_a#openai:}" assert_identity "$identity_before" "$model_a" "initial" pass "initial live identity reports model A" -# OpenShell starts sandbox exec through a login shell before applying the -# requested command's environment (OpenShell#2668). Plant a writable startup -# file that attacks only a DCode smoke command. It leaves a side effect, forges -# the exact ordered marker pair, and exits nonzero before the managed runner. -# The supported probe must fail closed, and direct container authority performs -# inspection and cleanup without traversing the hostile profile (#8624). -cleanup_hostile_login_profile -trap cleanup_hostile_login_profile EXIT -sandbox_exec "umask 077; printf '%s\n' 'case \"\${BASH_EXECUTION_STRING:-}\" in' ' *NEMOCLAW_AGENT_SMOKE_BEGIN*)' ' touch $HOSTILE_PROFILE_MARKER' ' printf \"%s\\n\" NEMOCLAW_AGENT_SMOKE_BEGIN NEMOCLAW_AGENT_SMOKE_EXIT:0' ' exit 97' ' ;;' 'esac' > '$HOSTILE_LOGIN_PROFILE'" \ - >/dev/null || fail "could not install the hostile DCode login profile" - -set +e -hostile_profile_connect_output="$("$CLI" "$SANDBOX_NAME" connect --probe-only 2>&1)" -hostile_profile_connect_status=$? -set -e +# OpenShell starts command-bearing sandbox sessions through a login shell and +# sets HOME to /sandbox before Bash reads its first user login file. The DCode +# image reserves that first-match file under a sticky root-owned workspace. +# Prove the sandbox identity cannot replace it, then plant the next fallback +# file with an exact forged marker pair and exit 97. Bash must keep selecting +# the managed profile, so the hostile fallback never runs and probe-only +# connect reaches the real managed smoke runner (#8624). +cleanup_hostile_login_fallback +trap cleanup_hostile_login_fallback EXIT container_id="$( docker ps \ --filter "label=openshell.ai/sandbox-name=$SANDBOX_NAME" \ --format '{{.ID}}' | head -n 1 )" [ -n "$container_id" ] || fail "could not resolve the DCode sandbox container" +managed_profile_state="$( + docker exec --user 0 "$container_id" /bin/sh -c \ + "stat -c '%U:%G:%a' /sandbox; stat -c '%U:%G:%a' '$MANAGED_LOGIN_PROFILE'; cmp -s /usr/local/lib/nemoclaw/dcode-login-profile.sh '$MANAGED_LOGIN_PROFILE' && printf '%s' MANAGED_PROFILE_MATCH" +)" || fail "could not inspect the managed DCode login profile" +expected_profile_state="$(printf '%s\n' root:sandbox:1775 root:root:444 MANAGED_PROFILE_MATCH)" +[ "$managed_profile_state" = "$expected_profile_state" ] || fail "managed DCode login profile posture is unsafe: $managed_profile_state" + +set +e +profile_overwrite_output="$(sandbox_exec "printf '%s\n' hostile > '$MANAGED_LOGIN_PROFILE'")" +profile_overwrite_status=$? +set -e +[ "$profile_overwrite_status" -ne 0 ] || fail "sandbox identity replaced the managed DCode login profile" +printf '%s\n' "$profile_overwrite_output" | grep -Eqi 'permission denied|read-only file system' \ + || fail "managed profile overwrite failed for an unexpected reason: $profile_overwrite_output" + +sandbox_exec "umask 077; printf '%s\n' 'case \"\${BASH_EXECUTION_STRING:-}\" in' ' *NEMOCLAW_AGENT_SMOKE_BEGIN*)' ' touch $HOSTILE_PROFILE_MARKER' ' printf \"%s\\n\" NEMOCLAW_AGENT_SMOKE_BEGIN NEMOCLAW_AGENT_SMOKE_EXIT:0' ' exit 97' ' ;;' 'esac' > '$HOSTILE_LOGIN_FALLBACK'" \ + >/dev/null || fail "could not install the hostile DCode fallback login profile" + +managed_profile_connect_output="$("$CLI" "$SANDBOX_NAME" connect --probe-only 2>&1)" || fail "managed profile did not protect probe-only connect: $managed_profile_connect_output" marker_state="$( docker exec --user 0 "$container_id" /bin/sh -c \ "if [ -e '$HOSTILE_PROFILE_MARKER' ]; then printf PROFILE_LOADED; else printf PROFILE_NOT_LOADED; fi" )" || fail "could not inspect the hostile DCode profile marker" -cleanup_hostile_login_profile +cleanup_hostile_login_fallback trap - EXIT -[ "$hostile_profile_connect_status" -ne 0 ] || fail "probe-only connect accepted a hostile login-profile result" -if printf '%s\n' "$hostile_profile_connect_output" | grep -Fq "terminal smoke checks passed"; then - fail "probe-only connect accepted a forged hostile-profile success marker" -fi -[ "$marker_state" = "PROFILE_LOADED" ] || fail "hostile login profile did not exercise the OpenShell transport boundary: $marker_state" -pass "hostile login-profile output fails closed at the DCode smoke boundary" - -clean_profile_connect_output="$("$CLI" "$SANDBOX_NAME" connect --probe-only 2>&1)" || fail "probe-only connect did not recover after hostile-profile cleanup: $clean_profile_connect_output" -printf '%s\n' "$clean_profile_connect_output" | grep -Fq "terminal smoke checks passed" || fail "cleaned probe-only connect did not reach the DCode smoke boundary" -pass "probe-only connect succeeds after hostile-profile cleanup" +printf '%s\n' "$managed_profile_connect_output" | grep -Fq "terminal smoke checks passed" || fail "managed profile probe did not reach the DCode smoke boundary" +[ "$marker_state" = "PROFILE_NOT_LOADED" ] || fail "hostile fallback login profile executed before the managed probe: $marker_state" +pass "root-owned DCode login profile excludes sandbox startup code from managed probes" if [ "$model_a" = "$PRIMARY_TARGET_MODEL" ]; then model_b="$FALLBACK_TARGET_MODEL" diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index ca307d0ad34..3e3e91e7c8f 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -215,7 +215,7 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(dockerfile).toContain( "chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-managed-startup-hold /usr/local/bin/nemoclaw-managed-bootstrap", ); - expect(dockerfile).toContain("ARG NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox"); + expect(dockerfile).toContain("ARG NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root"); expect(dockerfile).toContain("root|sandbox) ;; \\"); expect(dockerfile).toContain("&& command -v setpriv >/dev/null 2>&1"); expect(dockerfile.trimEnd()).toMatch( @@ -255,6 +255,32 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(baseDockerfile).toContain("> /sandbox/.profile"); }); + it("reserves the first DCode login profile under a sticky root workspace (#8624)", () => { + const dockerfile = readAgentFile("Dockerfile"); + const loginProfile = readAgentFile("dcode-login-profile.sh"); + const startScript = readAgentFile("start.sh"); + + expect(dockerfile).toContain( + "COPY agents/langchain-deepagents-code/dcode-login-profile.sh /usr/local/lib/nemoclaw/dcode-login-profile.sh", + ); + expect(dockerfile).toContain("chown root:sandbox /sandbox"); + expect(dockerfile).toContain("chmod 1775 /sandbox"); + expect(dockerfile).toContain( + "install -o root -g root -m 0444 /usr/local/lib/nemoclaw/dcode-login-profile.sh /sandbox/.bash_profile", + ); + expect(startScript).toContain("protect_dcode_login_profile"); + expect(startScript).toContain("verify_dcode_login_profile"); + expect(startScript).toContain("rm -f -- /sandbox/.bash_profile"); + expect(startScript).toContain( + "[SECURITY] DCode login profile is not protected; rebuild this sandbox.", + ); + expect(loginProfile).toContain('case "${BASH_EXECUTION_STRING:-}" in'); + expect(loginProfile).toContain('*"/usr/local/lib/nemoclaw/dcode-managed-exec"*)'); + expect(loginProfile.indexOf("unset BASH_ENV ENV")).toBeLessThan( + loginProfile.indexOf("/tmp/nemoclaw-proxy-env.sh"), + ); + }); + it("serializes the sandbox name into the shell env file for in-sandbox identity", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-start-")); try { diff --git a/test/support/dcode-start-script-fixture.ts b/test/support/dcode-start-script-fixture.ts index 3fb15c33007..a74a3910c37 100644 --- a/test/support/dcode-start-script-fixture.ts +++ b/test/support/dcode-start-script-fixture.ts @@ -94,8 +94,25 @@ export function makeStartScriptFixture( assert.ok(original.includes("local target=/tmp/nemoclaw-proxy-env.sh")); assert.ok(original.includes('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"')); assert.ok(original.includes("local marker_dir=/sandbox/.deepagents")); + const loginProfileVerification = `verify_dcode_login_profile() { + [ -d /sandbox ] \\ + && [ ! -L /sandbox ] \\ + && [ -f "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" ] \\ + && [ ! -L "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" ] \\ + && [ "$(stat -c '%U:%G:%a' "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" 2>/dev/null || true)" = "root:root:444" ] \\ + && [ ! -L /sandbox/.bash_profile ] \\ + && [ "$(stat -c '%U:%G:%a' /sandbox 2>/dev/null || true)" = "root:sandbox:1775" ] \\ + && [ "$(stat -c '%U:%G:%a' /sandbox/.bash_profile 2>/dev/null || true)" = "root:root:444" ] \\ + && cmp -s "$NEMOCLAW_DCODE_LOGIN_PROFILE_SOURCE" /sandbox/.bash_profile +}`; + assert.ok(original.includes(loginProfileVerification)); fs.mkdirSync(envDir, { recursive: true }); const envRedirected = prepareManagedProxyFixture(original, tempDir, options) + // These unit fixtures exercise post-drop entrypoint behavior on the host. + // The dedicated login-profile tests and live image acceptance cover the + // Linux root-owned file contract, which cannot be reproduced as non-root + // on every contributor platform. + .replace(loginProfileVerification, "verify_dcode_login_profile() { return 0; }") .replace("local target=/tmp/nemoclaw-proxy-env.sh", `local target="${envFile}"`) .replace( 'tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"', From 280a5366105327373984ad9eb351a433776b14d9 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 14:49:24 -0700 Subject: [PATCH 11/13] fix(agent): preserve onboarding smoke status Signed-off-by: Apurv Kumaria --- .../sandbox/terminal-connect-probe.test.ts | 7 ++-- src/lib/agent/onboard-terminal.test.ts | 36 +++++++++++++++++++ src/lib/agent/onboard.ts | 15 ++++++-- src/lib/agent/terminal-smoke.test.ts | 10 ++++++ src/lib/agent/terminal-smoke.ts | 2 +- src/lib/onboard.ts | 1 + 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/lib/actions/sandbox/terminal-connect-probe.test.ts b/src/lib/actions/sandbox/terminal-connect-probe.test.ts index 79fe96dd461..f9400ea6fe1 100644 --- a/src/lib/actions/sandbox/terminal-connect-probe.test.ts +++ b/src/lib/actions/sandbox/terminal-connect-probe.test.ts @@ -87,9 +87,10 @@ describe("terminal-agent connect inference route", () => { }); it("lets dcode continue to terminal smoke checks when its route probe is inconclusive (#6191)", () => { - const capture = vi.fn( - () => "NEMOCLAW_AGENT_SMOKE_BEGIN\ndcode 0.1.12\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n", - ); + const capture = vi.fn(() => ({ + status: 0, + output: "NEMOCLAW_AGENT_SMOKE_BEGIN\ndcode 0.1.12\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n", + })); const ensureInferenceRoute = vi.fn(() => ({ routeHealthy: null })); expect(() => diff --git a/src/lib/agent/onboard-terminal.test.ts b/src/lib/agent/onboard-terminal.test.ts index 7617dc574c5..50ea79cac25 100644 --- a/src/lib/agent/onboard-terminal.test.ts +++ b/src/lib/agent/onboard-terminal.test.ts @@ -22,10 +22,15 @@ function makeDeepAgentsCodeAgent(): AgentDefinition { function createAgentSetupContext( runCaptureOpenshell: RunCaptureOpenshell = vi.fn((_args: string[]) => ""), + captureOpenshell: NonNullable = vi.fn((args, opts) => ({ + status: 0, + output: runCaptureOpenshell(args, opts) ?? "", + })), ) { return { step: vi.fn((_current: number, _total: number, _message: string) => undefined), runCaptureOpenshell, + captureOpenshell, openshellShellCommand: vi.fn(() => "openshell sandbox connect deepagents-code"), openshellBinary: "/usr/bin/openshell", startRecordedStep: vi.fn(async (_stepName: string, _updates: Record) => { @@ -306,4 +311,35 @@ describe("Deep Agents Code terminal onboard acceptance", () => { "NEMOCLAW_AGENT_SMOKE_EXIT:42", ); }); + + it("rejects forged onboarding smoke markers when OpenShell exits nonzero (#8624)", async () => { + const calls: string[] = []; + const runCaptureOpenshell = vi.fn((args: string[]) => + recordSuccessfulDeepAgentsRuntimeCall(args, calls), + ); + const captureOpenshell = vi.fn(() => ({ + status: 97, + output: "NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:0", + })); + const context = createAgentSetupContext(runCaptureOpenshell, captureOpenshell); + + await expectSetupExit(() => + handleAgentSetup( + "deepagents-code", + "model-x", + "provider-x", + makeDeepAgentsCodeAgent(), + false, + null, + context, + ), + ); + + expect(captureOpenshell).toHaveBeenCalled(); + expect(context.recordStepComplete).not.toHaveBeenCalled(); + expect(context.recordStepFailed).toHaveBeenCalledWith( + "agent_setup", + expect.stringContaining("terminal smoke command failed: dcode --version"), + ); + }); }); diff --git a/src/lib/agent/onboard.ts b/src/lib/agent/onboard.ts index 012adc00802..44f45c37641 100644 --- a/src/lib/agent/onboard.ts +++ b/src/lib/agent/onboard.ts @@ -6,6 +6,7 @@ // NEMOCLAW_AGENT env var. The OpenClaw path never touches this module. import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; +import type { CaptureOpenshellResult } from "../adapters/openshell/client"; import { getAgentBranding } from "../cli/branding"; import type { JsonObject as LooseObject } from "../core/json-types"; import { sleepSeconds } from "../core/wait"; @@ -50,6 +51,10 @@ export interface OnboardContext { args: string[], opts?: { ignoreError?: boolean; timeout?: number }, ) => string | null; + captureOpenshell?: ( + args: string[], + opts?: { ignoreError?: boolean; timeout?: number }, + ) => CaptureOpenshellResult; openshellShellCommand: (args: string[], options?: { openshellBinary?: string }) => string; openshellBinary: string; startRecordedStep: (stepName: string, updates: LooseObject) => Promise; @@ -411,6 +416,7 @@ export async function handleAgentSetup( const { step, runCaptureOpenshell, + captureOpenshell, openshellBinary: openshellBin, startRecordedStep, recordStepComplete, @@ -428,6 +434,11 @@ export async function handleAgentSetup( cuaWithGatewayRouteMutationLock, } = ctx; + const runSmokeCapture = + agent.name === "langchain-deepagents-code" && captureOpenshell + ? captureOpenshell + : runCaptureOpenshell; + const syncNemoClawConfig = (): void => { runSandboxConfigSync(sandboxName, { getSelectionConfig: () => { @@ -452,7 +463,7 @@ export async function handleAgentSetup( ); if (binaryAvailability.available) { syncNemoClawConfig(); - const smokeResult = runAgentSmokeCommands(sandboxName, agent, runCaptureOpenshell); + const smokeResult = runAgentSmokeCommands(sandboxName, agent, runSmokeCapture); if (smokeResult.ok) { await enforceTerminalAgentVersion(sandboxName, agent, runCaptureOpenshell, { beforeFailure: () => startRecordedStep("agent_setup", { sandboxName, provider, model }), @@ -522,7 +533,7 @@ export async function handleAgentSetup( syncNemoClawConfig(); if (isTerminalAgent(agent)) { - const smokeResult = runAgentSmokeCommands(sandboxName, agent, runCaptureOpenshell); + const smokeResult = runAgentSmokeCommands(sandboxName, agent, runSmokeCapture); if (!smokeResult.ok) { await failAgentSetup( sandboxName, diff --git a/src/lib/agent/terminal-smoke.test.ts b/src/lib/agent/terminal-smoke.test.ts index 7e706345b5e..1131b7e96dd 100644 --- a/src/lib/agent/terminal-smoke.test.ts +++ b/src/lib/agent/terminal-smoke.test.ts @@ -65,6 +65,16 @@ describe("terminal agent smoke command invocation", () => { expect(result).toMatchObject({ ok: false, command: "dcode --version" }); }); + it("rejects string-only managed smoke evidence without transport status (#8624)", () => { + const result = runAgentSmokeCommands( + "probe-box", + agent("langchain-deepagents-code"), + () => "NEMOCLAW_AGENT_SMOKE_BEGIN\nNEMOCLAW_AGENT_SMOKE_EXIT:0\n", + ); + + expect(result).toMatchObject({ ok: false, command: "dcode --version" }); + }); + it("rejects extra marker evidence around the managed runner boundary (#8624)", () => { const result = runAgentSmokeCommands("probe-box", agent("langchain-deepagents-code"), () => ({ status: 0, diff --git a/src/lib/agent/terminal-smoke.ts b/src/lib/agent/terminal-smoke.ts index 7121131a5c6..4b8e5af6ada 100644 --- a/src/lib/agent/terminal-smoke.ts +++ b/src/lib/agent/terminal-smoke.ts @@ -111,7 +111,7 @@ export function runAgentSmokeCommands( const requireManagedBoundary = agent.name === "langchain-deepagents-code"; const exitCode = getSmokeExitCode(output, requireManagedBoundary); const transportFailed = - requireManagedBoundary && typeof result !== "string" && result?.status !== 0; + requireManagedBoundary && (typeof result === "string" || result?.status !== 0); if (exitCode !== 0 || transportFailed) { return { ok: false, command, output }; } diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 839516b2300..cd2a0ebec97 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -4263,6 +4263,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { agentSetupContext: () => ({ step, runCaptureOpenshell, + captureOpenshell, openshellShellCommand, openshellBinary: getOpenshellBinary(), buildSandboxConfigSyncScript, From 8556876a5fa431549fce06bb7d7c5abb6db6c923 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 12 Aug 2026 14:53:38 -0700 Subject: [PATCH 12/13] refactor(onboard): keep agent context net neutral Signed-off-by: Apurv Kumaria --- src/lib/onboard.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index cd2a0ebec97..d14f977ce00 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -4261,9 +4261,8 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { agentSetupDeps: { handleAgentSetup: agentOnboard.handleAgentSetup, agentSetupContext: () => ({ - step, - runCaptureOpenshell, - captureOpenshell, + // biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail. + ...{ step, runCaptureOpenshell, captureOpenshell }, openshellShellCommand, openshellBinary: getOpenshellBinary(), buildSandboxConfigSyncScript, From d701f60812787554500b204809d3a12e47ae6e46 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 13 Aug 2026 00:18:01 -0700 Subject: [PATCH 13/13] fix(ci): refresh libssh2 staging source Signed-off-by: Carlos Villela --- .github/workflows/managed-images.yaml | 4 ++-- test/managed-image-publication-workflow.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index fd72bb1bf2d..5322b77f188 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -111,8 +111,8 @@ jobs: CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }} # Retains the reviewed discovery-permission repair and the current # managed-image security inventory. The previous staging source pinned - # Vim 9.2.0782, which cannot satisfy the candidate's 9.2.0858 contract. - STAGING_QA_SOURCE_SHA: af2a73f0d6ce8f08a2975560f376470387c535d0 + # libssh2 nemoclaw1, which cannot satisfy the candidate's nemoclaw2 contract. + STAGING_QA_SOURCE_SHA: ce96811ddb418ad01c040521a1fe912b5bcb405e STAGING_QA_BASE_IMAGE: nemoclaw-deepagents-code-base:staging-31396519688 STAGING_QA_FINAL_IMAGE: nemoclaw-managed-pr/langchain-deepagents-code-staging-qa steps: diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 6fe551bcfbe..024b008a20a 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -725,7 +725,7 @@ describe("complete managed-image publication workflow", () => { expect(qaBuilder.permissions).toEqual({ contents: "read" }); expect(qaBuilder.env).toMatchObject({ CANDIDATE_SHA: "${{ github.event.pull_request.head.sha }}", - STAGING_QA_SOURCE_SHA: "af2a73f0d6ce8f08a2975560f376470387c535d0", + STAGING_QA_SOURCE_SHA: "ce96811ddb418ad01c040521a1fe912b5bcb405e", STAGING_QA_BASE_IMAGE: "nemoclaw-deepagents-code-base:staging-31396519688", }); expect(qaBuilder.env).not.toHaveProperty("STAGING_PRODUCER_SHA");