From 6183451839c5e5961b4e41f0050b7511bd05022c Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 27 May 2026 10:59:42 -0700 Subject: [PATCH 1/2] fix(sandbox): restore residual capability warning behavior Signed-off-by: Carlos Villela --- .github/workflows/e2e-script.yaml | 7 -- .github/workflows/nightly-e2e.yaml | 12 +- docs/reference/commands.mdx | 1 - scripts/lib/sandbox-init.sh | 46 +------- src/lib/onboard.ts | 13 ++- test/e2e-gateway-isolation.sh | 39 ++----- test/e2e-script-workflow.test.ts | 5 - test/e2e/test-full-e2e.sh | 5 - test/e2e/test-hermes-e2e.sh | 5 - test/helpers/e2e-workflow-contract.ts | 2 - test/sandbox-init.test.ts | 156 ++------------------------ 11 files changed, 27 insertions(+), 264 deletions(-) diff --git a/.github/workflows/e2e-script.yaml b/.github/workflows/e2e-script.yaml index ea53638b98..db07747be0 100644 --- a/.github/workflows/e2e-script.yaml +++ b/.github/workflows/e2e-script.yaml @@ -66,13 +66,6 @@ on: permissions: contents: read -env: - # GitHub-hosted Linux runners retain dangerous bounding-set capabilities but - # do not grant CAP_SETPCAP to the sandbox entrypoint. Functional E2E runs opt - # in explicitly so #4264's fail-closed guard remains covered by the dedicated - # gateway-isolation security regression instead of breaking every sandbox use. - NEMOCLAW_ALLOW_RESIDUAL_CAPS: "1" - jobs: run: runs-on: ${{ inputs.runner }} diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index d67294f53c..efca3afc5d 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -137,14 +137,6 @@ on: permissions: contents: read -env: - # GitHub-hosted Linux runners retain dangerous bounding-set capabilities but - # do not grant CAP_SETPCAP to the sandbox entrypoint. Nightly functional E2E - # jobs opt in explicitly so #4264's fail-closed guard remains covered by the - # dedicated gateway-isolation security regression instead of breaking every - # sandbox use on these CI hosts. - NEMOCLAW_ALLOW_RESIDUAL_CAPS: "1" - concurrency: group: nightly-e2e-${{ github.event_name }}-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', github.ref, inputs.pr_number || 'manual') || 'schedule' }} cancel-in-progress: true @@ -720,7 +712,7 @@ jobs: timeout_minutes: 60 artifact_name: "openclaw-onboard-security-posture-install-log" artifact_path: "/tmp/nemoclaw-e2e-install.log" - env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_ALLOW_RESIDUAL_CAPS":"1","NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST":"1","NEMOCLAW_E2E_SECURITY_POSTURE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-openclaw-security-posture"}' + env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST":"1","NEMOCLAW_E2E_SECURITY_POSTURE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-openclaw-security-posture"}' nvidia_api_key: true github_token: true secrets: @@ -738,7 +730,7 @@ jobs: timeout_minutes: 60 artifact_name: "hermes-onboard-security-posture-install-log" artifact_path: "/tmp/nemoclaw-e2e-hermes-install.log" - env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_ALLOW_RESIDUAL_CAPS":"1","NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST":"1","NEMOCLAW_E2E_SECURITY_POSTURE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes-security-posture"}' + env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST":"1","NEMOCLAW_E2E_SECURITY_POSTURE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes-security-posture"}' nvidia_api_key: true github_token: true secrets: diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 83ee3dc008..85451dca94 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -1277,7 +1277,6 @@ Set them before running `nemoclaw onboard`. | `NEMOCLAW_OLLAMA_INSTALL_MODE` | `system`, `user`, or empty/unset | Pins the Linux Ollama install location; see the Linux Ollama install mode details below. | | `NEMOCLAW_PROXY_HOST` | hostname or IP | Overrides the sandbox-side outbound HTTP proxy host. Defaults to `10.200.0.1`. | | `NEMOCLAW_PROXY_PORT` | integer port | Overrides the sandbox-side outbound HTTP proxy port. Defaults to `3128`. | -| `NEMOCLAW_ALLOW_RESIDUAL_CAPS` | `1` to opt in; unset by default | Allows sandbox startup to continue on hosts that cannot drop dangerous bounding-set capabilities. Only set this when you accept the weaker security posture. | | `NEMOCLAW_OPENSHELL_BIN` | path | Overrides the `openshell` binary the CLI invokes. Defaults to `openshell` (resolved via `PATH`). | | `NEMOCLAW_SANDBOX` | sandbox name | Alternate spelling of `NEMOCLAW_SANDBOX_NAME`; used by `services` and `debug` lookups when neither a flag nor `NEMOCLAW_SANDBOX_NAME` is set. | | `NEMOCLAW_INSTALL_REF` | git ref | For internal installer commands: the git ref to install from. Overridden by the `--install-ref` flag. | diff --git a/scripts/lib/sandbox-init.sh b/scripts/lib/sandbox-init.sh index d54f945a6b..54edab7533 100755 --- a/scripts/lib/sandbox-init.sh +++ b/scripts/lib/sandbox-init.sh @@ -235,15 +235,10 @@ drop_capabilities() { --drop=cap_sys_admin,cap_sys_ptrace,cap_net_raw,cap_dac_override,cap_sys_chroot,cap_fsetid,cap_setfcap,cap_mknod,cap_audit_write,cap_net_bind_service \ -- -c "exec $entrypoint \"\$@\"" -- "$@" else - # report_residual_capabilities intentionally returns non-zero when - # dangerous caps remain. Handle that status explicitly so `set -e` - # cannot skip the refusal banner or the opt-in escape hatch. - report_residual_capabilities || true - enforce_residual_capability_policy "CAP_SETPCAP unavailable" + report_residual_capabilities fi elif [ "${NEMOCLAW_CAPS_DROPPED:-}" != "1" ]; then echo "[SECURITY WARNING] capsh not available — running with default capabilities" >&2 - enforce_residual_capability_policy "capsh not available" fi } @@ -251,8 +246,6 @@ drop_capabilities() { # residual dangerous bounding-set caps surface in logs instead of being # silently inherited from the container runtime. Called from the # CAP_SETPCAP-missing fallback path of drop_capabilities() (issue #3280). -# Returns 0 when no dangerous caps remain (or status unreadable), non-zero -# when dangerous caps were detected so callers can refuse to start. report_residual_capabilities() { echo "[SECURITY] CAP_SETPCAP not available — cannot drop bounding-set caps via capsh" >&2 @@ -281,44 +274,7 @@ report_residual_capabilities() { done if [ -n "$present_caps" ]; then echo "[SECURITY] Dangerous caps remain in bounding set: ${present_caps}" >&2 - return 1 - fi - return 0 -} - -# Refuse to continue when the bounding-set drop did not actually happen, so -# the sandbox is never started with a security posture weaker than the -# script's stated intent (issue #4264). NEMOCLAW_ALLOW_RESIDUAL_CAPS=1 is -# the explicit opt-in for environments like Brev shadecloud where the host -# kernel doesn't grant CAP_SETPCAP — operators acknowledge they are running -# with a weaker posture. -enforce_residual_capability_policy() { - local reason="$1" - if [ "${NEMOCLAW_ALLOW_RESIDUAL_CAPS:-}" = "1" ]; then - echo "[SECURITY] NEMOCLAW_ALLOW_RESIDUAL_CAPS=1 set — continuing with weakened posture" >&2 - return 0 fi - cat >&2 <&1) || RC=$? -if [ "$RC" -ne 0 ] \ - && echo "$OUT" | grep -q "Refusing to start sandbox" \ - && echo "$OUT" | grep -q "NEMOCLAW_ALLOW_RESIDUAL_CAPS=1" \ - && ! echo "$OUT" | grep -q "SHOULD_NOT_REACH"; then - pass "non-root CAP_SETPCAP fallback fails closed with an explicit residual-cap banner" -else - fail "non-root residual-cap fallback did not fail closed as expected (rc=$RC): $OUT" -fi - -# ── Test 28: Non-root mode executes without gosu when opted in ──── +# ── Test 27: Non-root mode executes without gosu ────────────────── # The entrypoint detects uid != 0, skips gosu, and execs the command directly. # Use the image's actual sandbox uid/gid here: the system-assigned sandbox uid # is not guaranteed to be 1000 on every runner, and the non-root fallback is -# designed to run as that sandbox user. This test opts in to the known weaker -# residual-cap posture because it is validating the non-root execution path, -# while Test 27 validates the default refusal. +# designed to run as that sandbox user. -info "28. Non-root mode executes command without gosu when residual caps are explicitly allowed" -OUT=$(docker run --rm --user "${SB_UID}:${SB_GID}" -e NEMOCLAW_ALLOW_RESIDUAL_CAPS=1 "$IMAGE" bash -c 'printf "%s\n" "NON_ROOT_EXEC_OK"; sleep 0.2' 2>&1 || true) +info "27. Non-root mode executes command without gosu" +OUT=$(docker run --rm --user "${SB_UID}:${SB_GID}" "$IMAGE" bash -c 'printf "%s\n" "NON_ROOT_EXEC_OK"; sleep 0.2' 2>&1 || true) if echo "$OUT" | grep -q "NON_ROOT_EXEC_OK"; then - pass "non-root mode executed command directly (no gosu) with explicit residual-cap opt-in" + pass "non-root mode executed command directly (no gosu)" else - fail "non-root command execution failed despite residual-cap opt-in: $OUT" + fail "non-root command execution failed: $OUT" fi -# ── Test 29: Model override patches openclaw.json at startup ───── +# ── Test 28: Model override patches openclaw.json at startup ───── # NEMOCLAW_MODEL_OVERRIDE should patch agents.defaults.model.primary, # model id, and model name in openclaw.json before Landlock locks it. # Ref: https://github.com/NVIDIA/NemoClaw/issues/759 -info "29. NEMOCLAW_MODEL_OVERRIDE patches openclaw.json" +info "28. NEMOCLAW_MODEL_OVERRIDE patches openclaw.json" OUT=$(docker run --rm -e NEMOCLAW_MODEL_OVERRIDE="test/override-model" \ --entrypoint "" "$IMAGE" bash -c ' # Source the entrypoint functions without running the full startup @@ -559,9 +540,9 @@ else fail "model override did not patch correctly: $OUT" fi -# ── Test 30: Model override is a no-op when env var is unset ───── +# ── Test 29: Model override is a no-op when env var is unset ───── -info "30. No override when NEMOCLAW_MODEL_OVERRIDE is unset" +info "29. No override when NEMOCLAW_MODEL_OVERRIDE is unset" OUT=$(docker run --rm --entrypoint "" "$IMAGE" bash -c ' source <(sed -n "/^apply_model_override/,/^}/p" /usr/local/bin/nemoclaw-start) ORIGINAL=$(python3 -c "import json; print(json.load(open(\"/sandbox/.openclaw/openclaw.json\"))[\"agents\"][\"defaults\"][\"model\"][\"primary\"])") diff --git a/test/e2e-script-workflow.test.ts b/test/e2e-script-workflow.test.ts index cfc9f62026..cacf36d1db 100644 --- a/test/e2e-script-workflow.test.ts +++ b/test/e2e-script-workflow.test.ts @@ -8,11 +8,6 @@ import { loadE2eWorkflowContract, reusableNightlyJobs } from "./helpers/e2e-work describe("E2E reusable workflow contract", () => { const { runnerWorkflow, nightlyWorkflow, action } = loadE2eWorkflowContract(); - it("opts functional E2E workflows into residual-cap execution on CI hosts", () => { - expect(runnerWorkflow.env?.NEMOCLAW_ALLOW_RESIDUAL_CAPS).toBe("1"); - expect(nightlyWorkflow.env?.NEMOCLAW_ALLOW_RESIDUAL_CAPS).toBe("1"); - }); - it("does not persist checkout credentials in the reusable runner", () => { const checkoutSteps = runnerWorkflow.jobs.run.steps.filter((step) => String(step.uses ?? "").startsWith("actions/checkout@"), diff --git a/test/e2e/test-full-e2e.sh b/test/e2e/test-full-e2e.sh index ecfc15862f..f8685b1181 100755 --- a/test/e2e/test-full-e2e.sh +++ b/test/e2e/test-full-e2e.sh @@ -138,11 +138,6 @@ if [ "${NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE:-}" != "1" ]; then exit 1 fi -if [ "${NEMOCLAW_E2E_SECURITY_POSTURE:-}" = "1" ] && [ "${NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST:-}" = "1" ]; then - export NEMOCLAW_ALLOW_RESIDUAL_CAPS="${NEMOCLAW_ALLOW_RESIDUAL_CAPS:-1}" - info "Security-posture E2E is explicitly opting in to residual caps on non-root hosts; fail-closed behavior is covered by gateway-isolation." -fi - # ══════════════════════════════════════════════════════════════════ # Phase 2: Install nemoclaw (non-interactive mode) # ══════════════════════════════════════════════════════════════════ diff --git a/test/e2e/test-hermes-e2e.sh b/test/e2e/test-hermes-e2e.sh index 6829d01e77..94029f182d 100755 --- a/test/e2e/test-hermes-e2e.sh +++ b/test/e2e/test-hermes-e2e.sh @@ -185,11 +185,6 @@ fi info "NEMOCLAW_AGENT=${NEMOCLAW_AGENT}" -if [ "${NEMOCLAW_E2E_SECURITY_POSTURE:-}" = "1" ] && [ "${NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST:-}" = "1" ]; then - export NEMOCLAW_ALLOW_RESIDUAL_CAPS="${NEMOCLAW_ALLOW_RESIDUAL_CAPS:-1}" - info "Security-posture E2E is explicitly opting in to residual caps on non-root hosts; fail-closed behavior is covered by gateway-isolation." -fi - # ══════════════════════════════════════════════════════════════════ # Phase 2: Install nemoclaw (non-interactive mode, --agent hermes) # ══════════════════════════════════════════════════════════════════ diff --git a/test/helpers/e2e-workflow-contract.ts b/test/helpers/e2e-workflow-contract.ts index f293990c42..451e69fa10 100644 --- a/test/helpers/e2e-workflow-contract.ts +++ b/test/helpers/e2e-workflow-contract.ts @@ -23,12 +23,10 @@ export type WorkflowStep = { }; export type NightlyWorkflow = { - env?: Record; jobs: Record; }; export type RunnerWorkflow = { - env?: Record; jobs: { run: { steps: WorkflowStep[]; diff --git a/test/sandbox-init.test.ts b/test/sandbox-init.test.ts index 38bd02fc06..b9ee593cf0 100644 --- a/test/sandbox-init.test.ts +++ b/test/sandbox-init.test.ts @@ -97,33 +97,6 @@ function pathExists(filePath: string): boolean { } } -function makeCapSetpcapUnavailableStubs(): string { - const stubDir = mkdtempSync(join(tmpdir(), "sandbox-init-capsh-")); - writeFileSync( - join(stubDir, "capsh"), - [ - "#!/bin/sh", - 'if [ "${1:-}" = "--has-p=cap_setpcap" ]; then', - " exit 1", - "fi", - "exit 99", - "", - ].join("\n"), - { mode: 0o755 }, - ); - writeFileSync( - join(stubDir, "awk"), - [ - "#!/bin/sh", - "# cap_sys_admin (bit 21) + cap_dac_override (bit 1)", - "printf '%s\\n' '0000000000200002'", - "", - ].join("\n"), - { mode: 0o755 }, - ); - return stubDir; -} - function backupTmpArtifacts(paths: string[], backupDir: string): Record { const backups: Record = {}; @@ -405,102 +378,19 @@ EOF }); describe("drop_capabilities", () => { - it("refuses to start when capsh is unavailable (no NEMOCLAW_ALLOW_RESIDUAL_CAPS) (#4264)", () => { - const { stdout, stderr } = runWithLib( - ` - # Hide capsh from PATH so the function falls through; should exit 1 - drop_capabilities /usr/local/bin/fake-entrypoint - echo "SHOULD_NOT_REACH" - `, - { - env: { - PATH: "/usr/bin:/bin", - NEMOCLAW_CAPS_DROPPED: "", - NEMOCLAW_ALLOW_RESIDUAL_CAPS: "", - }, - expectFail: true, - }, - ); - const combined = `${stdout}\n${stderr}`; - expect(combined).toContain("capsh not available"); - expect(combined).toContain("Refusing to start sandbox"); - expect(combined).toContain("NEMOCLAW_ALLOW_RESIDUAL_CAPS=1"); - expect(combined).not.toContain("SHOULD_NOT_REACH"); - }); - - it("continues with explicit opt-in via NEMOCLAW_ALLOW_RESIDUAL_CAPS=1 (#4264)", () => { + it("function is defined and callable", () => { + // We can't test actual capsh on macOS, but verify the function exists + // and handles the no-capsh case gracefully. Capture stderr via redirect. const { stdout } = runWithLib( ` + # Hide capsh from PATH so the function falls through drop_capabilities /usr/local/bin/fake-entrypoint 2>&1 - echo "CONTINUED_OK" + echo "FALLTHROUGH_OK" `, - { - env: { - PATH: "/usr/bin:/bin", - NEMOCLAW_CAPS_DROPPED: "", - NEMOCLAW_ALLOW_RESIDUAL_CAPS: "1", - }, - }, + { env: { PATH: "/usr/bin:/bin", NEMOCLAW_CAPS_DROPPED: "" } }, ); expect(stdout).toContain("capsh not available"); - expect(stdout).toContain("NEMOCLAW_ALLOW_RESIDUAL_CAPS=1 set"); - expect(stdout).toContain("CONTINUED_OK"); - }); - - it("prints the refusal banner when CAP_SETPCAP is unavailable and dangerous caps remain (#4264)", () => { - const stubDir = makeCapSetpcapUnavailableStubs(); - try { - const { stdout, stderr } = runWithLib( - ` - drop_capabilities /usr/local/bin/fake-entrypoint - echo "SHOULD_NOT_REACH" - `, - { - env: { - PATH: `${stubDir}:${process.env.PATH ?? ""}`, - NEMOCLAW_CAPS_DROPPED: "", - NEMOCLAW_ALLOW_RESIDUAL_CAPS: "", - }, - expectFail: true, - }, - ); - const combined = `${stdout}\n${stderr}`; - expect(combined).toContain("CAP_SETPCAP unavailable"); - expect(combined).toContain("Residual CapBnd=0000000000200002"); - expect(combined).toContain( - "Dangerous caps remain in bounding set: cap_sys_admin,cap_dac_override", - ); - expect(combined).toContain("Refusing to start sandbox"); - expect(combined).toContain("NEMOCLAW_ALLOW_RESIDUAL_CAPS=1"); - expect(combined).not.toContain("SHOULD_NOT_REACH"); - } finally { - rmSync(stubDir, { recursive: true, force: true }); - } - }); - - it("honors residual-cap opt-in after CAP_SETPCAP diagnostics under set -e (#4264)", () => { - const stubDir = makeCapSetpcapUnavailableStubs(); - try { - const { stdout } = runWithLib( - ` - drop_capabilities /usr/local/bin/fake-entrypoint 2>&1 - echo "CONTINUED_CAP_SETPCAP_OPT_IN" - `, - { - env: { - PATH: `${stubDir}:${process.env.PATH ?? ""}`, - NEMOCLAW_CAPS_DROPPED: "", - NEMOCLAW_ALLOW_RESIDUAL_CAPS: "1", - }, - }, - ); - expect(stdout).toContain("CAP_SETPCAP not available"); - expect(stdout).toContain("Dangerous caps remain in bounding set"); - expect(stdout).toContain("NEMOCLAW_ALLOW_RESIDUAL_CAPS=1 set"); - expect(stdout).toContain("CONTINUED_CAP_SETPCAP_OPT_IN"); - } finally { - rmSync(stubDir, { recursive: true, force: true }); - } + expect(stdout).toContain("FALLTHROUGH_OK"); }); it("skips when NEMOCLAW_CAPS_DROPPED=1", () => { @@ -515,38 +405,6 @@ EOF }); }); - describe("enforce_residual_capability_policy", () => { - it("returns 0 with an opt-in note when NEMOCLAW_ALLOW_RESIDUAL_CAPS=1 (#4264)", () => { - const { stdout } = runWithLib( - ` - enforce_residual_capability_policy "test reason" 2>&1 - echo "RETURNED_OK" - `, - { env: { NEMOCLAW_ALLOW_RESIDUAL_CAPS: "1" } }, - ); - expect(stdout).toContain("continuing with weakened posture"); - expect(stdout).toContain("RETURNED_OK"); - }); - - it("exits 1 with a banner when no opt-in (#4264)", () => { - const { stdout, stderr } = runWithLib( - ` - enforce_residual_capability_policy "test reason" - echo "SHOULD_NOT_REACH" - `, - { - env: { NEMOCLAW_ALLOW_RESIDUAL_CAPS: "" }, - expectFail: true, - }, - ); - const combined = `${stdout}\n${stderr}`; - expect(combined).toContain("Refusing to start sandbox"); - expect(combined).toContain("test reason"); - expect(combined).toContain("https://github.com/NVIDIA/NemoClaw/issues/4264"); - expect(stdout).not.toContain("SHOULD_NOT_REACH"); - }); - }); - describe("init_step_down_prefixes", () => { it("falls back to gosu when setpriv is unavailable", () => { // Source-time init runs before our test body, so re-run it with a From 34ea16fd029fdc491eec19c50af5d2a3940dbee4 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 27 May 2026 12:17:12 -0700 Subject: [PATCH 2/2] fix(onboard): trim sandbox env allowlist comment Signed-off-by: Carlos Villela --- src/lib/onboard.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index eeb113feda..761c3c2454 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3514,13 +3514,10 @@ async function createSandbox( // and passed EVERYTHING else — including GITHUB_TOKEN, // AWS_SECRET_ACCESS_KEY, SSH_AUTH_SOCK, KUBECONFIG, NPM_TOKEN, and // any CI/CD secrets that happened to be in the host environment. - // The allowlist inverts the default: only known-safe env vars are - // forwarded, everything else is dropped. - // - // For the sandbox specifically, we also strip KUBECONFIG and - // SSH_AUTH_SOCK — the generic allowlist includes these for host-side - // subprocesses (gateway start, openshell CLI) but the sandbox should - // never have access to the host's Kubernetes cluster or SSH agent. + // The allowlist inverts the default: only known-safe env vars are forwarded. + // For sandbox create, also strip KUBECONFIG and SSH_AUTH_SOCK: the generic + // allowlist needs them for host-side subprocesses, but sandbox code must not + // access host Kubernetes or SSH-agent credentials. const envArgs = [formatEnvAssignment("CHAT_UI_URL", chatUiUrl)]; // Always pass the effective dashboard port into the sandbox so // nemoclaw-start.sh starts the gateway on the correct port. When the