Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9188073
fix(sandbox): run Deep Agents Code smoke commands without a login shell
Dongni-Yang Aug 10, 2026
245da7c
test(sandbox): read the smoke command from the end of the stub argv
Dongni-Yang Aug 10, 2026
deeb253
Merge branch 'main' into fix/8624-dcode-probe-login-profile
apurvvkumaria Aug 10, 2026
b08ad2d
Merge branch 'main' into fix/8624-dcode-probe-login-profile
apurvvkumaria Aug 10, 2026
9410fe0
docs(sandbox): clarify nested login shell boundary
apurvvkumaria Aug 10, 2026
c93f3a5
Merge branch 'main' into fix/8624-dcode-probe-login-profile
cv Aug 11, 2026
89fe87a
Merge branch 'main' into fix/8624-dcode-probe-login-profile
cv Aug 11, 2026
a19233f
Merge branch 'main' into fix/8624-dcode-probe-login-profile
cv Aug 11, 2026
d5039af
fix(agent): reject forged smoke exit markers
prekshivyas Aug 11, 2026
3656529
Merge branch 'main' into fix/8624-dcode-probe-login-profile
cv Aug 11, 2026
7540048
fix(agent): narrow smoke hardening scope
apurvvkumaria Aug 12, 2026
82bd827
Merge branch 'main' into fix/8624-dcode-probe-login-profile
cv Aug 12, 2026
b63f6e2
fix(agent): bind DCode smoke evidence to managed launch
apurvvkumaria Aug 12, 2026
d46333e
merge(main): synchronize DCode smoke hardening
apurvvkumaria Aug 12, 2026
96966bb
merge: refresh PR branch from main
apurvvkumaria Aug 12, 2026
402e9b6
merge: include latest main update
apurvvkumaria Aug 12, 2026
6926d02
test(dcode): cover hostile login profile boundary
apurvvkumaria Aug 12, 2026
3136c8e
Merge branch 'main' into fix/8624-dcode-probe-login-profile
cv Aug 12, 2026
c00b0f0
test(dcode): verify hostile profile fails closed
apurvvkumaria Aug 12, 2026
a34bbd3
fix(dcode): honor smoke transport failures
apurvvkumaria Aug 12, 2026
5e1f01a
Merge branch 'main' into fix/8624-dcode-probe-login-profile
apurvvkumaria Aug 12, 2026
27ceb4b
fix(dcode): protect managed login probes
apurvvkumaria Aug 12, 2026
280a536
fix(agent): preserve onboarding smoke status
apurvvkumaria Aug 12, 2026
8556876
refactor(onboard): keep agent context net neutral
apurvvkumaria Aug 12, 2026
5e6bedb
merge(main): refresh PR 8670 for current CI fixes
cv Aug 13, 2026
d701f60
fix(ci): refresh libssh2 staging source
cv Aug 13, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
Expand Down
27 changes: 14 additions & 13 deletions src/lib/actions/sandbox/connect-inference-route-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand Down
54 changes: 54 additions & 0 deletions src/lib/agent/terminal-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -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 adding 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("does not add a login shell to Deep Agents Code smoke exec (#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");
});
});
73 changes: 57 additions & 16 deletions src/lib/agent/terminal-smoke.ts
Original file line number Diff line number Diff line change
@@ -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 = (
Expand All @@ -20,6 +21,59 @@ 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 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,
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,
Expand All @@ -28,23 +82,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) {
Expand Down
4 changes: 3 additions & 1 deletion test/cli/connect-terminal-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;;',
Expand Down
Loading