From cf2761beedc58456f85dff7e799d7f45616681b0 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Thu, 2 Jul 2026 15:35:30 +0000 Subject: [PATCH 01/14] feat(deepagents-code): add dcode status and allow OpenShell TLS key in secret guard Signed-off-by: Tinson Lai --- .../dcode-wrapper.sh | 57 +++++- agents/langchain-deepagents-code/start.sh | 1 + .../quickstart-langchain-deepagents-code.mdx | 10 + src/lib/onboard.ts | 1 + src/lib/onboard/sandbox-create-launch.test.ts | 35 ++++ src/lib/onboard/sandbox-create-launch.ts | 5 + test/dcode-wrapper-identity.test.ts | 172 ++++++++++++++++++ test/langchain-deepagents-code-image.test.ts | 19 ++ 8 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 test/dcode-wrapper-identity.test.ts diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 7cf928bf94d..a383e44f82e 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -14,6 +14,7 @@ export DEEPAGENTS_CODE_OPENAI_API_KEY="${DEEPAGENTS_CODE_OPENAI_API_KEY:-nemocla export OPENAI_BASE_URL="${OPENAI_BASE_URL:-https://inference.local/v1}" readonly DEEPAGENTS_ENV_FILE="/sandbox/.deepagents/.env" +readonly DEEPAGENTS_CONFIG_FILE="/sandbox/.deepagents/config.toml" run_dcode() { exec python3 -m deepagents_code "$@" @@ -194,6 +195,22 @@ has_credential_name_context() { return 1 } +# SECURITY: OpenShell injects transport-layer infrastructure variables (such as +# the sandbox's local TLS listener key) into the runtime environment. Their names +# end in a credential keyword, so the name-context heuristic above would refuse to +# start on them even though they are not user provider secrets. Exempt these exact +# names from the name-context rejection ONLY; is_secret_shaped_value still runs on +# every value, so a real provider token carried under one of these names is still +# rejected. Keep this list narrow and exact — never a prefix wildcard. +is_openshell_infra_key_name() { + case "$1" in + OPENSHELL_TLS_KEY) + return 0 + ;; + esac + return 1 +} + is_dynamic_dotenv_value() { local value="$1" case "$value" in @@ -232,7 +249,7 @@ assert_no_secret_runtime_env() { if is_secret_shaped_value "$value"; then refuse_secret_env "runtime environment variable" "$name" fi - if has_credential_name_context "$name" && [ ${#value} -ge 10 ]; then + if has_credential_name_context "$name" && [ ${#value} -ge 10 ] && ! is_openshell_infra_key_name "$name"; then refuse_secret_env "runtime environment variable" "$name" fi done < <(env -0) @@ -282,7 +299,7 @@ assert_no_secret_env_file() { if is_secret_shaped_value "$value"; then refuse_secret_env "$env_file" "$key" fi - if has_credential_name_context "$key" && [ ${#value} -ge 10 ]; then + if has_credential_name_context "$key" && [ ${#value} -ge 10 ] && ! is_openshell_infra_key_name "$key"; then refuse_secret_env "$env_file" "$key" fi done @@ -291,7 +308,43 @@ assert_no_secret_env_file() { assert_no_secret_runtime_env assert_no_secret_env_file +toml_scalar() { + local key="$1" line + [ -r "$DEEPAGENTS_CONFIG_FILE" ] || return 0 + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + "$key = \""*) + line="${line#"$key = \""}" + printf '%s' "${line%\"}" + return 0 + ;; + esac + done <"$DEEPAGENTS_CONFIG_FILE" + return 0 +} + +print_identity() { + local sandbox_name model endpoint + sandbox_name="${NEMOCLAW_SANDBOX_NAME:-unknown}" + model="$(toml_scalar default)" + endpoint="$(toml_scalar base_url)" + [ -n "$endpoint" ] || endpoint="${OPENAI_BASE_URL:-}" + printf 'Sandbox: %s\n' "$sandbox_name" + printf 'Agent: %s\n' 'langchain-deepagents-code' + if [ -n "$model" ]; then + printf 'Model: %s\n' "$model" + fi + if [ -n "$endpoint" ]; then + printf 'Endpoint: %s\n' "$endpoint" + fi + printf 'Runtime: %s\n' 'Deep Agents Code (terminal)' +} + case "${1:-}" in + status | whoami | identity) + print_identity + exit 0 + ;; --version | -v | -V | --help | -h) run_dcode "$@" ;; diff --git a/agents/langchain-deepagents-code/start.sh b/agents/langchain-deepagents-code/start.sh index 320df4abb53..36975d2c5d1 100755 --- a/agents/langchain-deepagents-code/start.sh +++ b/agents/langchain-deepagents-code/start.sh @@ -74,6 +74,7 @@ prepare_runtime_env() { write_export_if_set LANGSMITH_TRACING write_export_if_set LANGSMITH_PROJECT write_export_if_set DEEPAGENTS_CODE_LANGSMITH_PROJECT + write_export_if_set NEMOCLAW_SANDBOX_NAME } >"$tmp" chmod 400 "$tmp" mv -f "$tmp" "$target" diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index f35ea12b60a..cd14a12c2bf 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -63,6 +63,16 @@ dcode -n "Summarize this repository" The managed wrapper launches Deep Agents Code with `HOME=/sandbox`, update checks disabled, remote Deep Agents sandbox providers disabled, MCP auto-loading disabled, and shell allow-list overrides blocked. +To confirm which sandbox a session is in, run the identity command: + +```bash +dcode status +``` + +It prints the sandbox name, agent, configured model, and inference endpoint, then exits without starting the interactive UI. +`dcode whoami` and `dcode identity` are aliases. +The sandbox name resolves when you run the command from a `nemoclaw connect` shell, which loads the NemoClaw runtime environment. + ## Python Environment Deep Agents Code runs from a NemoClaw-managed Python virtual environment at `/opt/venv`. diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index a4b9f265cc3..d6000508a1c 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3031,6 +3031,7 @@ async function createSandbox( hermesDashboardState, manageDashboard, openshellShellCommand, + sandboxName, }); const dockerGpuCreatePatch = dockerGpuSandboxCreate.createDockerGpuSandboxCreatePatch({ enabled: useDockerGpuPatch, diff --git a/src/lib/onboard/sandbox-create-launch.test.ts b/src/lib/onboard/sandbox-create-launch.test.ts index 850ca7be0fb..580fa9cdb74 100644 --- a/src/lib/onboard/sandbox-create-launch.test.ts +++ b/src/lib/onboard/sandbox-create-launch.test.ts @@ -223,4 +223,39 @@ describe("prepareSandboxCreateLaunch", () => { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); + + it("forwards the sandbox name into the Deep Agents Code sandbox create env", () => { + const result = prepareSandboxCreateLaunch({ + agent: { name: "langchain-deepagents-code" } as any, + chatUiUrl: "", + createArgs: ["--name", "dcode-demo"], + env: {}, + extraPlaceholderKeys: [], + getDashboardForwardPort: vi.fn(() => "0"), + hermesDashboardState: disabledHermesDashboardState, + manageDashboard: false, + openshellShellCommand: (args) => args.join(" "), + sandboxName: "dcode-demo", + buildEnv: () => ({}), + }); + + expect(result.envArgs).toContain("NEMOCLAW_SANDBOX_NAME=dcode-demo"); + }); + + it("does not forward the sandbox name for non-Deep-Agents-Code agents", () => { + const result = prepareSandboxCreateLaunch({ + agent: { name: "openclaw", configPaths: { dir: "/sandbox/.custom-openclaw" } } as any, + chatUiUrl: "http://127.0.0.1:19000/", + createArgs: ["--name", "demo"], + env: {}, + extraPlaceholderKeys: [], + getDashboardForwardPort: () => "19000", + hermesDashboardState: disabledHermesDashboardState, + openshellShellCommand: (args) => args.join(" "), + sandboxName: "demo", + buildEnv: () => ({}), + }); + + expect(result.envArgs.some((arg) => arg.startsWith("NEMOCLAW_SANDBOX_NAME="))).toBe(false); + }); }); diff --git a/src/lib/onboard/sandbox-create-launch.ts b/src/lib/onboard/sandbox-create-launch.ts index 27cca4bbdd7..98a7ae56e63 100644 --- a/src/lib/onboard/sandbox-create-launch.ts +++ b/src/lib/onboard/sandbox-create-launch.ts @@ -23,6 +23,7 @@ export interface SandboxCreateLaunchInput { hermesDashboardState: HermesDashboardOnboardState; manageDashboard?: boolean; openshellShellCommand: OpenshellShellCommand; + sandboxName?: string; buildEnv?(): Record; } @@ -77,6 +78,10 @@ export function prepareSandboxCreateLaunch(input: SandboxCreateLaunchInput): San envArgs.push(formatEnvAssignment("NEMOCLAW_PROXY_PORT", sandboxProxyPort)); } + if (input.agent?.name === "langchain-deepagents-code" && input.sandboxName) { + envArgs.push(formatEnvAssignment("NEMOCLAW_SANDBOX_NAME", input.sandboxName)); + } + appendExtraPlaceholderKeysEnvArg(envArgs, input.extraPlaceholderKeys, formatEnvAssignment); const sandboxEnv = (input.buildEnv ?? buildSubprocessEnv)(); diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts new file mode 100644 index 00000000000..465b730ea0d --- /dev/null +++ b/test/dcode-wrapper-identity.test.ts @@ -0,0 +1,172 @@ +// 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 { describe, expect, it } from "vitest"; + +const WRAPPER = path.join( + import.meta.dirname, + "..", + "agents", + "langchain-deepagents-code", + "dcode-wrapper.sh", +); + +const canRun = process.platform === "linux"; + +const SAMPLE_CONFIG = [ + "# Generated by NemoClaw. This file contains no provider secrets.", + "", + "[models]", + 'default = "openai:demo-model"', + "", + "[models.providers.openai]", + 'models = ["demo-model"]', + 'base_url = "https://inference.local/v1"', + "enabled = true", + "", +].join("\n"); + +const OPAQUE = "Zx3Qw9Lp7Rt2Vn5Bd8Kf1Mh6Cg4Js0Ay"; + +type Fixture = { wrapperPath: string; ranMarker: string }; + +function buildFixture(tempDir: string, configContent: string): Fixture { + const wrapperPath = path.join(tempDir, "dcode"); + const ranMarker = path.join(tempDir, "dcode-ran"); + const envFile = path.join(tempDir, ".env"); + const configFile = path.join(tempDir, "config.toml"); + const fixture = fs + .readFileSync(WRAPPER, "utf8") + .replace( + 'readonly DEEPAGENTS_ENV_FILE="/sandbox/.deepagents/.env"', + `readonly DEEPAGENTS_ENV_FILE="${envFile}"`, + ) + .replace( + 'readonly DEEPAGENTS_CONFIG_FILE="/sandbox/.deepagents/config.toml"', + `readonly DEEPAGENTS_CONFIG_FILE="${configFile}"`, + ) + .replace( + "exec python3 -m deepagents_code", + `touch "${ranMarker}"; echo dcode-stub-ran; exit 0; : python3 -m deepagents_code`, + ); + fs.writeFileSync(envFile, "", "utf8"); + fs.writeFileSync(configFile, configContent, "utf8"); + fs.writeFileSync(wrapperPath, fixture, "utf8"); + fs.chmodSync(wrapperPath, 0o755); + return { wrapperPath, ranMarker }; +} + +type Run = { status: number | null; stdout: string; stderr: string; launched: boolean }; + +function runWrapper(fixture: Fixture, args: readonly string[], env: NodeJS.ProcessEnv): Run { + const result = spawnSync("bash", [fixture.wrapperPath, ...args], { + env: { + PATH: process.env.PATH ?? "/usr/bin:/bin", + HOME: path.dirname(fixture.wrapperPath), + ...env, + }, + encoding: "utf8", + timeout: 10000, + }); + return { + status: result.status, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + launched: fs.existsSync(fixture.ranMarker), + }; +} + +function withTempDir(run: (dir: string) => void): void { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-identity-")); + try { + run(dir); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +describe.skipIf(!canRun)( + "agents/langchain-deepagents-code/dcode-wrapper.sh identity command", + () => { + for (const sub of ["status", "whoami", "identity"]) { + it(`'${sub}' reports the sandbox identity and does not launch dcode`, () => { + withTempDir((dir) => { + const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [sub], { + NEMOCLAW_SANDBOX_NAME: "dcode-demo", + }); + + expect(run.status).toBe(0); + expect(run.launched).toBe(false); + expect(run.stdout).toContain("Sandbox: dcode-demo"); + expect(run.stdout).toContain("Agent: langchain-deepagents-code"); + expect(run.stdout).toContain("Model: openai:demo-model"); + expect(run.stdout).toContain("Endpoint: https://inference.local/v1"); + expect(run.stdout).toContain("Runtime: Deep Agents Code (terminal)"); + }); + }); + } + + it("reports the sandbox as unknown when the name was not injected", () => { + withTempDir((dir) => { + const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), ["status"], {}); + + expect(run.status).toBe(0); + expect(run.launched).toBe(false); + expect(run.stdout).toContain("Sandbox: unknown"); + }); + }); + + it("still launches dcode for a normal interactive invocation", () => { + withTempDir((dir) => { + const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { + NEMOCLAW_SANDBOX_NAME: "dcode-demo", + }); + + expect(run.status).toBe(0); + expect(run.launched).toBe(true); + }); + }); + }, +); + +describe.skipIf(!canRun)( + "agents/langchain-deepagents-code/dcode-wrapper.sh OpenShell infra-key allowlist", + () => { + it("starts dcode when OPENSHELL_TLS_KEY carries an opaque infrastructure value", () => { + withTempDir((dir) => { + const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { OPENSHELL_TLS_KEY: OPAQUE }); + + expect(run.status).toBe(0); + expect(run.launched).toBe(true); + expect(run.stderr).not.toContain("refusing to start"); + }); + }); + + it("still refuses when OPENSHELL_TLS_KEY carries a real provider token", () => { + withTempDir((dir) => { + const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { + OPENSHELL_TLS_KEY: `nvapi-${OPAQUE}`, + }); + + expect(run.status).toBe(2); + expect(run.launched).toBe(false); + expect(run.stderr).toContain("OPENSHELL_TLS_KEY"); + }); + }); + + it("still refuses an opaque credential-name-context variable outside the allowlist", () => { + withTempDir((dir) => { + const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { CUSTOM_API_KEY: OPAQUE }); + + expect(run.status).toBe(2); + expect(run.launched).toBe(false); + expect(run.stderr).toContain("CUSTOM_API_KEY"); + }); + }); + }, +); diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 1edfb5f4d09..6d30b4f36c7 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -211,6 +211,25 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(envFileText).toContain("export https_proxy=https://safe-proxy.example:8443"); }); + 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 { + const { envFile, scriptPath } = makeStartScriptFixture(tempDir); + + execFileSync("bash", [scriptPath, "sh", "-c", ":"], { + env: { + PATH: process.env.PATH ?? "/usr/bin:/bin", + NEMOCLAW_SANDBOX_NAME: "dcode-demo", + }, + encoding: "utf8", + }); + + expect(fs.readFileSync(envFile, "utf8")).toContain("export NEMOCLAW_SANDBOX_NAME=dcode-demo"); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("omits and unsets credential-bearing proxy URLs", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-start-")); const { envFile, scriptPath } = makeStartScriptFixture(tempDir); From 85588c9e6fb9bc7ba005c90015597e0b46fc80fb Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Thu, 2 Jul 2026 16:50:56 +0000 Subject: [PATCH 02/14] fix(onboard): derive dcode sandbox name from create args Read the sandbox name back out of the --name flag already present in createArgs instead of threading a new sandboxName field through prepareSandboxCreateLaunch. Keeps src/lib/onboard.ts at net-zero line growth, which codebase-growth-guardrails enforces for that file. Signed-off-by: Tinson Lai --- src/lib/onboard.ts | 1 - src/lib/onboard/sandbox-create-launch.test.ts | 2 -- src/lib/onboard/sandbox-create-launch.ts | 13 ++++++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index d6000508a1c..a4b9f265cc3 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3031,7 +3031,6 @@ async function createSandbox( hermesDashboardState, manageDashboard, openshellShellCommand, - sandboxName, }); const dockerGpuCreatePatch = dockerGpuSandboxCreate.createDockerGpuSandboxCreatePatch({ enabled: useDockerGpuPatch, diff --git a/src/lib/onboard/sandbox-create-launch.test.ts b/src/lib/onboard/sandbox-create-launch.test.ts index 580fa9cdb74..006e43ed7c0 100644 --- a/src/lib/onboard/sandbox-create-launch.test.ts +++ b/src/lib/onboard/sandbox-create-launch.test.ts @@ -235,7 +235,6 @@ describe("prepareSandboxCreateLaunch", () => { hermesDashboardState: disabledHermesDashboardState, manageDashboard: false, openshellShellCommand: (args) => args.join(" "), - sandboxName: "dcode-demo", buildEnv: () => ({}), }); @@ -252,7 +251,6 @@ describe("prepareSandboxCreateLaunch", () => { getDashboardForwardPort: () => "19000", hermesDashboardState: disabledHermesDashboardState, openshellShellCommand: (args) => args.join(" "), - sandboxName: "demo", buildEnv: () => ({}), }); diff --git a/src/lib/onboard/sandbox-create-launch.ts b/src/lib/onboard/sandbox-create-launch.ts index 98a7ae56e63..a2513dc0d31 100644 --- a/src/lib/onboard/sandbox-create-launch.ts +++ b/src/lib/onboard/sandbox-create-launch.ts @@ -23,7 +23,6 @@ export interface SandboxCreateLaunchInput { hermesDashboardState: HermesDashboardOnboardState; manageDashboard?: boolean; openshellShellCommand: OpenshellShellCommand; - sandboxName?: string; buildEnv?(): Record; } @@ -35,6 +34,11 @@ export interface SandboxCreateLaunch { sandboxStartupCommand: string[]; } +function readCreateArgValue(createArgs: readonly string[], flag: string): string | undefined { + const flagIndex = createArgs.indexOf(flag); + return flagIndex === -1 ? undefined : createArgs[flagIndex + 1]; +} + export function prepareSandboxCreateLaunch(input: SandboxCreateLaunchInput): SandboxCreateLaunch { const env = input.env ?? process.env; const manageDashboard = input.manageDashboard ?? true; @@ -78,8 +82,11 @@ export function prepareSandboxCreateLaunch(input: SandboxCreateLaunchInput): San envArgs.push(formatEnvAssignment("NEMOCLAW_PROXY_PORT", sandboxProxyPort)); } - if (input.agent?.name === "langchain-deepagents-code" && input.sandboxName) { - envArgs.push(formatEnvAssignment("NEMOCLAW_SANDBOX_NAME", input.sandboxName)); + if (input.agent?.name === "langchain-deepagents-code") { + const sandboxName = readCreateArgValue(input.createArgs, "--name"); + if (sandboxName) { + envArgs.push(formatEnvAssignment("NEMOCLAW_SANDBOX_NAME", sandboxName)); + } } appendExtraPlaceholderKeysEnvArg(envArgs, input.extraPlaceholderKeys, formatEnvAssignment); From 96aa3c44d00bf3da1e9b63deb03bfbeda7584475 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Thu, 2 Jul 2026 13:58:36 -0700 Subject: [PATCH 03/14] fix(deepagents-code): add Provider field to dcode status and tighten env-file secret guard PRA-4: print_identity now reads the provider route from the NemoClaw config comment and emits a Provider: line, satisfying #6186's accepted shape for dcode status. PRA-3: remove is_openshell_infra_key_name exemption from assert_no_secret_env_file. OPENSHELL_TLS_KEY is injected by OpenShell at runtime, not user-set in .deepagents/.env; the exemption only belongs in assert_no_secret_runtime_env. Co-Authored-By: Claude Sonnet 4.6 --- .../dcode-wrapper.sh | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index a383e44f82e..219e8b41c1f 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -299,7 +299,7 @@ assert_no_secret_env_file() { if is_secret_shaped_value "$value"; then refuse_secret_env "$env_file" "$key" fi - if has_credential_name_context "$key" && [ ${#value} -ge 10 ] && ! is_openshell_infra_key_name "$key"; then + if has_credential_name_context "$key" && [ ${#value} -ge 10 ]; then refuse_secret_env "$env_file" "$key" fi done @@ -323,14 +323,33 @@ toml_scalar() { return 0 } +toml_provider_route() { + local line + [ -r "$DEEPAGENTS_CONFIG_FILE" ] || return 0 + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + "# NemoClaw provider route: "*) + line="${line#"# NemoClaw provider route: "}" + printf '%s' "${line%%;*}" + return 0 + ;; + esac + done <"$DEEPAGENTS_CONFIG_FILE" + return 0 +} + print_identity() { - local sandbox_name model endpoint + local sandbox_name model endpoint provider sandbox_name="${NEMOCLAW_SANDBOX_NAME:-unknown}" model="$(toml_scalar default)" endpoint="$(toml_scalar base_url)" + provider="$(toml_provider_route)" [ -n "$endpoint" ] || endpoint="${OPENAI_BASE_URL:-}" printf 'Sandbox: %s\n' "$sandbox_name" printf 'Agent: %s\n' 'langchain-deepagents-code' + if [ -n "$provider" ]; then + printf 'Provider: %s\n' "$provider" + fi if [ -n "$model" ]; then printf 'Model: %s\n' "$model" fi From 313ccfc2442f9a1d969a4c8bc7d289af0f19af6b Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Thu, 2 Jul 2026 14:11:39 -0700 Subject: [PATCH 04/14] fix(deepagents-code): add tvly- prefix to secret-shape detection patterns Travily API tokens use the tvly- prefix; add it to both has_non_slack_secret_shape() and is_secret_shaped_value() so they are caught by the runtime and .env secret guards. Co-Authored-By: Claude Sonnet 4.6 --- agents/langchain-deepagents-code/dcode-wrapper.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 219e8b41c1f..968074b7d74 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -70,7 +70,7 @@ has_non_slack_secret_shape() { if [[ "$value" =~ sk-[A-Za-z0-9_-]{20,} ]]; then return 0 fi - if [[ "$value" =~ (nvapi-|nvcf-|ghp_|hf_|glpat-|gsk_|pypi-)[A-Za-z0-9_-]{10,} ]]; then + if [[ "$value" =~ (nvapi-|nvcf-|ghp_|hf_|glpat-|gsk_|pypi-|tvly-)[A-Za-z0-9_-]{10,} ]]; then return 0 fi if [[ "$value" =~ github_pat_[A-Za-z0-9_]{30,} ]]; then @@ -152,7 +152,7 @@ is_secret_shaped_value() { if [[ "$value" =~ sk-[A-Za-z0-9_-]{20,} ]]; then return 0 fi - if [[ "$value" =~ (nvapi-|nvcf-|ghp_|hf_|glpat-|gsk_|pypi-)[A-Za-z0-9_-]{10,} ]]; then + if [[ "$value" =~ (nvapi-|nvcf-|ghp_|hf_|glpat-|gsk_|pypi-|tvly-)[A-Za-z0-9_-]{10,} ]]; then return 0 fi if [[ "$value" =~ github_pat_[A-Za-z0-9_]{30,} ]]; then From a60c2d8bb85a8581493e33f4a4528b5ab805bd45 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 15:17:57 -0700 Subject: [PATCH 05/14] fix(deepagents-code): narrow OpenShell TLS key allowance Limit the runtime exception to OpenShell's canonical mounted key path, keep mutable .env files fail-closed, and cover Tavily token parity. Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../dcode-wrapper.sh | 25 +++--- test/dcode-wrapper-identity.test.ts | 76 +++++++++++++++---- test/langchain-deepagents-code-image.test.ts | 1 + 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 968074b7d74..126754776d6 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -15,6 +15,7 @@ export OPENAI_BASE_URL="${OPENAI_BASE_URL:-https://inference.local/v1}" readonly DEEPAGENTS_ENV_FILE="/sandbox/.deepagents/.env" readonly DEEPAGENTS_CONFIG_FILE="/sandbox/.deepagents/config.toml" +readonly OPENSHELL_TLS_KEY_PATH="/etc/openshell/tls/client/tls.key" run_dcode() { exec python3 -m deepagents_code "$@" @@ -195,20 +196,14 @@ has_credential_name_context() { return 1 } -# SECURITY: OpenShell injects transport-layer infrastructure variables (such as -# the sandbox's local TLS listener key) into the runtime environment. Their names -# end in a credential keyword, so the name-context heuristic above would refuse to -# start on them even though they are not user provider secrets. Exempt these exact -# names from the name-context rejection ONLY; is_secret_shaped_value still runs on -# every value, so a real provider token carried under one of these names is still -# rejected. Keep this list narrow and exact — never a prefix wildcard. -is_openshell_infra_key_name() { - case "$1" in - OPENSHELL_TLS_KEY) - return 0 - ;; - esac - return 1 +# SECURITY: OpenShell's supervisor injects this mounted TLS key path into the +# runtime environment. Allow only the exact name/value pair after the generic +# value scan. Never allow the name alone, and never apply this exception to the +# mutable Deep Agents Code .env file. +is_allowed_openshell_runtime_value() { + local name="$1" + local value="$2" + [ "$name" = "OPENSHELL_TLS_KEY" ] && [ "$value" = "$OPENSHELL_TLS_KEY_PATH" ] } is_dynamic_dotenv_value() { @@ -249,7 +244,7 @@ assert_no_secret_runtime_env() { if is_secret_shaped_value "$value"; then refuse_secret_env "runtime environment variable" "$name" fi - if has_credential_name_context "$name" && [ ${#value} -ge 10 ] && ! is_openshell_infra_key_name "$name"; then + if has_credential_name_context "$name" && [ ${#value} -ge 10 ] && ! is_allowed_openshell_runtime_value "$name" "$value"; then refuse_secret_env "runtime environment variable" "$name" fi done < <(env -0) diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index 465b730ea0d..07dacbb6211 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -32,8 +32,9 @@ const SAMPLE_CONFIG = [ ].join("\n"); const OPAQUE = "Zx3Qw9Lp7Rt2Vn5Bd8Kf1Mh6Cg4Js0Ay"; +const CANONICAL_TLS_KEY_PATH = "/etc/openshell/tls/client/tls.key"; -type Fixture = { wrapperPath: string; ranMarker: string }; +type Fixture = { wrapperPath: string; ranMarker: string; envFile: string }; function buildFixture(tempDir: string, configContent: string): Fixture { const wrapperPath = path.join(tempDir, "dcode"); @@ -58,12 +59,12 @@ function buildFixture(tempDir: string, configContent: string): Fixture { fs.writeFileSync(configFile, configContent, "utf8"); fs.writeFileSync(wrapperPath, fixture, "utf8"); fs.chmodSync(wrapperPath, 0o755); - return { wrapperPath, ranMarker }; + return { wrapperPath, ranMarker, envFile }; } type Run = { status: number | null; stdout: string; stderr: string; launched: boolean }; -function runWrapper(fixture: Fixture, args: readonly string[], env: NodeJS.ProcessEnv): Run { +function runBashWrapper(fixture: Fixture, args: readonly string[], env: NodeJS.ProcessEnv): Run { const result = spawnSync("bash", [fixture.wrapperPath, ...args], { env: { PATH: process.env.PATH ?? "/usr/bin:/bin", @@ -96,7 +97,7 @@ describe.skipIf(!canRun)( for (const sub of ["status", "whoami", "identity"]) { it(`'${sub}' reports the sandbox identity and does not launch dcode`, () => { withTempDir((dir) => { - const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [sub], { + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), [sub], { NEMOCLAW_SANDBOX_NAME: "dcode-demo", }); @@ -113,7 +114,7 @@ describe.skipIf(!canRun)( it("reports the sandbox as unknown when the name was not injected", () => { withTempDir((dir) => { - const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), ["status"], {}); + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["status"], {}); expect(run.status).toBe(0); expect(run.launched).toBe(false); @@ -123,7 +124,7 @@ describe.skipIf(!canRun)( it("still launches dcode for a normal interactive invocation", () => { withTempDir((dir) => { - const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { NEMOCLAW_SANDBOX_NAME: "dcode-demo", }); @@ -137,9 +138,11 @@ describe.skipIf(!canRun)( describe.skipIf(!canRun)( "agents/langchain-deepagents-code/dcode-wrapper.sh OpenShell infra-key allowlist", () => { - it("starts dcode when OPENSHELL_TLS_KEY carries an opaque infrastructure value", () => { + it("starts dcode when runtime OPENSHELL_TLS_KEY carries the canonical mounted path", () => { withTempDir((dir) => { - const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { OPENSHELL_TLS_KEY: OPAQUE }); + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["--version"], { + OPENSHELL_TLS_KEY: CANONICAL_TLS_KEY_PATH, + }); expect(run.status).toBe(0); expect(run.launched).toBe(true); @@ -147,21 +150,68 @@ describe.skipIf(!canRun)( }); }); - it("still refuses when OPENSHELL_TLS_KEY carries a real provider token", () => { - withTempDir((dir) => { - const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { - OPENSHELL_TLS_KEY: `nvapi-${OPAQUE}`, + it("refuses noncanonical OpenShell TLS key values without printing them", () => { + const pemValue = [ + "-----BEGIN PRIVATE ", + "KEY-----\nraw-private-key\n-----END PRIVATE ", + "KEY-----", + ].join(""); + for (const value of [ + OPAQUE, + pemValue, + "relative/tls.key", + "/tmp/tls.key", + `${CANONICAL_TLS_KEY_PATH}.bak`, + `tvly-${OPAQUE}`, + ]) { + withTempDir((dir) => { + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["--version"], { + OPENSHELL_TLS_KEY: value, + }); + + expect(run.status).toBe(2); + expect(run.launched).toBe(false); + expect(run.stderr).toContain("OPENSHELL_TLS_KEY"); + expect(run.stderr).not.toContain(value); }); + } + }); + + it("refuses the canonical OpenShell TLS key path in the mutable env file", () => { + withTempDir((dir) => { + const fixture = buildFixture(dir, SAMPLE_CONFIG); + fs.writeFileSync(fixture.envFile, `OPENSHELL_TLS_KEY=${CANONICAL_TLS_KEY_PATH}\n`, "utf8"); + + const run = runBashWrapper(fixture, ["--version"], {}); expect(run.status).toBe(2); expect(run.launched).toBe(false); expect(run.stderr).toContain("OPENSHELL_TLS_KEY"); + expect(run.stderr).toContain(path.join(dir, ".env")); + expect(run.stderr).not.toContain(CANONICAL_TLS_KEY_PATH); }); }); + it("still refuses recognized provider tokens carried by OPENSHELL_TLS_KEY", () => { + for (const value of [`nvapi-${OPAQUE}`, `tvly-${OPAQUE}`]) { + withTempDir((dir) => { + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["--version"], { + OPENSHELL_TLS_KEY: value, + }); + + expect(run.status).toBe(2); + expect(run.launched).toBe(false); + expect(run.stderr).toContain("OPENSHELL_TLS_KEY"); + expect(run.stderr).not.toContain(value); + }); + } + }); + it("still refuses an opaque credential-name-context variable outside the allowlist", () => { withTempDir((dir) => { - const run = runWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { CUSTOM_API_KEY: OPAQUE }); + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), [], { + CUSTOM_API_KEY: OPAQUE, + }); expect(run.status).toBe(2); expect(run.launched).toBe(false); diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index e44763195ad..f2ba0d0e9cb 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -1387,6 +1387,7 @@ describe("LangChain Deep Agents Code image contracts", () => { { name: "glpat", sample: "glpat-abcdefghijklmn" }, { name: "gsk", sample: "gsk_abcdefghijklmnop" }, { name: "pypi", sample: "pypi-abcdefghijklmnop" }, + { name: "tavily", sample: "tvly-abcdefghijklmnop" }, { name: "telegram", sample: "123456789:AbcDefGhiJklMnoPqrStuVwxYz012345678" }, { name: "telegram_bot", sample: "bot123456789:AbcDefGhiJklMnoPqrStuVwxYz012345678" }, { From 467f150c2b442a0c78cb348b30486e7f942976e8 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 15:33:32 -0700 Subject: [PATCH 06/14] fix(deepagents-code): address status review follow-ups Use the validated sandbox name as the identity source, pin provider output and mutable-env rejection, and align the quickstart wording. Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../quickstart-langchain-deepagents-code.mdx | 2 +- src/lib/onboard.ts | 1 + src/lib/onboard/sandbox-create-launch.test.ts | 6 +++-- src/lib/onboard/sandbox-create-launch.ts | 8 ++---- test/dcode-wrapper-identity.test.ts | 26 +++++++++++-------- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index cd14a12c2bf..4e5ce23ca1f 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -69,7 +69,7 @@ To confirm which sandbox a session is in, run the identity command: dcode status ``` -It prints the sandbox name, agent, configured model, and inference endpoint, then exits without starting the interactive UI. +It prints the sandbox name, agent, configured provider route and model, and inference endpoint, then exits without starting the interactive UI. `dcode whoami` and `dcode identity` are aliases. The sandbox name resolves when you run the command from a `nemoclaw connect` shell, which loads the NemoClaw runtime environment. diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 51fe60f5115..666fb7bebc8 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3025,6 +3025,7 @@ async function createSandbox( agent, chatUiUrl, createArgs, + sandboxName, env: process.env, extraPlaceholderKeys, getDashboardForwardPort, diff --git a/src/lib/onboard/sandbox-create-launch.test.ts b/src/lib/onboard/sandbox-create-launch.test.ts index 006e43ed7c0..dda31ff4c2f 100644 --- a/src/lib/onboard/sandbox-create-launch.test.ts +++ b/src/lib/onboard/sandbox-create-launch.test.ts @@ -224,11 +224,12 @@ describe("prepareSandboxCreateLaunch", () => { } }); - it("forwards the sandbox name into the Deep Agents Code sandbox create env", () => { + it("forwards the validated sandbox name into the Deep Agents Code sandbox create env", () => { const result = prepareSandboxCreateLaunch({ agent: { name: "langchain-deepagents-code" } as any, chatUiUrl: "", - createArgs: ["--name", "dcode-demo"], + createArgs: ["--name", "rendered-name"], + sandboxName: "dcode-demo", env: {}, extraPlaceholderKeys: [], getDashboardForwardPort: vi.fn(() => "0"), @@ -239,6 +240,7 @@ describe("prepareSandboxCreateLaunch", () => { }); expect(result.envArgs).toContain("NEMOCLAW_SANDBOX_NAME=dcode-demo"); + expect(result.envArgs).not.toContain("NEMOCLAW_SANDBOX_NAME=rendered-name"); }); it("does not forward the sandbox name for non-Deep-Agents-Code agents", () => { diff --git a/src/lib/onboard/sandbox-create-launch.ts b/src/lib/onboard/sandbox-create-launch.ts index ff280c57780..73203db0f04 100644 --- a/src/lib/onboard/sandbox-create-launch.ts +++ b/src/lib/onboard/sandbox-create-launch.ts @@ -17,6 +17,7 @@ export interface SandboxCreateLaunchInput { agent: AgentDefinition | null | undefined; chatUiUrl: string; createArgs: readonly string[]; + sandboxName?: string; env?: NodeJS.ProcessEnv; extraPlaceholderKeys: readonly string[]; getDashboardForwardPort(chatUiUrl: string): string; @@ -34,11 +35,6 @@ export interface SandboxCreateLaunch { sandboxStartupCommand: string[]; } -function readCreateArgValue(createArgs: readonly string[], flag: string): string | undefined { - const flagIndex = createArgs.indexOf(flag); - return flagIndex === -1 ? undefined : createArgs[flagIndex + 1]; -} - export function prepareSandboxCreateLaunch(input: SandboxCreateLaunchInput): SandboxCreateLaunch { const env = input.env ?? process.env; const manageDashboard = input.manageDashboard ?? true; @@ -80,7 +76,7 @@ export function prepareSandboxCreateLaunch(input: SandboxCreateLaunchInput): San } if (input.agent?.name === "langchain-deepagents-code") { - const sandboxName = readCreateArgValue(input.createArgs, "--name"); + const sandboxName = input.sandboxName; if (sandboxName) { envArgs.push(formatEnvAssignment("NEMOCLAW_SANDBOX_NAME", sandboxName)); } diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index 07dacbb6211..b9f4692aa72 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -20,6 +20,7 @@ const canRun = process.platform === "linux"; const SAMPLE_CONFIG = [ "# Generated by NemoClaw. This file contains no provider secrets.", + "# NemoClaw provider route: inference; upstream provider: nvidia-prod; API: openai-completions.", "", "[models]", 'default = "openai:demo-model"', @@ -105,6 +106,7 @@ describe.skipIf(!canRun)( expect(run.launched).toBe(false); expect(run.stdout).toContain("Sandbox: dcode-demo"); expect(run.stdout).toContain("Agent: langchain-deepagents-code"); + expect(run.stdout).toContain("Provider: inference"); expect(run.stdout).toContain("Model: openai:demo-model"); expect(run.stdout).toContain("Endpoint: https://inference.local/v1"); expect(run.stdout).toContain("Runtime: Deep Agents Code (terminal)"); @@ -177,19 +179,21 @@ describe.skipIf(!canRun)( } }); - it("refuses the canonical OpenShell TLS key path in the mutable env file", () => { - withTempDir((dir) => { - const fixture = buildFixture(dir, SAMPLE_CONFIG); - fs.writeFileSync(fixture.envFile, `OPENSHELL_TLS_KEY=${CANONICAL_TLS_KEY_PATH}\n`, "utf8"); + it("refuses OpenShell TLS key values in the mutable env file", () => { + for (const value of [CANONICAL_TLS_KEY_PATH, OPAQUE]) { + withTempDir((dir) => { + const fixture = buildFixture(dir, SAMPLE_CONFIG); + fs.writeFileSync(fixture.envFile, `OPENSHELL_TLS_KEY=${value}\n`, "utf8"); - const run = runBashWrapper(fixture, ["--version"], {}); + const run = runBashWrapper(fixture, ["--version"], {}); - expect(run.status).toBe(2); - expect(run.launched).toBe(false); - expect(run.stderr).toContain("OPENSHELL_TLS_KEY"); - expect(run.stderr).toContain(path.join(dir, ".env")); - expect(run.stderr).not.toContain(CANONICAL_TLS_KEY_PATH); - }); + expect(run.status).toBe(2); + expect(run.launched).toBe(false); + expect(run.stderr).toContain("OPENSHELL_TLS_KEY"); + expect(run.stderr).toContain(path.join(dir, ".env")); + expect(run.stderr).not.toContain(value); + }); + } }); it("still refuses recognized provider tokens carried by OPENSHELL_TLS_KEY", () => { From f25bac9555ea598825f3df140cef93be96e0b0cb Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 15:35:10 -0700 Subject: [PATCH 07/14] refactor(onboard): keep entrypoint growth neutral Inline the one-use messaging plan while passing the validated sandbox name, keeping the guarded onboard entrypoint net-neutral. Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- src/lib/onboard.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 666fb7bebc8..cd5d951a41f 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2998,10 +2998,9 @@ async function createSandbox( const envMessagingState = MessagingHostStateApplier.readPlanStateFromEnv(); const plannedMessagingState = envMessagingState?.plan.sandboxName === sandboxName ? envMessagingState : undefined; - const plannedMessagingPlan = plannedMessagingState?.plan; sandboxBuildPatchConfig.prepareSandboxBuildPatchConfig({ configuredMessagingChannels: - getChannelsFromPlan(plannedMessagingPlan) ?? activeMessagingChannels, + getChannelsFromPlan(plannedMessagingState?.plan) ?? activeMessagingChannels, }); const { buildId } = await sandboxDockerfilePatchFlow.prepareSandboxDockerfilePatch({ agent, From e54e9ba075f647ebc709d2aaed36a4600b60abe6 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 15:43:18 -0700 Subject: [PATCH 08/14] test(deepagents-code): extract start script fixture Move reusable start-script setup into focused test support so the image-contract suite remains under its enforced file-size budget. Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- test/langchain-deepagents-code-image.test.ts | 43 +-------------- test/support/dcode-start-script-fixture.ts | 57 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 test/support/dcode-start-script-fixture.ts diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index f2ba0d0e9cb..d810b5a1f55 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -10,6 +10,7 @@ import YAML from "yaml"; import { CONTEXT_PATTERNS, TOKEN_PREFIX_PATTERNS } from "../src/lib/security/secret-patterns.ts"; import { cloudExperimentalChecksForOnboarding } from "./e2e/live/cloud-experimental-check-list.ts"; +import { makeStartScriptFixture } from "./support/dcode-start-script-fixture.ts"; function fingerprint(patterns: readonly RegExp[]): string[] { return patterns.map((re) => `${re.source}::${re.flags}`); @@ -111,48 +112,6 @@ function policyBinaryPaths(policyText: string, policyName: string): string[] { }); } -function makeStartScriptFixture(tempDir: string): { - envFile: string; - scriptPath: string; -} { - const envFile = path.join(tempDir, "proxy-env.sh"); - const scriptPath = path.join(tempDir, "start.sh"); - const hostFile = path.join(tempDir, "trusted-proxy-host"); - const portFile = path.join(tempDir, "trusted-proxy-port"); - const original = readAgentFile("start.sh"); - expect(original).toContain("local target=/tmp/nemoclaw-proxy-env.sh"); - expect(original).toContain('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"'); - const fixture = original - .replace( - 'readonly MANAGED_PROXY_HOST_FILE="/usr/local/share/nemoclaw/dcode-proxy-host"', - `readonly MANAGED_PROXY_HOST_FILE="${hostFile}"`, - ) - .replace( - 'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"', - `readonly MANAGED_PROXY_PORT_FILE="${portFile}"`, - ) - .replace( - "readonly MANAGED_PROXY_OWNER_UID=0", - `readonly MANAGED_PROXY_OWNER_UID=${process.getuid?.() ?? 0}`, - ) - .replace("local target=/tmp/nemoclaw-proxy-env.sh", `local target="${envFile}"`) - .replace( - 'tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"', - `tmp="$(mktemp "${tempDir}/nemoclaw-proxy-env.XXXXXX")"`, - ); - expect(fixture).toContain(`local target="${envFile}"`); - expect(fixture).toContain(`tmp="$(mktemp "${tempDir}/nemoclaw-proxy-env.XXXXXX")"`); - expect(fixture).not.toContain("local target=/tmp/nemoclaw-proxy-env.sh"); - expect(fixture).not.toContain('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"'); - fs.writeFileSync(hostFile, "10.200.0.1\n", "utf8"); - fs.writeFileSync(portFile, "3128\n", "utf8"); - fs.chmodSync(hostFile, 0o444); - fs.chmodSync(portFile, 0o444); - fs.writeFileSync(scriptPath, fixture, "utf8"); - fs.chmodSync(scriptPath, 0o755); - return { envFile, scriptPath }; -} - const PROXY_URL_ENV_NAMES = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"] as const; const NO_PROXY_ENV_NAMES = ["NO_PROXY", "no_proxy"] as const; diff --git a/test/support/dcode-start-script-fixture.ts b/test/support/dcode-start-script-fixture.ts new file mode 100644 index 00000000000..9ba4e6271fc --- /dev/null +++ b/test/support/dcode-start-script-fixture.ts @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +const START_SCRIPT = path.join( + import.meta.dirname, + "..", + "..", + "agents", + "langchain-deepagents-code", + "start.sh", +); + +export function makeStartScriptFixture(tempDir: string): { + envFile: string; + scriptPath: string; +} { + const envFile = path.join(tempDir, "proxy-env.sh"); + const scriptPath = path.join(tempDir, "start.sh"); + const hostFile = path.join(tempDir, "trusted-proxy-host"); + const portFile = path.join(tempDir, "trusted-proxy-port"); + const original = fs.readFileSync(START_SCRIPT, "utf8"); + assert.ok(original.includes("local target=/tmp/nemoclaw-proxy-env.sh")); + assert.ok(original.includes('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"')); + const fixture = original + .replace( + 'readonly MANAGED_PROXY_HOST_FILE="/usr/local/share/nemoclaw/dcode-proxy-host"', + `readonly MANAGED_PROXY_HOST_FILE="${hostFile}"`, + ) + .replace( + 'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"', + `readonly MANAGED_PROXY_PORT_FILE="${portFile}"`, + ) + .replace( + "readonly MANAGED_PROXY_OWNER_UID=0", + `readonly MANAGED_PROXY_OWNER_UID=${process.getuid?.() ?? 0}`, + ) + .replace("local target=/tmp/nemoclaw-proxy-env.sh", `local target="${envFile}"`) + .replace( + 'tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"', + `tmp="$(mktemp "${tempDir}/nemoclaw-proxy-env.XXXXXX")"`, + ); + assert.ok(fixture.includes(`local target="${envFile}"`)); + assert.ok(fixture.includes(`tmp="$(mktemp "${tempDir}/nemoclaw-proxy-env.XXXXXX")"`)); + assert.ok(!fixture.includes("local target=/tmp/nemoclaw-proxy-env.sh")); + assert.ok(!fixture.includes('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"')); + fs.writeFileSync(hostFile, "10.200.0.1\n", "utf8"); + fs.writeFileSync(portFile, "3128\n", "utf8"); + fs.chmodSync(hostFile, 0o444); + fs.chmodSync(portFile, 0o444); + fs.writeFileSync(scriptPath, fixture, "utf8"); + fs.chmodSync(scriptPath, 0o755); + return { envFile, scriptPath }; +} From b0494d944606d797a84c79682a59f7a7867e43b1 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 15:56:12 -0700 Subject: [PATCH 09/14] fix(deepagents-code): complete managed identity status Report the active dcode agent and upstream provider separately from the NemoClaw harness and route, and advertise managed identity aliases in help. Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../dcode-wrapper.sh | 81 ++++++++++++++++--- .../quickstart-langchain-deepagents-code.mdx | 3 +- test/dcode-wrapper-identity.test.ts | 55 +++++++++++-- 3 files changed, 122 insertions(+), 17 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 126754776d6..94c95835e09 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -303,10 +303,21 @@ assert_no_secret_env_file() { assert_no_secret_runtime_env assert_no_secret_env_file -toml_scalar() { - local key="$1" line +toml_section_scalar() { + local section="$1" + local key="$2" + local line current_section="" [ -r "$DEEPAGENTS_CONFIG_FILE" ] || return 0 while IFS= read -r line || [ -n "$line" ]; do + line="$(trim_whitespace "$line")" + case "$line" in + \[*\]) + current_section="${line#\[}" + current_section="${current_section%\]}" + continue + ;; + esac + [ "$current_section" = "$section" ] || continue case "$line" in "$key = \""*) line="${line#"$key = \""}" @@ -318,14 +329,25 @@ toml_scalar() { return 0 } -toml_provider_route() { - local line +toml_provider_metadata() { + local field="$1" + local line route provider _api [ -r "$DEEPAGENTS_CONFIG_FILE" ] || return 0 while IFS= read -r line || [ -n "$line" ]; do case "$line" in "# NemoClaw provider route: "*) line="${line#"# NemoClaw provider route: "}" - printf '%s' "${line%%;*}" + IFS=';' read -r route provider _api <<<"$line" + route="$(trim_whitespace "$route")" + provider="$(trim_whitespace "$provider")" + case "$provider" in + "upstream provider: "*) provider="${provider#"upstream provider: "}" ;; + *) provider="" ;; + esac + case "$field" in + route) printf '%s' "$route" ;; + provider) printf '%s' "$provider" ;; + esac return 0 ;; esac @@ -333,15 +355,38 @@ toml_provider_route() { return 0 } +resolve_dcode_agent() { + local config_dir candidate + config_dir="${DEEPAGENTS_CONFIG_FILE%/*}" + candidate="$(toml_section_scalar agents default)" + if [ -n "$candidate" ] && [ -d "$config_dir/$candidate" ]; then + printf '%s' "$candidate" + return 0 + fi + candidate="$(toml_section_scalar agents recent)" + if [ -n "$candidate" ] && [ -d "$config_dir/$candidate" ]; then + printf '%s' "$candidate" + return 0 + fi + printf '%s' 'agent (default)' +} + print_identity() { - local sandbox_name model endpoint provider + local sandbox_name agent model endpoint route provider sandbox_name="${NEMOCLAW_SANDBOX_NAME:-unknown}" - model="$(toml_scalar default)" - endpoint="$(toml_scalar base_url)" - provider="$(toml_provider_route)" + agent="$(resolve_dcode_agent)" + model="$(toml_section_scalar models default)" + [ -n "$model" ] || model="$(toml_section_scalar models recent)" + endpoint="$(toml_section_scalar models.providers.openai base_url)" + route="$(toml_provider_metadata route)" + provider="$(toml_provider_metadata provider)" [ -n "$endpoint" ] || endpoint="${OPENAI_BASE_URL:-}" printf 'Sandbox: %s\n' "$sandbox_name" - printf 'Agent: %s\n' 'langchain-deepagents-code' + printf 'Harness: %s\n' 'langchain-deepagents-code' + printf 'Agent: %s\n' "$agent" + if [ -n "$route" ]; then + printf 'Route: %s\n' "$route" + fi if [ -n "$provider" ]; then printf 'Provider: %s\n' "$provider" fi @@ -354,12 +399,26 @@ print_identity() { printf 'Runtime: %s\n' 'Deep Agents Code (terminal)' } +print_managed_help() { + cat <<'EOF' +NemoClaw-managed commands: + dcode status Show managed sandbox and dcode runtime identity + dcode whoami Alias for dcode status + dcode identity Alias for dcode status + +EOF +} + case "${1:-}" in status | whoami | identity) print_identity exit 0 ;; - --version | -v | -V | --help | -h) + --help | -h | help) + print_managed_help + run_dcode "$@" + ;; + --version | -v | -V) run_dcode "$@" ;; esac diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index 4e5ce23ca1f..ecea9e2f617 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -69,8 +69,9 @@ To confirm which sandbox a session is in, run the identity command: dcode status ``` -It prints the sandbox name, agent, configured provider route and model, and inference endpoint, then exits without starting the interactive UI. +The command prints the sandbox name, NemoClaw harness, active `dcode` agent, configured inference route, upstream provider, model, endpoint, and runtime, then exits without starting the interactive UI. `dcode whoami` and `dcode identity` are aliases. +`dcode --help` lists the managed aliases before the upstream Deep Agents Code help. The sandbox name resolves when you run the command from a `nemoclaw connect` shell, which loads the NemoClaw runtime environment. ## Python Environment diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index b9f4692aa72..32079f0273f 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -22,6 +22,10 @@ const SAMPLE_CONFIG = [ "# Generated by NemoClaw. This file contains no provider secrets.", "# NemoClaw provider route: inference; upstream provider: nvidia-prod; API: openai-completions.", "", + "[agents]", + 'default = "backend-dev"', + 'recent = "frontend-dev"', + "", "[models]", 'default = "openai:demo-model"', "", @@ -35,7 +39,7 @@ const SAMPLE_CONFIG = [ const OPAQUE = "Zx3Qw9Lp7Rt2Vn5Bd8Kf1Mh6Cg4Js0Ay"; const CANONICAL_TLS_KEY_PATH = "/etc/openshell/tls/client/tls.key"; -type Fixture = { wrapperPath: string; ranMarker: string; envFile: string }; +type Fixture = { wrapperPath: string; ranMarker: string; envFile: string; configDir: string }; function buildFixture(tempDir: string, configContent: string): Fixture { const wrapperPath = path.join(tempDir, "dcode"); @@ -60,7 +64,11 @@ function buildFixture(tempDir: string, configContent: string): Fixture { fs.writeFileSync(configFile, configContent, "utf8"); fs.writeFileSync(wrapperPath, fixture, "utf8"); fs.chmodSync(wrapperPath, 0o755); - return { wrapperPath, ranMarker, envFile }; + return { wrapperPath, ranMarker, envFile, configDir: tempDir }; +} + +function addAgentDir(fixture: Fixture, name: string): void { + fs.mkdirSync(path.join(fixture.configDir, name)); } type Run = { status: number | null; stdout: string; stderr: string; launched: boolean }; @@ -98,15 +106,19 @@ describe.skipIf(!canRun)( for (const sub of ["status", "whoami", "identity"]) { it(`'${sub}' reports the sandbox identity and does not launch dcode`, () => { withTempDir((dir) => { - const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), [sub], { + const fixture = buildFixture(dir, SAMPLE_CONFIG); + addAgentDir(fixture, "backend-dev"); + const run = runBashWrapper(fixture, [sub], { NEMOCLAW_SANDBOX_NAME: "dcode-demo", }); expect(run.status).toBe(0); expect(run.launched).toBe(false); expect(run.stdout).toContain("Sandbox: dcode-demo"); - expect(run.stdout).toContain("Agent: langchain-deepagents-code"); - expect(run.stdout).toContain("Provider: inference"); + expect(run.stdout).toContain("Harness: langchain-deepagents-code"); + expect(run.stdout).toContain("Agent: backend-dev"); + expect(run.stdout).toContain("Route: inference"); + expect(run.stdout).toContain("Provider: nvidia-prod"); expect(run.stdout).toContain("Model: openai:demo-model"); expect(run.stdout).toContain("Endpoint: https://inference.local/v1"); expect(run.stdout).toContain("Runtime: Deep Agents Code (terminal)"); @@ -114,6 +126,39 @@ describe.skipIf(!canRun)( }); } + it("uses a valid recent dcode agent when the configured default is stale", () => { + withTempDir((dir) => { + const fixture = buildFixture(dir, SAMPLE_CONFIG); + addAgentDir(fixture, "frontend-dev"); + const run = runBashWrapper(fixture, ["status"], {}); + + expect(run.status).toBe(0); + expect(run.stdout).toContain("Agent: frontend-dev"); + }); + }); + + it("uses the upstream default agent when configured preferences are stale", () => { + withTempDir((dir) => { + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["status"], {}); + + expect(run.status).toBe(0); + expect(run.stdout).toContain("Agent: agent (default)"); + }); + }); + + it("advertises the managed identity commands before delegating help upstream", () => { + withTempDir((dir) => { + const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["--help"], {}); + + expect(run.status).toBe(0); + expect(run.launched).toBe(true); + expect(run.stdout).toContain("NemoClaw-managed commands:"); + expect(run.stdout).toContain("dcode status"); + expect(run.stdout).toContain("dcode whoami"); + expect(run.stdout).toContain("dcode identity"); + }); + }); + it("reports the sandbox as unknown when the name was not injected", () => { withTempDir((dir) => { const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["status"], {}); From ea57fdca1bc589aaa646a2c47e3f3f31b882e64f Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 16:06:53 -0700 Subject: [PATCH 10/14] fix(deepagents-code): harden identity metadata Suppress terminal control characters from status output and ignore traversal-shaped configured agent names before checking agent directories. Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../dcode-wrapper.sh | 39 ++++++++++++++----- test/dcode-wrapper-identity.test.ts | 31 +++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 94c95835e09..69aab4f5c73 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -355,31 +355,52 @@ toml_provider_metadata() { return 0 } +is_safe_dcode_agent_name() { + local value="$1" + local LC_ALL=C + [ -n "$value" ] || return 1 + case "$value" in + . | .. | */* | *\\*) return 1 ;; + esac + ! [[ "$value" =~ [[:cntrl:]] ]] +} + resolve_dcode_agent() { local config_dir candidate config_dir="${DEEPAGENTS_CONFIG_FILE%/*}" candidate="$(toml_section_scalar agents default)" - if [ -n "$candidate" ] && [ -d "$config_dir/$candidate" ]; then + if is_safe_dcode_agent_name "$candidate" && [ -d "$config_dir/$candidate" ]; then printf '%s' "$candidate" return 0 fi candidate="$(toml_section_scalar agents recent)" - if [ -n "$candidate" ] && [ -d "$config_dir/$candidate" ]; then + if is_safe_dcode_agent_name "$candidate" && [ -d "$config_dir/$candidate" ]; then printf '%s' "$candidate" return 0 fi printf '%s' 'agent (default)' } +terminal_safe_identity_value() { + local value="$1" + local fallback="${2:-}" + local LC_ALL=C + if [[ "$value" =~ [[:cntrl:]] ]]; then + printf '%s' "$fallback" + else + printf '%s' "$value" + fi +} + print_identity() { local sandbox_name agent model endpoint route provider - sandbox_name="${NEMOCLAW_SANDBOX_NAME:-unknown}" - agent="$(resolve_dcode_agent)" - model="$(toml_section_scalar models default)" - [ -n "$model" ] || model="$(toml_section_scalar models recent)" - endpoint="$(toml_section_scalar models.providers.openai base_url)" - route="$(toml_provider_metadata route)" - provider="$(toml_provider_metadata provider)" + sandbox_name="$(terminal_safe_identity_value "${NEMOCLAW_SANDBOX_NAME:-unknown}" unknown)" + agent="$(terminal_safe_identity_value "$(resolve_dcode_agent)" 'agent (default)')" + model="$(terminal_safe_identity_value "$(toml_section_scalar models default)")" + [ -n "$model" ] || model="$(terminal_safe_identity_value "$(toml_section_scalar models recent)")" + endpoint="$(terminal_safe_identity_value "$(toml_section_scalar models.providers.openai base_url)")" + route="$(terminal_safe_identity_value "$(toml_provider_metadata route)")" + provider="$(terminal_safe_identity_value "$(toml_provider_metadata provider)")" [ -n "$endpoint" ] || endpoint="${OPENAI_BASE_URL:-}" printf 'Sandbox: %s\n' "$sandbox_name" printf 'Harness: %s\n' 'langchain-deepagents-code' diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index 32079f0273f..39a2f0a8e16 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -146,6 +146,37 @@ describe.skipIf(!canRun)( }); }); + it("ignores traversal-shaped agent preferences", () => { + withTempDir((dir) => { + const config = SAMPLE_CONFIG.replace('default = "backend-dev"', 'default = ".."'); + const fixture = buildFixture(dir, config); + addAgentDir(fixture, "frontend-dev"); + const run = runBashWrapper(fixture, ["status"], {}); + + expect(run.status).toBe(0); + expect(run.stdout).toContain("Agent: frontend-dev"); + }); + }); + + it("does not write control characters from mutable identity metadata", () => { + withTempDir((dir) => { + const escape = "\u001b[31m"; + const config = SAMPLE_CONFIG.replace( + 'default = "openai:demo-model"', + `default = "openai:${escape}spoof"`, + ).replace("upstream provider: nvidia-prod", `upstream provider: nvidia-prod${escape}`); + const run = runBashWrapper(buildFixture(dir, config), ["status"], { + NEMOCLAW_SANDBOX_NAME: `demo${escape}`, + }); + + expect(run.status).toBe(0); + expect(run.stdout).not.toContain("\u001b"); + expect(run.stdout).toContain("Sandbox: unknown"); + expect(run.stdout).not.toContain("Provider:"); + expect(run.stdout).not.toContain("Model:"); + }); + }); + it("advertises the managed identity commands before delegating help upstream", () => { withTempDir((dir) => { const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["--help"], {}); From 7b9891ec78304b328bc1809fbe278b1fc70c9152 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 16:18:01 -0700 Subject: [PATCH 11/14] fix(dcode): harden identity display metadata Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../dcode-wrapper.sh | 10 ++--- test/dcode-wrapper-identity.test.ts | 42 ++++++++++++++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 69aab4f5c73..53ee92c2ccb 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -357,12 +357,11 @@ toml_provider_metadata() { is_safe_dcode_agent_name() { local value="$1" + local pattern='^[A-Za-z0-9_ -]+$' local LC_ALL=C [ -n "$value" ] || return 1 - case "$value" in - . | .. | */* | *\\*) return 1 ;; - esac - ! [[ "$value" =~ [[:cntrl:]] ]] + [ -n "$(trim_whitespace "$value")" ] || return 1 + [[ "$value" =~ $pattern ]] } resolve_dcode_agent() { @@ -385,7 +384,7 @@ terminal_safe_identity_value() { local value="$1" local fallback="${2:-}" local LC_ALL=C - if [[ "$value" =~ [[:cntrl:]] ]]; then + if [ ${#value} -gt 256 ] || [[ "$value" =~ [[:cntrl:]] ]]; then printf '%s' "$fallback" else printf '%s' "$value" @@ -402,6 +401,7 @@ print_identity() { route="$(terminal_safe_identity_value "$(toml_provider_metadata route)")" provider="$(terminal_safe_identity_value "$(toml_provider_metadata provider)")" [ -n "$endpoint" ] || endpoint="${OPENAI_BASE_URL:-}" + endpoint="$(terminal_safe_identity_value "$endpoint")" printf 'Sandbox: %s\n' "$sandbox_name" printf 'Harness: %s\n' 'langchain-deepagents-code' printf 'Agent: %s\n' "$agent" diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index 39a2f0a8e16..5e5bab5fb6c 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -158,15 +158,38 @@ describe.skipIf(!canRun)( }); }); + it("ignores agent preferences that dcode cannot activate", () => { + withTempDir((dir) => { + for (const invalidName of [".hidden", " "]) { + const config = SAMPLE_CONFIG.replace( + 'default = "backend-dev"', + `default = "${invalidName}"`, + ); + const fixture = buildFixture(dir, config); + addAgentDir(fixture, invalidName); + addAgentDir(fixture, "frontend-dev"); + const run = runBashWrapper(fixture, ["status"], {}); + + expect(run.status).toBe(0); + expect(run.stdout).toContain("Agent: frontend-dev"); + expect(run.stdout).not.toContain(`Agent: ${invalidName}`); + fs.rmSync(path.join(fixture.configDir, "frontend-dev"), { recursive: true }); + } + }); + }); + it("does not write control characters from mutable identity metadata", () => { withTempDir((dir) => { const escape = "\u001b[31m"; const config = SAMPLE_CONFIG.replace( 'default = "openai:demo-model"', `default = "openai:${escape}spoof"`, - ).replace("upstream provider: nvidia-prod", `upstream provider: nvidia-prod${escape}`); + ) + .replace("upstream provider: nvidia-prod", `upstream provider: nvidia-prod${escape}`) + .replace('base_url = "https://inference.local/v1"', ""); const run = runBashWrapper(buildFixture(dir, config), ["status"], { NEMOCLAW_SANDBOX_NAME: `demo${escape}`, + OPENAI_BASE_URL: `https://inference.local/${escape}`, }); expect(run.status).toBe(0); @@ -174,6 +197,23 @@ describe.skipIf(!canRun)( expect(run.stdout).toContain("Sandbox: unknown"); expect(run.stdout).not.toContain("Provider:"); expect(run.stdout).not.toContain("Model:"); + expect(run.stdout).not.toContain("Endpoint:"); + }); + }); + + it("does not write oversized mutable identity metadata", () => { + withTempDir((dir) => { + const oversized = "x".repeat(257); + const config = SAMPLE_CONFIG.replace('base_url = "https://inference.local/v1"', ""); + const run = runBashWrapper(buildFixture(dir, config), ["status"], { + NEMOCLAW_SANDBOX_NAME: oversized, + OPENAI_BASE_URL: oversized, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain("Sandbox: unknown"); + expect(run.stdout).not.toContain(oversized); + expect(run.stdout).not.toContain("Endpoint:"); }); }); From 4aaaef6422717088474b4374af9f594017eac90b Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 16:31:12 -0700 Subject: [PATCH 12/14] fix(dcode): redact unsafe status endpoints Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../dcode-wrapper.sh | 28 +++++++++- test/dcode-wrapper-identity.test.ts | 54 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 53ee92c2ccb..333b906ab23 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -391,17 +391,41 @@ terminal_safe_identity_value() { fi } +safe_endpoint_identity_value() { + local value scheme authority + value="$(terminal_safe_identity_value "$1")" + [ -n "$value" ] || return 0 + if is_secret_shaped_value "$value"; then + return 0 + fi + case "$value" in + *\\* | *\?* | *\#*) return 0 ;; + esac + scheme="${value%%://*}" + [ "$scheme" != "$value" ] || return 0 + case "${scheme,,}" in + http | https) ;; + *) return 0 ;; + esac + authority="${value#*://}" + authority="${authority%%/*}" + case "$authority" in + "" | *@*) return 0 ;; + esac + printf '%s' "$value" +} + print_identity() { local sandbox_name agent model endpoint route provider sandbox_name="$(terminal_safe_identity_value "${NEMOCLAW_SANDBOX_NAME:-unknown}" unknown)" agent="$(terminal_safe_identity_value "$(resolve_dcode_agent)" 'agent (default)')" model="$(terminal_safe_identity_value "$(toml_section_scalar models default)")" [ -n "$model" ] || model="$(terminal_safe_identity_value "$(toml_section_scalar models recent)")" - endpoint="$(terminal_safe_identity_value "$(toml_section_scalar models.providers.openai base_url)")" + endpoint="$(toml_section_scalar models.providers.openai base_url)" route="$(terminal_safe_identity_value "$(toml_provider_metadata route)")" provider="$(terminal_safe_identity_value "$(toml_provider_metadata provider)")" [ -n "$endpoint" ] || endpoint="${OPENAI_BASE_URL:-}" - endpoint="$(terminal_safe_identity_value "$endpoint")" + endpoint="$(safe_endpoint_identity_value "$endpoint")" printf 'Sandbox: %s\n' "$sandbox_name" printf 'Harness: %s\n' 'langchain-deepagents-code' printf 'Agent: %s\n' "$agent" diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index 5e5bab5fb6c..5c195846686 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -198,6 +198,20 @@ describe.skipIf(!canRun)( expect(run.stdout).not.toContain("Provider:"); expect(run.stdout).not.toContain("Model:"); expect(run.stdout).not.toContain("Endpoint:"); + + const unsafeConfigEndpoint = SAMPLE_CONFIG.replace( + "https://inference.local/v1", + `https://inference.local/${escape}`, + ); + const configEndpointRun = runBashWrapper( + buildFixture(dir, unsafeConfigEndpoint), + ["status"], + { OPENAI_BASE_URL: "https://safe-fallback.example.test/v1" }, + ); + + expect(configEndpointRun.status).toBe(0); + expect(configEndpointRun.stdout).not.toContain("safe-fallback.example.test"); + expect(configEndpointRun.stdout).not.toContain("Endpoint:"); }); }); @@ -217,6 +231,46 @@ describe.skipIf(!canRun)( }); }); + it("does not write unsafe endpoint values from mutable sources", () => { + withTempDir((dir) => { + const unsafeEndpoints = [ + "https://status-user:opaque-password@example.test/v1", + "https://example.test/v1?api_key=opaque-secret", + "https://example.test/v1#opaque-fragment", + "https://status-user:opaque-password\\u0040example.test/v1", + "https://example.test/v1\\u003Fapi_key=opaque-secret", + "https", + ]; + for (const endpoint of unsafeEndpoints) { + for (const source of ["config", "runtime"] as const) { + const config = + source === "config" + ? SAMPLE_CONFIG.replace("https://inference.local/v1", endpoint) + : SAMPLE_CONFIG.replace('base_url = "https://inference.local/v1"', ""); + const env = source === "runtime" ? { OPENAI_BASE_URL: endpoint } : {}; + const run = runBashWrapper(buildFixture(dir, config), ["status"], env); + + expect(run.status).toBe(0); + expect(run.stdout).not.toContain(endpoint); + expect(run.stdout).not.toContain("Endpoint:"); + } + } + }); + }); + + it("writes safe custom endpoint URLs from the runtime fallback", () => { + withTempDir((dir) => { + const endpoint = "https://api.example.test:8443/openai/v1"; + const config = SAMPLE_CONFIG.replace('base_url = "https://inference.local/v1"', ""); + const run = runBashWrapper(buildFixture(dir, config), ["status"], { + OPENAI_BASE_URL: endpoint, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain(`Endpoint: ${endpoint}`); + }); + }); + it("advertises the managed identity commands before delegating help upstream", () => { withTempDir((dir) => { const run = runBashWrapper(buildFixture(dir, SAMPLE_CONFIG), ["--help"], {}); From 4f3f72b97fabeade5921be8c70da01b3fda9aa0b Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 16:38:25 -0700 Subject: [PATCH 13/14] fix(dcode): suppress secret-shaped status metadata Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../dcode-wrapper.sh | 5 +---- test/dcode-wrapper-identity.test.ts | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 333b906ab23..23bc87b6593 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -384,7 +384,7 @@ terminal_safe_identity_value() { local value="$1" local fallback="${2:-}" local LC_ALL=C - if [ ${#value} -gt 256 ] || [[ "$value" =~ [[:cntrl:]] ]]; then + if [ ${#value} -gt 256 ] || [[ "$value" =~ [[:cntrl:]] ]] || is_secret_shaped_value "$value"; then printf '%s' "$fallback" else printf '%s' "$value" @@ -395,9 +395,6 @@ safe_endpoint_identity_value() { local value scheme authority value="$(terminal_safe_identity_value "$1")" [ -n "$value" ] || return 0 - if is_secret_shaped_value "$value"; then - return 0 - fi case "$value" in *\\* | *\?* | *\#*) return 0 ;; esac diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index 5c195846686..816b03c9ceb 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -231,6 +231,27 @@ describe.skipIf(!canRun)( }); }); + it("does not write secret-shaped mutable identity metadata", () => { + withTempDir((dir) => { + const secret = `tvly-${OPAQUE}`; + const config = SAMPLE_CONFIG.replace("route: inference", `route: ${secret}`) + .replace("upstream provider: nvidia-prod", `upstream provider: ${secret}`) + .replace('default = "backend-dev"', `default = "${secret}"`) + .replace('default = "openai:demo-model"', `default = "openai:${secret}"`); + const fixture = buildFixture(dir, config); + addAgentDir(fixture, secret); + const run = runBashWrapper(fixture, ["status"], {}); + + expect(run.status).toBe(0); + expect(run.stdout).not.toContain(secret); + expect(run.stdout).toContain("Sandbox: unknown"); + expect(run.stdout).toContain("Agent: agent (default)"); + expect(run.stdout).not.toContain("Route:"); + expect(run.stdout).not.toContain("Provider:"); + expect(run.stdout).not.toContain("Model:"); + }); + }); + it("does not write unsafe endpoint values from mutable sources", () => { withTempDir((dir) => { const unsafeEndpoints = [ From 97246a0e5eb5fea3190fd38456647f96decf5a12 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 2 Jul 2026 16:48:02 -0700 Subject: [PATCH 14/14] fix(dcode): align context secret detection Co-authored-by: Tinson Lai Signed-off-by: Apurv Kumaria --- .../dcode-wrapper.sh | 11 +++++ test/dcode-wrapper-identity.test.ts | 43 +++++++++++-------- test/langchain-deepagents-code-image.test.ts | 4 ++ 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 23bc87b6593..0f31c71922e 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -63,6 +63,11 @@ run_dcode() { # rejects secret-shaped runtime/.env values, or (b) all dcode invocations # route through a Node entrypoint that imports the canonical patterns directly. +has_context_secret_shape() { + local upper="${1^^}" + [[ "$upper" =~ (_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=:[:space:]][\'\"]?[A-Z0-9_.+/=-]{10,} ]] +} + has_non_slack_secret_shape() { local value="$1" if [[ "$value" =~ (sk-proj-|sk-ant-)[A-Za-z0-9_-]{10,} ]]; then @@ -92,6 +97,9 @@ has_non_slack_secret_shape() { if [[ "$value" =~ [Bb]earer[[:space:]]+[A-Za-z0-9_.+/=-]{10,} ]]; then return 0 fi + if has_context_secret_shape "$value"; then + return 0 + fi return 1 } @@ -180,6 +188,9 @@ is_secret_shaped_value() { if [[ "$value" =~ [Bb]earer[[:space:]]+[A-Za-z0-9_.+/=-]{10,} ]]; then return 0 fi + if has_context_secret_shape "$value"; then + return 0 + fi return 1 } diff --git a/test/dcode-wrapper-identity.test.ts b/test/dcode-wrapper-identity.test.ts index 816b03c9ceb..112079d49ff 100644 --- a/test/dcode-wrapper-identity.test.ts +++ b/test/dcode-wrapper-identity.test.ts @@ -233,22 +233,30 @@ describe.skipIf(!canRun)( it("does not write secret-shaped mutable identity metadata", () => { withTempDir((dir) => { - const secret = `tvly-${OPAQUE}`; - const config = SAMPLE_CONFIG.replace("route: inference", `route: ${secret}`) - .replace("upstream provider: nvidia-prod", `upstream provider: ${secret}`) - .replace('default = "backend-dev"', `default = "${secret}"`) - .replace('default = "openai:demo-model"', `default = "openai:${secret}"`); - const fixture = buildFixture(dir, config); - addAgentDir(fixture, secret); - const run = runBashWrapper(fixture, ["status"], {}); + const agentSecret = "PASSWORD opaquevalue12345"; + fs.mkdirSync(path.join(dir, agentSecret)); + const secretValues = [ + `tvly-${OPAQUE}`, + "API_KEY=opaquevalue12345", + "TOKEN:opaquevalue12345", + agentSecret, + ]; + for (const secret of secretValues) { + const config = SAMPLE_CONFIG.replace("route: inference", `route: ${secret}`) + .replace("upstream provider: nvidia-prod", `upstream provider: ${secret}`) + .replace('default = "backend-dev"', `default = "${agentSecret}"`) + .replace('default = "openai:demo-model"', `default = "openai:${secret}"`); + const run = runBashWrapper(buildFixture(dir, config), ["status"], {}); - expect(run.status).toBe(0); - expect(run.stdout).not.toContain(secret); - expect(run.stdout).toContain("Sandbox: unknown"); - expect(run.stdout).toContain("Agent: agent (default)"); - expect(run.stdout).not.toContain("Route:"); - expect(run.stdout).not.toContain("Provider:"); - expect(run.stdout).not.toContain("Model:"); + expect(run.status).toBe(0); + expect(run.stdout).not.toContain(secret); + expect(run.stdout).not.toContain(agentSecret); + expect(run.stdout).toContain("Sandbox: unknown"); + expect(run.stdout).toContain("Agent: agent (default)"); + expect(run.stdout).not.toContain("Route:"); + expect(run.stdout).not.toContain("Provider:"); + expect(run.stdout).not.toContain("Model:"); + } }); }); @@ -270,9 +278,10 @@ describe.skipIf(!canRun)( : SAMPLE_CONFIG.replace('base_url = "https://inference.local/v1"', ""); const env = source === "runtime" ? { OPENAI_BASE_URL: endpoint } : {}; const run = runBashWrapper(buildFixture(dir, config), ["status"], env); + const refusedByRuntimeGuard = source === "runtime" && /api_key=/i.test(endpoint); - expect(run.status).toBe(0); - expect(run.stdout).not.toContain(endpoint); + expect(run.status).toBe(refusedByRuntimeGuard ? 2 : 0); + expect(`${run.stdout}\n${run.stderr}`).not.toContain(endpoint); expect(run.stdout).not.toContain("Endpoint:"); } } diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index d810b5a1f55..ed82755a851 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -844,6 +844,8 @@ describe("LangChain Deep Agents Code image contracts", () => { const cases: Array<{ name: string; value: string }> = [ { name: "SLACK_BOT_TOKEN", value: "xoxb-sk-abcdefghijklmnopqrstuvwx" }, { name: "SLACK_APP_TOKEN", value: "xapp-ghp_abcdefghijklmnopqr" }, + { name: "SLACK_BOT_TOKEN", value: "xoxb-API_KEY=opaquevalue12345" }, + { name: "SLACK_APP_TOKEN", value: "xapp-TOKEN:opaquevalue12345" }, ]; for (const { name, value } of cases) { @@ -862,6 +864,8 @@ describe("LangChain Deep Agents Code image contracts", () => { const cases: Array<{ name: string; value: string }> = [ { name: "SLACK_BOT_TOKEN", value: "xoxb-nvapi-abcdefghijklmnop" }, { name: "SLACK_APP_TOKEN", value: "xapp-pypi-abcdefghijklmnop" }, + { name: "SLACK_BOT_TOKEN", value: "xoxb-PASSWORD opaquevalue12345" }, + { name: "SLACK_APP_TOKEN", value: "xapp-CREDENTIAL=opaquevalue12345" }, ]; for (const { name, value } of cases) {