Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/managed-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 10 additions & 3 deletions agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
strings "$binary" | grep -Fq '/usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh'

# hadolint ignore=DL3006
FROM ${BASE_IMAGE}

Check warning on line 68 in agents/langchain-deepagents-code/Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (Deep Agents Code)

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${BASE_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

Check warning on line 68 in agents/langchain-deepagents-code/Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (Deep Agents Code)

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${BASE_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

# The supplied base may end as a non-root runtime user. Reset the build user
# explicitly before installing the root-owned managed-startup handoff.
Expand Down Expand Up @@ -193,6 +193,7 @@
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
Expand All @@ -217,7 +218,7 @@
&& 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 \
Expand Down Expand Up @@ -309,7 +310,7 @@
&& env -i /usr/local/bin/dcode.real --version \
&& env -i /usr/local/bin/deepagents-code --version

ENV HOME=/sandbox \

Check warning on line 313 in agents/langchain-deepagents-code/Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (Deep Agents Code)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DEEPAGENTS_CODE_OPENAI_API_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 313 in agents/langchain-deepagents-code/Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (Deep Agents Code)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DEEPAGENTS_CODE_OPENAI_API_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
VIRTUAL_ENV=/opt/venv \
PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" \
NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
Expand Down Expand Up @@ -350,7 +351,13 @@
&& 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 \
Expand Down Expand Up @@ -425,7 +432,7 @@
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 ;; \
Expand Down
20 changes: 20 additions & 0 deletions agents/langchain-deepagents-code/dcode-login-profile.sh
Original file line number Diff line number Diff line change
@@ -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
47 changes: 44 additions & 3 deletions agents/langchain-deepagents-code/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions docs/manage-sandboxes/run-deep-agents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/lib/actions/sandbox/connect-inference-route-probe.test.ts
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 Expand Up @@ -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 {
Expand Down
28 changes: 15 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,18 @@ 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 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.
// 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 +99,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
32 changes: 31 additions & 1 deletion src/lib/actions/sandbox/terminal-connect-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +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(() => "dcode 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(() =>
Expand All @@ -107,4 +110,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);
});
});
4 changes: 3 additions & 1 deletion src/lib/agent/onboard-terminal-fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/agent/onboard-terminal-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}
Expand Down Expand Up @@ -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";
}
36 changes: 36 additions & 0 deletions src/lib/agent/onboard-terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ function makeDeepAgentsCodeAgent(): AgentDefinition {

function createAgentSetupContext(
runCaptureOpenshell: RunCaptureOpenshell = vi.fn((_args: string[]) => ""),
captureOpenshell: NonNullable<OnboardContext["captureOpenshell"]> = 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<string, unknown>) => {
Expand Down Expand Up @@ -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"),
);
});
});
Loading
Loading