diff --git a/.agents/skills/nemoclaw-user-configure-inference/references/switch-inference-providers.md b/.agents/skills/nemoclaw-user-configure-inference/references/switch-inference-providers.md index c3070247f2a..523259d0306 100644 --- a/.agents/skills/nemoclaw-user-configure-inference/references/switch-inference-providers.md +++ b/.agents/skills/nemoclaw-user-configure-inference/references/switch-inference-providers.md @@ -135,9 +135,11 @@ $ nemoclaw onboard `NEMOCLAW_AGENT_TIMEOUT` controls the per-request inference timeout baked into `agents.defaults.timeoutSeconds`. Increase it for slow local inference (for -example, CPU-only Ollama or vLLM on modest hardware). `openclaw.json` is -immutable at runtime, so this value can only be changed by rebuilding the -sandbox via `nemoclaw onboard`. +example, CPU-only Ollama or vLLM on modest hardware). NemoClaw writes this +value into `openclaw.json` during onboarding. The default sandbox may keep that +file writable for agent state, but direct in-sandbox edits are not the supported +or durable way to change NemoClaw-managed defaults. Rebuild the sandbox via +`nemoclaw onboard` to apply a new value. `NEMOCLAW_AGENT_HEARTBEAT_EVERY` sets `agents.defaults.heartbeat.every`. This controls OpenClaw's periodic main-session agent turn. @@ -145,8 +147,10 @@ Each interval, the agent wakes up to review follow-ups and read `HEARTBEAT.md` i The OpenClaw default is 30 minutes (1 hour for Anthropic OAuth / Claude CLI reuse). Tune the cadence with a duration string like `5m` or `2h`, or set `0m` to disable the periodic turns entirely. Disabling also drops `HEARTBEAT.md` from normal-run bootstrap context per upstream behavior, so the model no longer sees heartbeat-only instructions. -`openclaw.json` is immutable at runtime, so the in-sandbox `openclaw config set` command cannot change this. -Rebuild the sandbox via `nemoclaw onboard --resume` to apply a new value. +NemoClaw writes this value into `openclaw.json` during onboarding. +The in-sandbox `openclaw config set` command is not the supported path for +NemoClaw-managed build-time defaults, and direct file edits are overwritten by a +rebuild. Rebuild the sandbox via `nemoclaw onboard --resume` to apply a new value. These variables are build-time settings. If you change them on an existing sandbox, recreate the sandbox so the new values bake into the image: diff --git a/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md b/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md index b2d7e6a4b2e..fc1f8cc23a7 100644 --- a/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md +++ b/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md @@ -89,7 +89,7 @@ flowchart TB * - Filesystem - System binary tampering, credential theft, config manipulation. - Landlock LSM + container mounts - - No. Requires sandbox re-creation. + - Landlock layout: no. Requires sandbox re-creation. Config lockdown posture: yes, with host-side shields commands. * - Process - Privilege escalation, fork bombs, syscall abuse. @@ -230,6 +230,21 @@ For sensitive workloads, use a reviewed host-side immutability workflow after in | Risk of default | A writable `.openclaw` directory lets the agent modify its own gateway config: disabling CORS or redirecting inference to an attacker-controlled endpoint. | | Recommendation | For always-on assistants handling sensitive workloads, lock config after initial setup. For development workflows, the writable default is appropriate. | +### Locking Config with Shields + +NemoClaw exposes the reviewed host-side immutability workflow through shields commands: + +| Command | Purpose | +|---|---| +| `nemoclaw shields status` | Show whether the sandbox is in default mutable mode, locked mode, or temporarily unlocked mode. | +| `nemoclaw shields up` | Opt into lockdown for sensitive workloads by locking config and state entry points with root ownership, read-only modes, and the immutable flag where available. | +| `nemoclaw shields down --timeout 5m --reason ""` | Temporarily return a previously locked sandbox to the mutable default for maintenance, then auto-restore lockdown. | + +Run shields commands from the host. +They use privileged OpenShell and Kubernetes paths that do not inherit the sandbox process's Landlock context. +Landlock itself stays fixed at sandbox creation; `shields up` does not rewrite the Landlock policy. +Instead, it layers DAC permissions and `chattr +i` over paths that the default Landlock policy intentionally leaves writable. + ### Writable Paths The agent has read-write access to `/sandbox`, `/tmp`, and `/dev/null`. diff --git a/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md b/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md index caf1873860f..f7b7a89b500 100644 --- a/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md +++ b/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md @@ -88,6 +88,12 @@ The agent's home directory (`/sandbox`) is writable by default: | `/sandbox/.nemoclaw` | read-write | Plugin state and config; blueprints within are DAC-protected (root-owned) | | `/tmp` | read-write | Temporary files and logs | +This writable default is intentional. +Seeing the sandbox user create files under `/sandbox` or `/sandbox/.openclaw` in a fresh sandbox does not mean Landlock failed. +Landlock still enforces the fixed read-only system paths below. +Use `nemoclaw shields up` from the host to opt into config lockdown for sensitive workloads. +That host-side command layers root ownership, read-only modes, and the immutable flag where available; it does not change the Landlock policy after sandbox creation. + System paths remain read-only to prevent agents from: - Replacing system binaries with trojanized versions diff --git a/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md b/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md index 88bbf57a745..3abc3bf7b91 100644 --- a/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md +++ b/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md @@ -629,7 +629,8 @@ In non-interactive mode (`NEMOCLAW_NON_INTERACTIVE=1`), the commands stage the c ### `openclaw config set` or `unset` is blocked inside the sandbox This is expected. -The sandbox's OpenClaw configuration (`/sandbox/.openclaw/openclaw.json`) is baked into the container image at build time. +NemoClaw builds the sandbox's OpenClaw configuration (`/sandbox/.openclaw/openclaw.json`) from host-side onboarding, rebuild, inference, policy, and messaging inputs. +Fresh sandboxes keep that file writable by default so the agent can manage runtime state, but direct in-sandbox edits are not the supported or durable path for NemoClaw-managed settings. NemoClaw's sandbox entrypoint installs a guard that intercepts `openclaw config set` and `openclaw config unset` and prints an actionable error, because changes made inside the running sandbox do not persist across rebuilds. For most configuration changes, exit the sandbox and rerun onboarding: diff --git a/agents/hermes/Dockerfile.base b/agents/hermes/Dockerfile.base index c4fa2b612d5..9c73d026ae4 100644 --- a/agents/hermes/Dockerfile.base +++ b/agents/hermes/Dockerfile.base @@ -114,19 +114,18 @@ RUN mkdir -p /sandbox/.hermes/memories \ done # Pre-create shell init files for the sandbox user. -# The /sandbox home directory is Landlock read-only at runtime (#804), so these -# files must exist at build time. The Hermes entrypoint writes proxy vars and +# The Hermes entrypoint writes proxy vars and # HERMES_HOME to /tmp/nemoclaw-proxy-env.sh (root-owned, mode 444); these rc # files source it on every interactive `openshell sandbox connect` session. # Ref: #2376. # hadolint ignore=SC2016,SC2028 RUN printf '%s\n' \ - '# Source runtime proxy + HERMES_HOME config (Landlock read-only home, #804)' \ + '# Source runtime proxy + HERMES_HOME config' \ '[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \ 'export PATH="/usr/local/bin:/opt/hermes/.venv/bin:${PATH}"' \ > /sandbox/.bashrc \ && printf '%s\n' \ - '# Source runtime proxy + HERMES_HOME config (Landlock read-only home, #804)' \ + '# Source runtime proxy + HERMES_HOME config' \ '[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \ 'export PATH="/usr/local/bin:/opt/hermes/.venv/bin:${PATH}"' \ > /sandbox/.profile \ diff --git a/docs/deployment/sandbox-hardening.md b/docs/deployment/sandbox-hardening.md index 1497c6370e3..69093a54f2a 100644 --- a/docs/deployment/sandbox-hardening.md +++ b/docs/deployment/sandbox-hardening.md @@ -108,6 +108,12 @@ The agent's home directory (`/sandbox`) is writable by default: | `/sandbox/.nemoclaw` | read-write | Plugin state and config; blueprints within are DAC-protected (root-owned) | | `/tmp` | read-write | Temporary files and logs | +This writable default is intentional. +Seeing the sandbox user create files under `/sandbox` or `/sandbox/.openclaw` in a fresh sandbox does not mean Landlock failed. +Landlock still enforces the fixed read-only system paths below. +Use `nemoclaw shields up` from the host to opt into config lockdown for sensitive workloads. +That host-side command layers root ownership, read-only modes, and the immutable flag where available; it does not change the Landlock policy after sandbox creation. + System paths remain read-only to prevent agents from: - Replacing system binaries with trojanized versions diff --git a/docs/inference/switch-inference-providers.md b/docs/inference/switch-inference-providers.md index 96e7b3af14b..e758b243931 100644 --- a/docs/inference/switch-inference-providers.md +++ b/docs/inference/switch-inference-providers.md @@ -159,9 +159,11 @@ $ nemoclaw onboard `NEMOCLAW_AGENT_TIMEOUT` controls the per-request inference timeout baked into `agents.defaults.timeoutSeconds`. Increase it for slow local inference (for -example, CPU-only Ollama or vLLM on modest hardware). `openclaw.json` is -immutable at runtime, so this value can only be changed by rebuilding the -sandbox via `nemoclaw onboard`. +example, CPU-only Ollama or vLLM on modest hardware). NemoClaw writes this +value into `openclaw.json` during onboarding. The default sandbox may keep that +file writable for agent state, but direct in-sandbox edits are not the supported +or durable way to change NemoClaw-managed defaults. Rebuild the sandbox via +`nemoclaw onboard` to apply a new value. `NEMOCLAW_AGENT_HEARTBEAT_EVERY` sets `agents.defaults.heartbeat.every`. This controls OpenClaw's periodic main-session agent turn. @@ -169,8 +171,10 @@ Each interval, the agent wakes up to review follow-ups and read `HEARTBEAT.md` i The OpenClaw default is 30 minutes (1 hour for Anthropic OAuth / Claude CLI reuse). Tune the cadence with a duration string like `5m` or `2h`, or set `0m` to disable the periodic turns entirely. Disabling also drops `HEARTBEAT.md` from normal-run bootstrap context per upstream behavior, so the model no longer sees heartbeat-only instructions. -`openclaw.json` is immutable at runtime, so the in-sandbox `openclaw config set` command cannot change this. -Rebuild the sandbox via `nemoclaw onboard --resume` to apply a new value. +NemoClaw writes this value into `openclaw.json` during onboarding. +The in-sandbox `openclaw config set` command is not the supported path for +NemoClaw-managed build-time defaults, and direct file edits are overwritten by a +rebuild. Rebuild the sandbox via `nemoclaw onboard --resume` to apply a new value. These variables are build-time settings. If you change them on an existing sandbox, recreate the sandbox so the new values bake into the image: diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md index b3d7c685732..cbf0a9526f7 100644 --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -659,7 +659,8 @@ In non-interactive mode (`NEMOCLAW_NON_INTERACTIVE=1`), the commands stage the c ### `openclaw config set` or `unset` is blocked inside the sandbox This is expected. -The sandbox's OpenClaw configuration (`/sandbox/.openclaw/openclaw.json`) is baked into the container image at build time. +NemoClaw builds the sandbox's OpenClaw configuration (`/sandbox/.openclaw/openclaw.json`) from host-side onboarding, rebuild, inference, policy, and messaging inputs. +Fresh sandboxes keep that file writable by default so the agent can manage runtime state, but direct in-sandbox edits are not the supported or durable path for NemoClaw-managed settings. NemoClaw's sandbox entrypoint installs a guard that intercepts `openclaw config set` and `openclaw config unset` and prints an actionable error, because changes made inside the running sandbox do not persist across rebuilds. For most configuration changes, exit the sandbox and rerun onboarding: diff --git a/docs/security/best-practices.md b/docs/security/best-practices.md index ce936e4d062..3e524f02c8f 100644 --- a/docs/security/best-practices.md +++ b/docs/security/best-practices.md @@ -109,7 +109,7 @@ flowchart TB * - Filesystem - System binary tampering, credential theft, config manipulation. - Landlock LSM + container mounts - - No. Requires sandbox re-creation. + - Landlock layout: no. Requires sandbox re-creation. Config lockdown posture: yes, with host-side shields commands. * - Process - Privilege escalation, fork bombs, syscall abuse. @@ -250,6 +250,21 @@ For sensitive workloads, use a reviewed host-side immutability workflow after in | Risk of default | A writable `.openclaw` directory lets the agent modify its own gateway config: disabling CORS or redirecting inference to an attacker-controlled endpoint. | | Recommendation | For always-on assistants handling sensitive workloads, lock config after initial setup. For development workflows, the writable default is appropriate. | +### Locking Config with Shields + +NemoClaw exposes the reviewed host-side immutability workflow through shields commands: + +| Command | Purpose | +|---|---| +| `nemoclaw shields status` | Show whether the sandbox is in default mutable mode, locked mode, or temporarily unlocked mode. | +| `nemoclaw shields up` | Opt into lockdown for sensitive workloads by locking config and state entry points with root ownership, read-only modes, and the immutable flag where available. | +| `nemoclaw shields down --timeout 5m --reason ""` | Temporarily return a previously locked sandbox to the mutable default for maintenance, then auto-restore lockdown. | + +Run shields commands from the host. +They use privileged OpenShell and Kubernetes paths that do not inherit the sandbox process's Landlock context. +Landlock itself stays fixed at sandbox creation; `shields up` does not rewrite the Landlock policy. +Instead, it layers DAC permissions and `chattr +i` over paths that the default Landlock policy intentionally leaves writable. + ### Writable Paths The agent has read-write access to `/sandbox`, `/tmp`, and `/dev/null`. diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index b38c093ba98..b95b492f440 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -80,10 +80,10 @@ fi # PATH was already locked down at the top of this script (before the # early stderr capture). This comment marks the original location. -# Redirect tool caches and state to /tmp so they don't fail on the read-only -# /sandbox home directory (#804). Without these, tools would try to create -# dotfiles (~/.npm, ~/.cache, ~/.bash_history, ~/.gitconfig, ~/.local, ~/.claude) -# in the Landlock read-only home and fail. +# Redirect tool caches and state to /tmp so transient package-manager and +# shell state stays outside the agent's durable workspace. Without these, tools +# would create noisy dotfiles (~/.npm, ~/.cache, ~/.bash_history, ~/.gitconfig, +# ~/.local, ~/.claude) under /sandbox. # # IMPORTANT: This array is the single source of truth for tool-cache redirects. # The same entries are emitted into /tmp/nemoclaw-proxy-env.sh (see below) so @@ -1527,7 +1527,7 @@ GUARDENVEOF # Slack token rewriter for connect sessions — same conditional pattern. echo "[ -f \"$_SLACK_REWRITER_SCRIPT\" ] && export NODE_OPTIONS=\"\${NODE_OPTIONS:+\$NODE_OPTIONS }--require $_SLACK_REWRITER_SCRIPT\"" # Tool cache redirects — generated from _TOOL_REDIRECTS (single source of truth) - echo '# Tool cache redirects — /sandbox is Landlock read-only (#804)' + echo '# Tool cache redirects — keep transient tool state under /tmp' for _redir in "${_TOOL_REDIRECTS[@]}"; do echo "export ${_redir?}" done diff --git a/src/lib/actions/root-help.ts b/src/lib/actions/root-help.ts index 09801c3c8aa..73909cf0538 100644 --- a/src/lib/actions/root-help.ts +++ b/src/lib/actions/root-help.ts @@ -79,9 +79,14 @@ export function help(): void { lines.push( ` ${D}• Change credentials: credentials reset , then re-run onboard${R}`, ); - lines.push(` ${D}• Agent config is read-only inside the sandbox (Landlock enforced).${R}`); lines.push( - ` ${D} To change ${AGENT_PRODUCT_NAME} settings, re-run onboard to rebuild the sandbox.${R}`, + ` ${D}• Agent config is writable in the default sandbox so ${AGENT_PRODUCT_NAME} can manage runtime state.${R}`, + ); + lines.push( + ` ${D} Use host-side commands or re-run onboard for durable ${AGENT_PRODUCT_NAME} settings.${R}`, + ); + lines.push( + ` ${D} Run \`${CLI_NAME} shields up\` to lock config for sensitive workloads.${R}`, ); lines.push(""); diff --git a/test/e2e-gateway-isolation.sh b/test/e2e-gateway-isolation.sh index 9b2afe9cf7a..e553786f38d 100755 --- a/test/e2e-gateway-isolation.sh +++ b/test/e2e-gateway-isolation.sh @@ -275,8 +275,8 @@ else fi # ── Test 13b: Sandbox user cannot write to .nemoclaw parent ─────── -# Note: /sandbox itself is sandbox-owned (DAC allows writes). Landlock makes it -# read-only in production — tested in checks/04-landlock-readonly.sh instead. +# Note: /sandbox itself is sandbox-owned and writable in the mutable-default +# policy. This check only covers the root-owned .nemoclaw parent. info "13b. Sandbox user cannot create files in /sandbox/.nemoclaw" OUT=$(run_as_sandbox "touch /sandbox/.nemoclaw/testfile 2>&1 || echo BLOCKED") diff --git a/test/e2e/test-sandbox-survival.sh b/test/e2e/test-sandbox-survival.sh index d61bd3b07da..ca509e611c5 100755 --- a/test/e2e/test-sandbox-survival.sh +++ b/test/e2e/test-sandbox-survival.sh @@ -431,9 +431,8 @@ section "Phase 5: Plant state markers in sandbox" MARKER_VALUE="nemoclaw-survival-$(date +%s)" # 5a: Workspace file in writable agent state directory. -# /sandbox/ is read-only by policy (openclaw-sandbox.yaml); writable state -# lives under /sandbox/.openclaw/. OpenShell ≥0.0.36 correctly enforces -# this (NVIDIA/OpenShell#910), so markers must target the writable path. +# /sandbox is writable in the mutable-default policy. Use .openclaw for durable +# agent state markers so survival checks validate the configured state path. # shellcheck disable=SC2029 if ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "echo ${MARKER_VALUE} > /sandbox/.openclaw/.survival-marker-workspace" 2>/dev/null; then pass "Planted workspace marker: /sandbox/.openclaw/.survival-marker-workspace" @@ -477,7 +476,7 @@ if [ -n "$agent_files_before" ]; then fi # 5d: Record a deeper workspace file to test nested persistence -# Uses writable .openclaw path — /sandbox/ is read-only by policy. +# Uses the writable .openclaw path for durable agent state. # shellcheck disable=SC2029 if ssh "${SSH_OPTS[@]}" "$SSH_TARGET" \ "mkdir -p /sandbox/.openclaw/test-data && echo ${MARKER_VALUE} > /sandbox/.openclaw/test-data/nested-marker.txt" \ diff --git a/test/root-help.test.ts b/test/root-help.test.ts new file mode 100644 index 00000000000..005efdcdb5c --- /dev/null +++ b/test/root-help.test.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { help as renderRootHelp } from "../src/lib/actions/root-help"; + +describe("root help", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("describes mutable-default config and host-side lockdown", () => { + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + + renderRootHelp(); + + const output = log.mock.calls.map(([line]) => String(line)).join("\n"); + expect(output).toContain("Agent config is writable in the default sandbox"); + expect(output).toContain("Use host-side commands or re-run onboard"); + expect(output).toContain("shields up"); + expect(output).not.toContain("Agent config is read-only inside the sandbox"); + expect(output).not.toContain("Landlock enforced"); + }); +});