From fba46e40407ea272f77ed4a8ea7da0cbf3c5346b Mon Sep 17 00:00:00 2001 From: Ho Lim Date: Fri, 10 Jul 2026 10:35:50 -0700 Subject: [PATCH 1/5] fix(dcode): validate managed fetch CA bundle Signed-off-by: Ho Lim --- .../dcode-launcher.sh | 37 +++++++++++ agents/langchain-deepagents-code/start.sh | 61 +++++++++++++++++++ .../langchain-deepagents-code-headless.ts | 7 +++ ...ain-deepagents-code-proxy-launcher.test.ts | 44 +++++++++++++ test/support/dcode-start-script-fixture.ts | 7 +++ 5 files changed, 156 insertions(+) diff --git a/agents/langchain-deepagents-code/dcode-launcher.sh b/agents/langchain-deepagents-code/dcode-launcher.sh index e1d4918e498..13ad629a2d8 100755 --- a/agents/langchain-deepagents-code/dcode-launcher.sh +++ b/agents/langchain-deepagents-code/dcode-launcher.sh @@ -14,6 +14,7 @@ unset _nemoclaw_auto_approval_env readonly MANAGED_DCODE_WRAPPER="/usr/local/lib/nemoclaw/dcode-wrapper.sh" readonly MANAGED_EXEC_LAUNCHER="/usr/local/lib/nemoclaw/dcode-managed-exec" readonly MANAGED_OBSERVABILITY_MARKER="/sandbox/.deepagents/.nemoclaw-observability-enabled" +readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem" export HOME=/sandbox export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" @@ -97,10 +98,46 @@ read_managed_proxy_value() { printf '%s' "$value" } +managed_fetch_ca_bundle_metadata() { + local file="$1" + local metadata + if metadata="$(stat -c '%u:%a:%s' "$file" 2>/dev/null)"; then + printf '%s' "$metadata" + else + stat -f '%u:%Lp:%z' "$file" 2>/dev/null + fi +} + +validate_managed_fetch_ca_bundle() { + local file="$MANAGED_FETCH_CA_BUNDLE_FILE" + local metadata owner mode size extra + [ -e "$file" ] || return 0 + if [ ! -f "$file" ] || [ -L "$file" ] || [ ! -r "$file" ]; then + printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2 + return 1 + fi + metadata="$(managed_fetch_ca_bundle_metadata "$file")" || { + printf '%s\n' 'Cannot inspect managed fetch CA bundle file.' >&2 + return 1 + } + IFS=: read -r owner mode size extra <<<"$metadata" + if [ -n "${extra:-}" ] \ + || [[ ! "$owner" =~ ^[0-9]+$ ]] \ + || [[ ! "$mode" =~ ^[0-7]{3,4}$ ]] \ + || [[ ! "$size" =~ ^[0-9]+$ ]] \ + || [ "$owner" != "$MANAGED_PROXY_OWNER_UID" ] \ + || [ "$size" -le 0 ] \ + || (((8#$mode & 0022) != 0)); then + printf '%s\n' 'Unsafe ownership or mode on managed fetch CA bundle file.' >&2 + return 1 + fi +} + # Onboard validates the build args and the Dockerfile stores them in root-owned # files. Runtime env is untrusted and cannot override those image-baked values. PROXY_HOST="$(read_managed_proxy_value "$MANAGED_PROXY_HOST_FILE" "host")" PROXY_PORT="$(read_managed_proxy_value "$MANAGED_PROXY_PORT_FILE" "port")" +validate_managed_fetch_ca_bundle unset NEMOCLAW_PROXY_HOST NEMOCLAW_PROXY_PORT # Generic proxy fallbacks are outside the managed dcode contract and may carry # host credentials even after the scheme-specific proxy values are normalized. diff --git a/agents/langchain-deepagents-code/start.sh b/agents/langchain-deepagents-code/start.sh index 719a756e6fc..b37e41064b9 100755 --- a/agents/langchain-deepagents-code/start.sh +++ b/agents/langchain-deepagents-code/start.sh @@ -71,6 +71,7 @@ unset _NEMOCLAW_SANDBOX_RLIMITS # or when dcode no longer uses inference.local. readonly MANAGED_PROXY_HOST_FILE="/usr/local/share/nemoclaw/dcode-proxy-host" readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port" +readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem" readonly MANAGED_PROXY_OWNER_UID=0 managed_proxy_file_metadata() { @@ -104,10 +105,46 @@ read_managed_proxy_value() { printf '%s' "$value" } +managed_fetch_ca_bundle_metadata() { + local file="$1" + local metadata + if metadata="$(stat -c '%u:%a:%s' "$file" 2>/dev/null)"; then + printf '%s' "$metadata" + else + stat -f '%u:%Lp:%z' "$file" 2>/dev/null + fi +} + +validate_managed_fetch_ca_bundle() { + local file="$MANAGED_FETCH_CA_BUNDLE_FILE" + local metadata owner mode size extra + [ -e "$file" ] || return 0 + if [ ! -f "$file" ] || [ -L "$file" ] || [ ! -r "$file" ]; then + printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2 + return 1 + fi + metadata="$(managed_fetch_ca_bundle_metadata "$file")" || { + printf '%s\n' 'Cannot inspect managed fetch CA bundle file.' >&2 + return 1 + } + IFS=: read -r owner mode size extra <<<"$metadata" + if [ -n "${extra:-}" ] \ + || [[ ! "$owner" =~ ^[0-9]+$ ]] \ + || [[ ! "$mode" =~ ^[0-7]{3,4}$ ]] \ + || [[ ! "$size" =~ ^[0-9]+$ ]] \ + || [ "$owner" != "$MANAGED_PROXY_OWNER_UID" ] \ + || [ "$size" -le 0 ] \ + || (((8#$mode & 0022) != 0)); then + printf '%s\n' 'Unsafe ownership or mode on managed fetch CA bundle file.' >&2 + return 1 + fi +} + # Fail closed if the root-owned image contract is missing. Process-level # NEMOCLAW_PROXY_* values are not a trusted runtime routing source. PROXY_HOST="$(read_managed_proxy_value "$MANAGED_PROXY_HOST_FILE" "host")" PROXY_PORT="$(read_managed_proxy_value "$MANAGED_PROXY_PORT_FILE" "port")" +validate_managed_fetch_ca_bundle unset NEMOCLAW_PROXY_HOST NEMOCLAW_PROXY_PORT # Generic proxy fallbacks are outside the managed dcode contract and may carry # host credentials even after the scheme-specific proxy values are normalized. @@ -200,6 +237,30 @@ prepare_runtime_env() { write_export_if_set http_proxy write_export_if_set https_proxy write_export_if_set no_proxy + printf '_nemoclaw_dcode_ca_bundle=%q\n' "$MANAGED_FETCH_CA_BUNDLE_FILE" + printf '_nemoclaw_dcode_ca_owner_uid=%q\n' "$MANAGED_PROXY_OWNER_UID" + cat <<'NEMOCLAW_DCODE_CA_GUARD' +if [ -e "$_nemoclaw_dcode_ca_bundle" ]; then + if [ ! -f "$_nemoclaw_dcode_ca_bundle" ] || [ -L "$_nemoclaw_dcode_ca_bundle" ] || [ ! -r "$_nemoclaw_dcode_ca_bundle" ]; then + printf "%s\n" "Missing or unsafe managed fetch CA bundle file." >&2 + return 1 2>/dev/null || exit 1 + fi + _nemoclaw_dcode_ca_meta="$(stat -c "%u:%a:%s" "$_nemoclaw_dcode_ca_bundle" 2>/dev/null || stat -f "%u:%Lp:%z" "$_nemoclaw_dcode_ca_bundle" 2>/dev/null || true)" + IFS=: read -r _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra <<<"$_nemoclaw_dcode_ca_meta" + if [ -n "${_nemoclaw_dcode_ca_extra:-}" ] \ + || [[ ! "$_nemoclaw_dcode_ca_owner" =~ ^[0-9]+$ ]] \ + || [[ ! "$_nemoclaw_dcode_ca_owner_uid" =~ ^[0-9]+$ ]] \ + || [[ ! "$_nemoclaw_dcode_ca_mode" =~ ^[0-7]{3,4}$ ]] \ + || [[ ! "$_nemoclaw_dcode_ca_size" =~ ^[0-9]+$ ]] \ + || [ "$_nemoclaw_dcode_ca_owner" != "$_nemoclaw_dcode_ca_owner_uid" ] \ + || [ "$_nemoclaw_dcode_ca_size" -le 0 ] \ + || (((8#$_nemoclaw_dcode_ca_mode & 0022) != 0)); then + printf "%s\n" "Unsafe ownership or mode on managed fetch CA bundle file." >&2 + return 1 2>/dev/null || exit 1 + fi +fi +unset _nemoclaw_dcode_ca_bundle _nemoclaw_dcode_ca_owner_uid _nemoclaw_dcode_ca_meta _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra +NEMOCLAW_DCODE_CA_GUARD write_export_if_set SSL_CERT_FILE write_export_if_set REQUESTS_CA_BUNDLE write_export_if_set NODE_EXTRA_CA_CERTS diff --git a/test/helpers/langchain-deepagents-code-headless.ts b/test/helpers/langchain-deepagents-code-headless.ts index 47c620c47be..4c1b4428f71 100644 --- a/test/helpers/langchain-deepagents-code-headless.ts +++ b/test/helpers/langchain-deepagents-code-headless.ts @@ -53,6 +53,7 @@ export function makeStartScriptFixture( const rlimitLib = path.join(tempDir, "sandbox-rlimits.sh"); const hostFile = path.join(tempDir, "trusted-proxy-host"); const portFile = path.join(tempDir, "trusted-proxy-port"); + const caFile = path.join(tempDir, "trusted-ca-bundle.pem"); const markerDir = path.join(tempDir, "persistent-dcode-state"); expect(original).toContain("local target=/tmp/nemoclaw-proxy-env.sh"); expect(original).toContain('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"'); @@ -67,6 +68,10 @@ export function makeStartScriptFixture( 'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"', `readonly MANAGED_PROXY_PORT_FILE="${portFile}"`, ) + .replace( + 'readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"', + `readonly MANAGED_FETCH_CA_BUNDLE_FILE="${caFile}"`, + ) .replace( "readonly MANAGED_PROXY_OWNER_UID=0", `readonly MANAGED_PROXY_OWNER_UID=${process.getuid?.() ?? 0}`, @@ -85,6 +90,7 @@ export function makeStartScriptFixture( expect(fixture).not.toContain("local marker_dir=/sandbox/.deepagents"); fs.writeFileSync(hostFile, "10.200.0.1\n", "utf8"); fs.writeFileSync(portFile, "3128\n", "utf8"); + fs.writeFileSync(caFile, "trusted CA bundle\n", "utf8"); fs.writeFileSync( rlimitLib, "harden_resource_limits() { :; }\nverify_resource_limits_exact() { :; }\n", @@ -92,6 +98,7 @@ export function makeStartScriptFixture( ); fs.chmodSync(hostFile, 0o444); fs.chmodSync(portFile, 0o444); + fs.chmodSync(caFile, 0o444); fs.writeFileSync(scriptPath, fixture, "utf8"); fs.chmodSync(scriptPath, 0o755); return { envFile, scriptPath }; diff --git a/test/langchain-deepagents-code-proxy-launcher.test.ts b/test/langchain-deepagents-code-proxy-launcher.test.ts index b74bea1924c..6c4eef1e30d 100644 --- a/test/langchain-deepagents-code-proxy-launcher.test.ts +++ b/test/langchain-deepagents-code-proxy-launcher.test.ts @@ -40,12 +40,16 @@ function writeManagedProxyFiles( ): void { const hostFile = path.join(tempDir, "trusted-proxy-host"); const portFile = path.join(tempDir, "trusted-proxy-port"); + const caFile = path.join(tempDir, "trusted-ca-bundle.pem"); fs.rmSync(hostFile, { force: true }); fs.rmSync(portFile, { force: true }); + fs.rmSync(caFile, { force: true }); fs.writeFileSync(hostFile, `${managedProxy.host}\n`); fs.writeFileSync(portFile, `${managedProxy.port}\n`); + fs.writeFileSync(caFile, "trusted CA bundle\n"); fs.chmodSync(hostFile, 0o444); fs.chmodSync(portFile, 0o444); + fs.chmodSync(caFile, 0o444); } function replaceManagedProxyFileConstants(source: string, tempDir: string): string { @@ -65,6 +69,10 @@ function replaceManagedProxyFileConstants(source: string, tempDir: string): stri 'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"', `readonly MANAGED_PROXY_PORT_FILE="${path.join(tempDir, "trusted-proxy-port")}"`, ) + .replace( + 'readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"', + `readonly MANAGED_FETCH_CA_BUNDLE_FILE="${path.join(tempDir, "trusted-ca-bundle.pem")}"`, + ) .replace( "readonly MANAGED_PROXY_OWNER_UID=0", `readonly MANAGED_PROXY_OWNER_UID=${TEST_OWNER_UID}`, @@ -489,6 +497,42 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { ); }); + it("rejects writable managed fetch CA bundles in start, connect, and direct dcode paths (#6636)", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-ca-bundle-")); + const launcherPath = makeLauncherProxyProbeFixture(tempDir); + const { envFile, scriptPath } = makeStartProxyProbeFixture(tempDir); + const caFile = path.join(tempDir, "trusted-ca-bundle.pem"); + + const safeStart = spawnSync("bash", [scriptPath, "true"], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }); + expect(safeStart.status, safeStart.stderr).toBe(0); + expect(fs.existsSync(envFile)).toBe(true); + + fs.chmodSync(caFile, 0o666); + const launcherResult = runLauncher(launcherPath, ["-n", "PONG"], {}); + const startResult = spawnSync("bash", [scriptPath, "true"], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }); + const connectSourceResult = spawnSync( + "bash", + ["--noprofile", "--norc", "-c", '. "$1"', "bash", envFile], + { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }, + ); + + expect(launcherResult.status).not.toBe(0); + expect(startResult.status).not.toBe(0); + expect(connectSourceResult.status).not.toBe(0); + const combined = `${launcherResult.stderr}\n${startResult.stderr}\n${connectSourceResult.stderr}`; + expect(combined).toContain("Unsafe ownership or mode on managed fetch CA bundle file"); + expect(combined).not.toContain(caFile); + }); + it("keeps dcode shell proxy validators aligned with onboard validation (#6191)", () => { const start = readAgentFile("start.sh"); const launcher = readAgentFile("dcode-launcher.sh"); diff --git a/test/support/dcode-start-script-fixture.ts b/test/support/dcode-start-script-fixture.ts index 7ae222bfd57..42e881bd8d6 100644 --- a/test/support/dcode-start-script-fixture.ts +++ b/test/support/dcode-start-script-fixture.ts @@ -23,6 +23,7 @@ export function makeStartScriptFixture(tempDir: string): { const rlimitLib = path.join(tempDir, "sandbox-rlimits.sh"); const hostFile = path.join(tempDir, "trusted-proxy-host"); const portFile = path.join(tempDir, "trusted-proxy-port"); + const caFile = path.join(tempDir, "trusted-ca-bundle.pem"); 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)"')); @@ -36,6 +37,10 @@ export function makeStartScriptFixture(tempDir: string): { 'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"', `readonly MANAGED_PROXY_PORT_FILE="${portFile}"`, ) + .replace( + 'readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"', + `readonly MANAGED_FETCH_CA_BUNDLE_FILE="${caFile}"`, + ) .replace( "readonly MANAGED_PROXY_OWNER_UID=0", `readonly MANAGED_PROXY_OWNER_UID=${process.getuid?.() ?? 0}`, @@ -51,6 +56,7 @@ export function makeStartScriptFixture(tempDir: string): { 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.writeFileSync(caFile, "trusted CA bundle\n", "utf8"); fs.writeFileSync( rlimitLib, "harden_resource_limits() { :; }\nverify_resource_limits_exact() { :; }\n", @@ -58,6 +64,7 @@ export function makeStartScriptFixture(tempDir: string): { ); fs.chmodSync(hostFile, 0o444); fs.chmodSync(portFile, 0o444); + fs.chmodSync(caFile, 0o444); fs.writeFileSync(scriptPath, fixture, "utf8"); fs.chmodSync(scriptPath, 0o755); return { envFile, scriptPath }; From a4f3fac5cfc671921009d1794876edbe3b37ff0f Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Fri, 10 Jul 2026 20:57:09 -0700 Subject: [PATCH 2/5] fix(dcode): reject dangling managed CA links Signed-off-by: Carlos Villela --- .../dcode-launcher.sh | 6 ++++- agents/langchain-deepagents-code/start.sh | 13 ++++++++--- ...ain-deepagents-code-proxy-launcher.test.ts | 23 ++++++++++++++++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/agents/langchain-deepagents-code/dcode-launcher.sh b/agents/langchain-deepagents-code/dcode-launcher.sh index 13ad629a2d8..c8084dccadb 100755 --- a/agents/langchain-deepagents-code/dcode-launcher.sh +++ b/agents/langchain-deepagents-code/dcode-launcher.sh @@ -111,8 +111,12 @@ managed_fetch_ca_bundle_metadata() { validate_managed_fetch_ca_bundle() { local file="$MANAGED_FETCH_CA_BUNDLE_FILE" local metadata owner mode size extra + if [ -L "$file" ]; then + printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2 + return 1 + fi [ -e "$file" ] || return 0 - if [ ! -f "$file" ] || [ -L "$file" ] || [ ! -r "$file" ]; then + if [ ! -f "$file" ] || [ ! -r "$file" ]; then printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2 return 1 fi diff --git a/agents/langchain-deepagents-code/start.sh b/agents/langchain-deepagents-code/start.sh index b37e41064b9..5624dd67e23 100755 --- a/agents/langchain-deepagents-code/start.sh +++ b/agents/langchain-deepagents-code/start.sh @@ -118,8 +118,12 @@ managed_fetch_ca_bundle_metadata() { validate_managed_fetch_ca_bundle() { local file="$MANAGED_FETCH_CA_BUNDLE_FILE" local metadata owner mode size extra + if [ -L "$file" ]; then + printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2 + return 1 + fi [ -e "$file" ] || return 0 - if [ ! -f "$file" ] || [ -L "$file" ] || [ ! -r "$file" ]; then + if [ ! -f "$file" ] || [ ! -r "$file" ]; then printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2 return 1 fi @@ -240,8 +244,11 @@ prepare_runtime_env() { printf '_nemoclaw_dcode_ca_bundle=%q\n' "$MANAGED_FETCH_CA_BUNDLE_FILE" printf '_nemoclaw_dcode_ca_owner_uid=%q\n' "$MANAGED_PROXY_OWNER_UID" cat <<'NEMOCLAW_DCODE_CA_GUARD' -if [ -e "$_nemoclaw_dcode_ca_bundle" ]; then - if [ ! -f "$_nemoclaw_dcode_ca_bundle" ] || [ -L "$_nemoclaw_dcode_ca_bundle" ] || [ ! -r "$_nemoclaw_dcode_ca_bundle" ]; then +if [ -L "$_nemoclaw_dcode_ca_bundle" ]; then + printf "%s\n" "Missing or unsafe managed fetch CA bundle file." >&2 + return 1 2>/dev/null || exit 1 +elif [ -e "$_nemoclaw_dcode_ca_bundle" ]; then + if [ ! -f "$_nemoclaw_dcode_ca_bundle" ] || [ ! -r "$_nemoclaw_dcode_ca_bundle" ]; then printf "%s\n" "Missing or unsafe managed fetch CA bundle file." >&2 return 1 2>/dev/null || exit 1 fi diff --git a/test/langchain-deepagents-code-proxy-launcher.test.ts b/test/langchain-deepagents-code-proxy-launcher.test.ts index 6c4eef1e30d..f064c72e896 100644 --- a/test/langchain-deepagents-code-proxy-launcher.test.ts +++ b/test/langchain-deepagents-code-proxy-launcher.test.ts @@ -497,7 +497,24 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { ); }); - it("rejects writable managed fetch CA bundles in start, connect, and direct dcode paths (#6636)", () => { + it.each([ + { + condition: "writable", + expected: "Unsafe ownership or mode on managed fetch CA bundle file", + mutate: (caFile: string) => fs.chmodSync(caFile, 0o666), + }, + { + condition: "dangling symlink", + expected: "Missing or unsafe managed fetch CA bundle file", + mutate: (caFile: string) => { + fs.rmSync(caFile); + fs.symlinkSync(`${caFile}.missing`, caFile); + }, + }, + ])("rejects $condition managed fetch CA bundles in start, connect, and direct dcode paths (#6636)", ({ + expected, + mutate, + }) => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-ca-bundle-")); const launcherPath = makeLauncherProxyProbeFixture(tempDir); const { envFile, scriptPath } = makeStartProxyProbeFixture(tempDir); @@ -510,7 +527,7 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { expect(safeStart.status, safeStart.stderr).toBe(0); expect(fs.existsSync(envFile)).toBe(true); - fs.chmodSync(caFile, 0o666); + mutate(caFile); const launcherResult = runLauncher(launcherPath, ["-n", "PONG"], {}); const startResult = spawnSync("bash", [scriptPath, "true"], { env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, @@ -529,7 +546,7 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { expect(startResult.status).not.toBe(0); expect(connectSourceResult.status).not.toBe(0); const combined = `${launcherResult.stderr}\n${startResult.stderr}\n${connectSourceResult.stderr}`; - expect(combined).toContain("Unsafe ownership or mode on managed fetch CA bundle file"); + expect(combined).toContain(expected); expect(combined).not.toContain(caFile); }); From c65f0a4e5476c30ad3415d8963804895f8925aef Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 11 Jul 2026 01:17:06 -0700 Subject: [PATCH 3/5] test(dcode): cover managed CA bundle states Signed-off-by: Carlos Villela --- ...ain-deepagents-code-proxy-launcher.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/langchain-deepagents-code-proxy-launcher.test.ts b/test/langchain-deepagents-code-proxy-launcher.test.ts index f064c72e896..81f6f94bc46 100644 --- a/test/langchain-deepagents-code-proxy-launcher.test.ts +++ b/test/langchain-deepagents-code-proxy-launcher.test.ts @@ -503,6 +503,37 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { expected: "Unsafe ownership or mode on managed fetch CA bundle file", mutate: (caFile: string) => fs.chmodSync(caFile, 0o666), }, + { + condition: "unreadable", + expected: "Missing or unsafe managed fetch CA bundle file", + mutate: (caFile: string) => fs.chmodSync(caFile, 0o000), + }, + { + condition: "empty", + expected: "Unsafe ownership or mode on managed fetch CA bundle file", + mutate: (caFile: string) => { + fs.chmodSync(caFile, 0o600); + fs.truncateSync(caFile, 0); + fs.chmodSync(caFile, 0o444); + }, + }, + { + condition: "non-regular", + expected: "Missing or unsafe managed fetch CA bundle file", + mutate: (caFile: string) => { + fs.rmSync(caFile); + fs.mkdirSync(caFile); + }, + }, + { + condition: "regular-file symlink", + expected: "Missing or unsafe managed fetch CA bundle file", + mutate: (caFile: string) => { + const target = `${caFile}.target`; + fs.renameSync(caFile, target); + fs.symlinkSync(target, caFile); + }, + }, { condition: "dangling symlink", expected: "Missing or unsafe managed fetch CA bundle file", From 40c975b0b939fc73bbcfc29fa74277934aaaab97 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 11 Jul 2026 01:29:34 -0700 Subject: [PATCH 4/5] test(dcode): harden managed CA assertions Signed-off-by: Carlos Villela --- ...ain-deepagents-code-proxy-launcher.test.ts | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/test/langchain-deepagents-code-proxy-launcher.test.ts b/test/langchain-deepagents-code-proxy-launcher.test.ts index 81f6f94bc46..06471231708 100644 --- a/test/langchain-deepagents-code-proxy-launcher.test.ts +++ b/test/langchain-deepagents-code-proxy-launcher.test.ts @@ -497,17 +497,56 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { ); }); + const expectManagedCaBundleRejection = ({ + expected, + mutate, + }: { + expected: string; + mutate: (caFile: string) => void; + }): void => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-ca-bundle-")); + const launcherPath = makeLauncherProxyProbeFixture(tempDir); + const { envFile, scriptPath } = makeStartProxyProbeFixture(tempDir); + const caFile = path.join(tempDir, "trusted-ca-bundle.pem"); + + const safeStart = spawnSync("bash", [scriptPath, "true"], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }); + expect(safeStart.status, safeStart.stderr).toBe(0); + expect(fs.existsSync(envFile)).toBe(true); + + mutate(caFile); + const launcherResult = runLauncher(launcherPath, ["-n", "PONG"], {}); + const startResult = spawnSync("bash", [scriptPath, "true"], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }); + const connectSourceResult = spawnSync( + "bash", + ["--noprofile", "--norc", "-c", '. "$1"', "bash", envFile], + { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }, + ); + + expect(launcherResult.status).not.toBe(0); + expect(startResult.status).not.toBe(0); + expect(connectSourceResult.status).not.toBe(0); + expect(launcherResult.stderr).toContain(expected); + expect(startResult.stderr).toContain(expected); + expect(connectSourceResult.stderr).toContain(expected); + const combined = `${launcherResult.stderr}\n${startResult.stderr}\n${connectSourceResult.stderr}`; + expect(combined).not.toContain(caFile); + }; + it.each([ { condition: "writable", expected: "Unsafe ownership or mode on managed fetch CA bundle file", mutate: (caFile: string) => fs.chmodSync(caFile, 0o666), }, - { - condition: "unreadable", - expected: "Missing or unsafe managed fetch CA bundle file", - mutate: (caFile: string) => fs.chmodSync(caFile, 0o000), - }, { condition: "empty", expected: "Unsafe ownership or mode on managed fetch CA bundle file", @@ -546,41 +585,19 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { expected, mutate, }) => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-ca-bundle-")); - const launcherPath = makeLauncherProxyProbeFixture(tempDir); - const { envFile, scriptPath } = makeStartProxyProbeFixture(tempDir); - const caFile = path.join(tempDir, "trusted-ca-bundle.pem"); - - const safeStart = spawnSync("bash", [scriptPath, "true"], { - env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, - encoding: "utf8", - }); - expect(safeStart.status, safeStart.stderr).toBe(0); - expect(fs.existsSync(envFile)).toBe(true); - - mutate(caFile); - const launcherResult = runLauncher(launcherPath, ["-n", "PONG"], {}); - const startResult = spawnSync("bash", [scriptPath, "true"], { - env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, - encoding: "utf8", - }); - const connectSourceResult = spawnSync( - "bash", - ["--noprofile", "--norc", "-c", '. "$1"', "bash", envFile], - { - env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, - encoding: "utf8", - }, - ); - - expect(launcherResult.status).not.toBe(0); - expect(startResult.status).not.toBe(0); - expect(connectSourceResult.status).not.toBe(0); - const combined = `${launcherResult.stderr}\n${startResult.stderr}\n${connectSourceResult.stderr}`; - expect(combined).toContain(expected); - expect(combined).not.toContain(caFile); + expectManagedCaBundleRejection({ expected, mutate }); }); + it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( + "rejects an unreadable managed fetch CA bundle in start, connect, and direct dcode paths (#6636)", + () => { + expectManagedCaBundleRejection({ + expected: "Missing or unsafe managed fetch CA bundle file", + mutate: (caFile: string) => fs.chmodSync(caFile, 0o000), + }); + }, + ); + it("keeps dcode shell proxy validators aligned with onboard validation (#6191)", () => { const start = readAgentFile("start.sh"); const launcher = readAgentFile("dcode-launcher.sh"); From 64846bd8ea5f9c3d413c0ed7c46aa69444f6f3f4 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 11 Jul 2026 02:07:14 -0700 Subject: [PATCH 5/5] fix(dcode): make connect CA guard POSIX-compatible Signed-off-by: Carlos Villela --- agents/langchain-deepagents-code/start.sh | 29 ++++++++++++++----- ...ain-deepagents-code-proxy-launcher.test.ts | 17 ++++++----- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/agents/langchain-deepagents-code/start.sh b/agents/langchain-deepagents-code/start.sh index 5624dd67e23..6a6c113c0de 100755 --- a/agents/langchain-deepagents-code/start.sh +++ b/agents/langchain-deepagents-code/start.sh @@ -253,20 +253,33 @@ elif [ -e "$_nemoclaw_dcode_ca_bundle" ]; then return 1 2>/dev/null || exit 1 fi _nemoclaw_dcode_ca_meta="$(stat -c "%u:%a:%s" "$_nemoclaw_dcode_ca_bundle" 2>/dev/null || stat -f "%u:%Lp:%z" "$_nemoclaw_dcode_ca_bundle" 2>/dev/null || true)" - IFS=: read -r _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra <<<"$_nemoclaw_dcode_ca_meta" - if [ -n "${_nemoclaw_dcode_ca_extra:-}" ] \ - || [[ ! "$_nemoclaw_dcode_ca_owner" =~ ^[0-9]+$ ]] \ - || [[ ! "$_nemoclaw_dcode_ca_owner_uid" =~ ^[0-9]+$ ]] \ - || [[ ! "$_nemoclaw_dcode_ca_mode" =~ ^[0-7]{3,4}$ ]] \ - || [[ ! "$_nemoclaw_dcode_ca_size" =~ ^[0-9]+$ ]] \ + IFS=: read -r _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra <&2 return 1 2>/dev/null || exit 1 fi fi -unset _nemoclaw_dcode_ca_bundle _nemoclaw_dcode_ca_owner_uid _nemoclaw_dcode_ca_meta _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra +unset _nemoclaw_dcode_ca_bundle _nemoclaw_dcode_ca_owner_uid _nemoclaw_dcode_ca_meta _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra _nemoclaw_dcode_ca_valid NEMOCLAW_DCODE_CA_GUARD write_export_if_set SSL_CERT_FILE write_export_if_set REQUESTS_CA_BUNDLE diff --git a/test/langchain-deepagents-code-proxy-launcher.test.ts b/test/langchain-deepagents-code-proxy-launcher.test.ts index 06471231708..d15479c01fa 100644 --- a/test/langchain-deepagents-code-proxy-launcher.test.ts +++ b/test/langchain-deepagents-code-proxy-launcher.test.ts @@ -419,6 +419,11 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { expect(launcherResult.status, launcherResult.stderr).toBe(0); expect(startResult.status, startResult.stderr).toBe(0); const envFileText = fs.readFileSync(envFile, "utf8"); + const posixSourceResult = spawnSync("sh", ["-c", '. "$1"', "sh", envFile], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }); + expect(posixSourceResult.status, posixSourceResult.stderr).toBe(0); const launcherNoProxy = launcherResult.stdout.match(/^LAUNCHER_NO_PROXY=(.*)$/m)?.[1]; const startNoProxy = startResult.stdout.match(/^START_PROXY=[^|]*\|([^|]*)\|/m)?.[1]; expect(fs.statSync(envFile).mode & 0o777).toBe(0o444); @@ -522,14 +527,10 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, encoding: "utf8", }); - const connectSourceResult = spawnSync( - "bash", - ["--noprofile", "--norc", "-c", '. "$1"', "bash", envFile], - { - env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, - encoding: "utf8", - }, - ); + const connectSourceResult = spawnSync("sh", ["-c", '. "$1"', "sh", envFile], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, + encoding: "utf8", + }); expect(launcherResult.status).not.toBe(0); expect(startResult.status).not.toBe(0);