From 4a827431dcaa5ec2fdf4d5cb56f35fced4faeb25 Mon Sep 17 00:00:00 2001 From: Kagura Chen Date: Sun, 22 Mar 2026 20:12:04 +0800 Subject: [PATCH] fix(sandbox): allow openclaw.json writes for channel configuration (fixes #606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openclaw.json is locked (root:root 444) at build time to prevent agent tampering (#514, #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 #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 --- scripts/nemoclaw-start.sh | 24 +++++++++++++++++++- test/e2e-test.sh | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index d28b96374..5817763c3 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -43,7 +43,7 @@ print_dashboard_urls() { token="$(python3 - <<'PYTOKEN' import json import os -path = os.path.expanduser('~/.openclaw/openclaw.json') +path = os.environ.get('OPENCLAW_CONFIG_PATH') or os.path.expanduser('~/.openclaw/openclaw.json') try: cfg = json.load(open(path)) except Exception: @@ -126,10 +126,32 @@ PYAUTOPAIR echo "[gateway] auto-pair watcher launched (pid $!)" } +prepare_writable_config() { + # openclaw.json is baked at build time and locked (root:root 444) to prevent + # agent tampering. However, the user may legitimately need to modify config + # at runtime (e.g. `openclaw onboard` to add a Discord token — see #606). + # + # Solution: copy the immutable config to the writable state directory and + # redirect OpenClaw via OPENCLAW_CONFIG_PATH. The original file stays + # intact as a read-only reference; runtime writes go to the copy. + local immutable_cfg="${HOME}/.openclaw/openclaw.json" + local writable_cfg="${HOME}/.openclaw-data/openclaw.json" + + if [ ! -f "$writable_cfg" ] && [ -f "$immutable_cfg" ]; then + cp "$immutable_cfg" "$writable_cfg" + chmod 600 "$writable_cfg" + fi + + if [ -f "$writable_cfg" ]; then + export OPENCLAW_CONFIG_PATH="$writable_cfg" + fi +} + echo 'Setting up NemoClaw...' # openclaw doctor --fix and openclaw plugins install already ran at build time # (Dockerfile Step 28). At runtime they fail with EPERM against the locked # /sandbox/.openclaw directory and accomplish nothing. +prepare_writable_config write_auth_profile if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then diff --git a/test/e2e-test.sh b/test/e2e-test.sh index 2ec70331b..70c77daf2 100755 --- a/test/e2e-test.sh +++ b/test/e2e-test.sh @@ -241,6 +241,52 @@ console.assert(state.lastAction === null, 'Should be cleared'); console.log('State management: create, save, load, clear all working'); " && pass "NemoClaw state management works" || fail "State management broken" +# ------------------------------------------------------- +info "11. Verify writable config overlay for runtime writes (#606)" +# ------------------------------------------------------- +python3 -c " +import json, os, shutil + +home = os.environ.get('HOME', '/sandbox') +immutable = os.path.join(home, '.openclaw', 'openclaw.json') +writable = os.path.join(home, '.openclaw-data', 'openclaw.json') + +# Clean up any prior writable copy +if os.path.exists(writable): + os.remove(writable) + +# Simulate what prepare_writable_config does +assert os.path.isfile(immutable), f'Immutable config missing: {immutable}' +shutil.copy2(immutable, writable) +os.chmod(writable, 0o600) + +# Verify the copy is writable by sandbox user +with open(writable) as f: + cfg = json.load(f) + +# Write a channel token to the writable copy +cfg.setdefault('channels', {}).setdefault('discord', {})['botToken'] = 'test-token-606' +with open(writable, 'w') as f: + json.dump(cfg, f, indent=2) + +# Verify the write succeeded +with open(writable) as f: + updated = json.load(f) +assert updated['channels']['discord']['botToken'] == 'test-token-606', 'Token write failed' + +# Verify the immutable original is unchanged +with open(immutable) as f: + original = json.load(f) +assert 'discord' not in original.get('channels', {}), 'Immutable config was modified!' + +# Verify OPENCLAW_CONFIG_PATH would point to writable copy +print(f'Writable config at: {writable}') +print(f'Immutable config untouched: {immutable}') + +# Clean up +os.remove(writable) +" && pass "Writable config overlay allows runtime writes without modifying immutable config" || fail "Writable config overlay failed" + echo "" echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} ALL E2E TESTS PASSED${NC}"