Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,22 @@ $ 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.
Each interval, the agent wakes up to review follow-ups and read `HEARTBEAT.md` if present in the workspace.
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <name> shields status` | Show whether the sandbox is in default mutable mode, locked mode, or temporarily unlocked mode. |
| `nemoclaw <name> 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 <name> shields down --timeout 5m --reason "<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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 3 additions & 4 deletions agents/hermes/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
6 changes: 6 additions & 0 deletions docs/deployment/sandbox-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> 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
Expand Down
14 changes: 9 additions & 5 deletions docs/inference/switch-inference-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,22 @@ $ 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.
Each interval, the agent wakes up to review follow-ups and read `HEARTBEAT.md` if present in the workspace.
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:
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 16 additions & 1 deletion docs/security/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <name> shields status` | Show whether the sandbox is in default mutable mode, locked mode, or temporarily unlocked mode. |
| `nemoclaw <name> 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 <name> shields down --timeout 5m --reason "<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`.
Expand Down
10 changes: 5 additions & 5 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions src/lib/actions/root-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,14 @@ export function help(): void {
lines.push(
` ${D}• Change credentials: credentials reset <PROVIDER>, 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} <name> shields up\` to lock config for sensitive workloads.${R}`,
);

lines.push("");
Expand Down
4 changes: 2 additions & 2 deletions test/e2e-gateway-isolation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 3 additions & 4 deletions test/e2e/test-sandbox-survival.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" \
Expand Down
25 changes: 25 additions & 0 deletions test/root-help.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading