Skip to content

fix: make openclaw.json immutable at runtime, move config to build time#588

Merged
kjw3 merged 3 commits into
mainfrom
fix/immutable-openclaw-config
Mar 21, 2026
Merged

fix: make openclaw.json immutable at runtime, move config to build time#588
kjw3 merged 3 commits into
mainfrom
fix/immutable-openclaw-config

Conversation

@ericksoa

@ericksoa ericksoa commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes openclaw.json fully immutable at runtime by moving all configuration to build time and removing all runtime config modification from inside the sandbox. Model routing is handled by the host-side gateway (openshell inference set), not from inside the sandbox.

  • Bake both nvidia and inference providers into openclaw.json at Docker build time
  • Remove runtime Python config-patching from buildSandboxConfigSyncScript (was writing to locked root:root 444 file)
  • Remove openclaw models set from sync script — it also writes to openclaw.json, which is correctly locked. Model selection happens from the host via openshell inference set (Step 5 of onboard)
  • Add identity/, devices/, canvas/, cron/, update-check.json to .openclaw-data symlinks so the gateway can write device-auth.json at runtime
  • Remove dead openclaw doctor --fix and openclaw plugins install calls from nemoclaw-start.sh (already ran at build time, fail with EPERM at runtime)
  • Fix # pragma: allowlist secret comments that broke inline Python in Dockerfile (DanTup's finding on fix: resolve openclaw.json permissions conflict and scope Dockerfile … #570)
  • Clean up dead code: pythonLiteralJson, unused getOpenClawPrimaryModel/DEFAULT_OLLAMA_MODEL imports

Caused-by: 2d3f84e (fix: lock gateway config via Landlock filesystem policy)
Fixes #580
Follow-up to #514
Supersedes #570 (cherry-picked + pragma fix + removed openclaw models set)

Design

The sandbox should never modify its own configuration — that's giving the prisoner keys to his own cell. The OpenShell security model enforces this via:

  1. Landlock/sandbox/.openclaw is read-only in the filesystem policy
  2. DACopenclaw.json is root:root 444
  3. Host-side routingopenshell inference set configures the gateway's privacy router from outside; the sandbox just calls https://inference.local/v1 and the gateway handles model routing, credential injection, and request rewriting

The sync script (Step 6) now only writes NemoClaw's own selection config to ~/.nemoclaw/config.json (writable). All model/provider config lives at the gateway layer.

Test results

Automated

  • npm test — 173/173 pass

Docker build

  • Build succeeds (pragma comment fix unblocks the inline Python)
  • Both providers (nvidia, inference) baked into openclaw.json
  • openclaw.json is root:root 444
  • All 10 symlinks present (agents, extensions, workspace, skills, hooks, identity, devices, canvas, cron, update-check.json)
  • Writable dirs confirmed: identity, devices, canvas, cron

Security verification

  • echo >> ~/.openclaw/openclaw.json fails with permission denied from inside sandbox
  • openclaw models set fails with EACCES from inside sandbox (writes to openclaw.json)
  • openclaw.json hash unchanged after onboard (no runtime writes)

Host-side model routing

  • openshell inference set switches model from host without sandbox involvement
  • Switched model routes correctly inside sandbox (gateway rewrites requests)
  • Second model switch works dynamically (no sandbox restart needed)
  • Brand-new model (not in Dockerfile or onboard picker) works without image rebuild
  • Full onboard with non-default model selection completes successfully

Summary by CodeRabbit

  • Chores
    • Restructured initialization and model configuration to occur at Docker build time rather than runtime startup, reducing startup overhead.
    • Simplified the application startup entrypoint by removing redundant initialization steps.
    • Updated model routing configuration defaults and provider mapping structure.

franknvda1 and others added 2 commits March 21, 2026 08:17
…lockdown

- Bake both 'nvidia' and 'inference' providers into openclaw.json at
  image build time; remove runtime Python config-patching from
  buildSandboxConfigSyncScript (writes to locked root:root 444 file)
- Use `openclaw models set` for runtime model selection (writes to
  writable agent config in .openclaw-data/)
- Add identity/, devices/, canvas/, cron/ to .openclaw-data symlinks
  so the gateway can write device-auth.json at runtime
- Remove dead `openclaw doctor --fix` and `openclaw plugins install`
  calls from nemoclaw-start.sh (already ran at build time, fail with
  EPERM at runtime)

Caused-by: 2d3f84e (fix: lock gateway config via Landlock filesystem policy)
Fixes #514
The # pragma: allowlist secret comments inside the multi-line python3 -c
string cause Python to treat everything after # as a comment, swallowing
the \ line continuation and closing braces. This results in:
  SyntaxError: '{' was never closed

Reported by DanTup in PR #570.
@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 212614e0-2dfe-4d0e-95cb-13457a37f0f6

📥 Commits

Reviewing files that changed from the base of the PR and between a7102a4 and 5695b93.

📒 Files selected for processing (4)
  • Dockerfile
  • bin/lib/onboard.js
  • scripts/nemoclaw-start.sh
  • test/onboard.test.js

📝 Walkthrough

Walkthrough

The changes refactor OpenClaw configuration initialization from runtime to build-time, moving configuration logic from startup scripts and onboarding into the Dockerfile. Runtime scripts now expect .openclaw/openclaw.json to be pre-configured and immutable, with state directories symlinked to writable locations, while model routing defaults to the inference provider.

Changes

Cohort / File(s) Summary
Docker Image Configuration
Dockerfile
Adds writable state directories (identity, devices, canvas, cron) under /sandbox/.openclaw-data with symlinks into /sandbox/.openclaw. Provisions update-check.json as writable state. Updates openclaw.json generation to set default agent model primary to inference/{model} and expands models.providers to include both inference and nvidia provider configurations.
Onboarding Logic Simplification
bin/lib/onboard.js
Removes dependency on DEFAULT_OLLAMA_MODEL and getOpenClawPrimaryModel. Simplifies buildSandboxConfigSyncScript to only write ~/.nemoclaw/config.json, eliminating the Python block that previously modified ~/.openclaw/openclaw.json, injected provider configurations, and executed openclaw models set at runtime.
Startup Script
scripts/nemoclaw-start.sh
Removes execution of openclaw doctor --fix and openclaw plugins install /opt/nemoclaw, replacing them with inline comments indicating these operations occur at Docker build time.
Test Updates
test/onboard.test.js
Updates buildSandboxConfigSyncScript test assertions to verify the script only writes NemoClaw selection configuration and does not contain references to openclaw.json or openclaw models set, removing previous assertions about OpenClaw config modifications.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 We've shuffled the timing with care and with grace,
Build-time configuration takes first-to-the-race,
State stays writable, configs locked tight,
The sandbox runs safer—security's right! ✨
No runtime rewrites of paths that we guard,
Just symlinks and symlinks, our new safety card! 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving OpenClaw configuration to build time and making openclaw.json immutable at runtime.
Linked Issues check ✅ Passed The PR addresses both linked issues: #580 by removing failing runtime config operations, and #514 by making openclaw.json immutable through build-time configuration and Dockerfile changes.
Out of Scope Changes check ✅ Passed All changes directly support the core objectives of making openclaw.json immutable and moving configuration to build time, with no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/immutable-openclaw-config

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread Dockerfile
'api': 'openai-completions', \
'models': [{'id': model.split('/')[-1], 'name': model, 'reasoning': False, 'input': ['text'], 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': 131072, 'maxTokens': 4096}] \
}}}, \
'agents': {'defaults': {'model': {'primary': f'inference/{model}'}}}, \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part is where I'm concerned. We need to have all the inference choices in BEFORE we configure the immutable file OR we have to have a way to update the config from outside the sandbox (this is more likely what we need)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the model inference choices have to be handled from the outside the sandbox via openshell with initial versions baked in before we configure immutability - going to add additional tests and affordances for that.

@EltronAI EltronAI Mar 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ericksoa and @kjw3, since openclaw.json is now immutable at runtime in the sandbox, what is the recommended way to update config settings and override skill configurations if defaults are baked in at build time? openclaw config set commands can no longer update in sandbox. Clarifying this would help users safely manage OpenClaw config settings and skills under the new sandbox model.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @EltronAI we are looking at ways to implement this in a way that is consistent with the policy engine in openshell, specifically allowing more granular control of what can change. For example, there maybe configurations where the policy engine will forbid changes to skills unless certain guidelines are met, or forbid changes to system prompt, and others where those changes are 100% fine.

Some of these are design details we are still working out, and hope to support very soon.

Appreciate the feedback!

openclaw models set writes to openclaw.json, which is correctly locked
(root:root 444 + Landlock read-only). Model routing is handled by the
host-side gateway via openshell inference set (Step 5), not from inside
the sandbox. The sync script should only write NemoClaw's own selection
config to ~/.nemoclaw/config.json.

Remove openclaw models set call, dead pythonLiteralJson helper, and
unused getOpenClawPrimaryModel/DEFAULT_OLLAMA_MODEL imports.
@ericksoa ericksoa marked this pull request as ready for review March 21, 2026 16:16
@kjw3

kjw3 commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Re-review summary: this looks good to merge.

Review take:

  • The PR addresses a real regression introduced by making openclaw.json immutable after the earlier security hardening.
  • The fix direction is correct: stop sandbox-side writes to ~/.openclaw/openclaw.json, rely on host-side inference routing, and keep the sandbox sync step limited to NemoClaw-owned config.
  • The patch is small, coherent, and aligned with the current security model. I did not see any new security, CI, or supply-chain concerns in the changed files.

Validation:

  • Static/code review: approve
  • Unit coverage: test/onboard.test.js covers the key behavior change and passed in review
  • Manual runtime validation completed by @kjw3 on all three target environments: macOS, Brev, and Spark
  • Result: patch behaved correctly across those environments

From my side, this is ready to merge once final approval is in place. Let it rip 🤙

@kjw3 kjw3 self-assigned this Mar 21, 2026
@kjw3

kjw3 commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Thanks for carrying this through and for fixing the remaining gap.

Also want to explicitly credit @franknvda1 here: the core direction came from #570, and this PR cleanly finishes that work by removing the remaining sandbox-side openclaw.json mutation and tightening the immutable-config path.

Let it rip 🤙

@kjw3 kjw3 merged commit 0f8eedd into main Mar 21, 2026
3 checks passed
@kjw3 kjw3 deleted the fix/immutable-openclaw-config branch March 21, 2026 16:43
@DanTup

DanTup commented Mar 21, 2026

Copy link
Copy Markdown

ericksoa added a commit that referenced this pull request Mar 21, 2026
OpenClaw's Discord integration fails when it tries to write a
user-provided bot token to openclaw.json, which is now immutable
(root:root 444, Landlock read-only) after #588.

OpenClaw already supports reading DISCORD_BOT_TOKEN and
SLACK_BOT_TOKEN from environment variables, so we pass them through
at sandbox creation time — the same pattern used for NVIDIA_API_KEY
and TELEGRAM_BOT_TOKEN.

Also disables channels.defaults.configWrites in the build-time config
to prevent any channel plugin from attempting runtime writes to the
immutable config file.

Closes #599
kjw3 pushed a commit that referenced this pull request Mar 21, 2026
* docs: add community feedback invitation for policy presets

* docs: link baseline policy reference to the YAML file on GitHub

* fix: pass Discord and Slack bot tokens via env vars into sandbox

OpenClaw's Discord integration fails when it tries to write a
user-provided bot token to openclaw.json, which is now immutable
(root:root 444, Landlock read-only) after #588.

OpenClaw already supports reading DISCORD_BOT_TOKEN and
SLACK_BOT_TOKEN from environment variables, so we pass them through
at sandbox creation time — the same pattern used for NVIDIA_API_KEY
and TELEGRAM_BOT_TOKEN.

Also disables channels.defaults.configWrites in the build-time config
to prevent any channel plugin from attempting runtime writes to the
immutable config file.

Closes #599

* fix: add Discord endpoints to default sandbox policy

Add discord.com, gateway.discord.gg, and cdn.discordapp.com to the
baseline sandbox policy so Discord integration works without needing
the preset applied separately.
kagura-agent pushed a commit to kagura-agent/NemoClaw that referenced this pull request Mar 22, 2026
…fixes NVIDIA#628)

When users select Local Ollama during onboarding, the sandbox's
openclaw.json still showed the default cloud model because the
Dockerfile was built before the model was selected.

Fix: reorder the onboarding flow so inference provider/model
selection (Step 3) happens before sandbox creation (Step 4).
The selected model is then patched into the Dockerfile's
ARG NEMOCLAW_MODEL default before the image build, ensuring
openclaw.json is baked with the correct model at build time.

This preserves the immutable config design from PR NVIDIA#588 —
openclaw.json is still root:root 444 and never modified at
runtime. The fix operates entirely at build time.

Changes:
- Rename setupNim() → selectInference() (no sandbox dependency)
- Add patchDockerfileModel() to rewrite ARG NEMOCLAW_MODEL
- Defer NIM container start to after sandbox creation
- Move registry.updateSandbox() to main onboard() flow
- Add tests for Dockerfile patching
kagura-agent pushed a commit to kagura-agent/NemoClaw that referenced this pull request Mar 22, 2026
…ixes NVIDIA#606)

openclaw.json is locked (root:root 444) at build time to prevent agent
tampering (NVIDIA#514, NVIDIA#588).  However, users legitimately need to modify
config at runtime — e.g. running `openclaw onboard` to add a Discord
bot token.  The atomic write (tmp → copyfile → rename) in OpenClaw's
config writer fails with EACCES against the immutable file.

PR NVIDIA#601 addressed the env-var path (passing DISCORD_BOT_TOKEN into the
sandbox), but the underlying issue remains: any `openclaw onboard` or
`/config` write inside the sandbox hits the same EACCES error.

Fix: at sandbox startup, copy the immutable openclaw.json to the
writable state directory (~/.openclaw-data/) and set
OPENCLAW_CONFIG_PATH to redirect all OpenClaw config reads/writes to
the copy.  The original immutable file stays intact as a read-only
reference; the Landlock policy on /sandbox/.openclaw continues to
protect it.

Changes:
- nemoclaw-start.sh: add prepare_writable_config() that copies the
  locked config to ~/.openclaw-data/openclaw.json and exports
  OPENCLAW_CONFIG_PATH; update print_dashboard_urls to respect the
  env var
- e2e-test.sh: add test 11 verifying writable overlay works and
  immutable original stays untouched
Ryuketsukami pushed a commit to Ryuketsukami/NemoClaw that referenced this pull request Mar 24, 2026
…me (NVIDIA#588)

* fix: resolve openclaw.json permissions conflict and scope Dockerfile lockdown

- Bake both 'nvidia' and 'inference' providers into openclaw.json at
  image build time; remove runtime Python config-patching from
  buildSandboxConfigSyncScript (writes to locked root:root 444 file)
- Use `openclaw models set` for runtime model selection (writes to
  writable agent config in .openclaw-data/)
- Add identity/, devices/, canvas/, cron/ to .openclaw-data symlinks
  so the gateway can write device-auth.json at runtime
- Remove dead `openclaw doctor --fix` and `openclaw plugins install`
  calls from nemoclaw-start.sh (already ran at build time, fail with
  EPERM at runtime)

Caused-by: 2d3f84e (fix: lock gateway config via Landlock filesystem policy)
Fixes NVIDIA#514

* fix: remove pragma comments that break inline Python in Dockerfile

The # pragma: allowlist secret comments inside the multi-line python3 -c
string cause Python to treat everything after # as a comment, swallowing
the \ line continuation and closing braces. This results in:
  SyntaxError: '{' was never closed

Reported by DanTup in PR NVIDIA#570.

* fix: remove openclaw models set from sync script — config stays on host

openclaw models set writes to openclaw.json, which is correctly locked
(root:root 444 + Landlock read-only). Model routing is handled by the
host-side gateway via openshell inference set (Step 5), not from inside
the sandbox. The sync script should only write NemoClaw's own selection
config to ~/.nemoclaw/config.json.

Remove openclaw models set call, dead pythonLiteralJson helper, and
unused getOpenClawPrimaryModel/DEFAULT_OLLAMA_MODEL imports.

---------

Co-authored-by: Frank Ruiz <frankr@nvidia.com>
Ryuketsukami pushed a commit to Ryuketsukami/NemoClaw that referenced this pull request Mar 24, 2026
…DIA#601)

* docs: add community feedback invitation for policy presets

* docs: link baseline policy reference to the YAML file on GitHub

* fix: pass Discord and Slack bot tokens via env vars into sandbox

OpenClaw's Discord integration fails when it tries to write a
user-provided bot token to openclaw.json, which is now immutable
(root:root 444, Landlock read-only) after NVIDIA#588.

OpenClaw already supports reading DISCORD_BOT_TOKEN and
SLACK_BOT_TOKEN from environment variables, so we pass them through
at sandbox creation time — the same pattern used for NVIDIA_API_KEY
and TELEGRAM_BOT_TOKEN.

Also disables channels.defaults.configWrites in the build-time config
to prevent any channel plugin from attempting runtime writes to the
immutable config file.

Closes NVIDIA#599

* fix: add Discord endpoints to default sandbox policy

Add discord.com, gateway.discord.gg, and cdn.discordapp.com to the
baseline sandbox policy so Discord integration works without needing
the preset applied separately.
jessesanford pushed a commit to jessesanford/NemoClaw that referenced this pull request Mar 24, 2026
…me (NVIDIA#588)

* fix: resolve openclaw.json permissions conflict and scope Dockerfile lockdown

- Bake both 'nvidia' and 'inference' providers into openclaw.json at
  image build time; remove runtime Python config-patching from
  buildSandboxConfigSyncScript (writes to locked root:root 444 file)
- Use `openclaw models set` for runtime model selection (writes to
  writable agent config in .openclaw-data/)
- Add identity/, devices/, canvas/, cron/ to .openclaw-data symlinks
  so the gateway can write device-auth.json at runtime
- Remove dead `openclaw doctor --fix` and `openclaw plugins install`
  calls from nemoclaw-start.sh (already ran at build time, fail with
  EPERM at runtime)

Caused-by: 2d3f84e (fix: lock gateway config via Landlock filesystem policy)
Fixes NVIDIA#514

* fix: remove pragma comments that break inline Python in Dockerfile

The # pragma: allowlist secret comments inside the multi-line python3 -c
string cause Python to treat everything after # as a comment, swallowing
the \ line continuation and closing braces. This results in:
  SyntaxError: '{' was never closed

Reported by DanTup in PR NVIDIA#570.

* fix: remove openclaw models set from sync script — config stays on host

openclaw models set writes to openclaw.json, which is correctly locked
(root:root 444 + Landlock read-only). Model routing is handled by the
host-side gateway via openshell inference set (Step 5), not from inside
the sandbox. The sync script should only write NemoClaw's own selection
config to ~/.nemoclaw/config.json.

Remove openclaw models set call, dead pythonLiteralJson helper, and
unused getOpenClawPrimaryModel/DEFAULT_OLLAMA_MODEL imports.

---------

Co-authored-by: Frank Ruiz <frankr@nvidia.com>
jessesanford pushed a commit to jessesanford/NemoClaw that referenced this pull request Mar 24, 2026
…DIA#601)

* docs: add community feedback invitation for policy presets

* docs: link baseline policy reference to the YAML file on GitHub

* fix: pass Discord and Slack bot tokens via env vars into sandbox

OpenClaw's Discord integration fails when it tries to write a
user-provided bot token to openclaw.json, which is now immutable
(root:root 444, Landlock read-only) after NVIDIA#588.

OpenClaw already supports reading DISCORD_BOT_TOKEN and
SLACK_BOT_TOKEN from environment variables, so we pass them through
at sandbox creation time — the same pattern used for NVIDIA_API_KEY
and TELEGRAM_BOT_TOKEN.

Also disables channels.defaults.configWrites in the build-time config
to prevent any channel plugin from attempting runtime writes to the
immutable config file.

Closes NVIDIA#599

* fix: add Discord endpoints to default sandbox policy

Add discord.com, gateway.discord.gg, and cdn.discordapp.com to the
baseline sandbox policy so Discord integration works without needing
the preset applied separately.
@wscurran wscurran added the bug-fix PR fixes a bug or regression label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR fixes a bug or regression

Projects

None yet

6 participants