Skip to content
Closed
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
24 changes: 23 additions & 1 deletion scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Comment on lines +140 to +143
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing parent directory creation for writable config.

The cp command on line 141 will fail with "No such file or directory" if ~/.openclaw-data/ doesn't exist. Add directory creation before copying.

🛠️ Proposed fix
   if [ ! -f "$writable_cfg" ] && [ -f "$immutable_cfg" ]; then
+    mkdir -p "$(dirname "$writable_cfg")"
     cp "$immutable_cfg" "$writable_cfg"
     chmod 600 "$writable_cfg"
   fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [ ! -f "$writable_cfg" ] && [ -f "$immutable_cfg" ]; then
cp "$immutable_cfg" "$writable_cfg"
chmod 600 "$writable_cfg"
fi
if [ ! -f "$writable_cfg" ] && [ -f "$immutable_cfg" ]; then
mkdir -p "$(dirname "$writable_cfg")"
cp "$immutable_cfg" "$writable_cfg"
chmod 600 "$writable_cfg"
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/nemoclaw-start.sh` around lines 140 - 143, The copy can fail if the
parent directory of the target writable config doesn't exist; before copying
immutable_cfg to writable_cfg add creation of the parent directory for
writable_cfg (use mkdir -p on the dirname of the writable_cfg variable) so the
cp will succeed, then proceed to cp "$immutable_cfg" "$writable_cfg" and chmod
600 "$writable_cfg"; ensure the mkdir is no-op if the directory already exists
and occurs only when writable_cfg is missing.


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
Expand Down
46 changes: 46 additions & 0 deletions test/e2e-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +258 to +261
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for creation of .openclaw-data directory
rg -n '\.openclaw-data' --type sh --type py

Repository: NVIDIA/NemoClaw

Length of output: 235


🏁 Script executed:

# Search for any mkdir/makedirs operations
rg -n '(mkdir|makedirs)' --type sh --type py

Repository: NVIDIA/NemoClaw

Length of output: 1597


🏁 Script executed:

# Look at prepare_writable_config function in nemoclaw-start.sh
sed -n '130,150p' scripts/nemoclaw-start.sh

Repository: NVIDIA/NemoClaw

Length of output: 889


🏁 Script executed:

# Examine the complete test setup around lines 244-288
sed -n '244,288p' test/e2e-test.sh

Repository: NVIDIA/NemoClaw

Length of output: 1692


🏁 Script executed:

# Search for test setup/initialization
sed -n '1,243p' test/e2e-test.sh | grep -n -A2 -B2 "openclaw-data\|mkdirSync\|makedirs"

Repository: NVIDIA/NemoClaw

Length of output: 368


🏁 Script executed:

# Check if there's a setup.sh or fixture that creates directories
git ls-files | grep -E "(setup|fixture|init)" | head -20

Repository: NVIDIA/NemoClaw

Length of output: 392


Missing directory creation before copying config.

The test and prepare_writable_config() in nemoclaw-start.sh both assume ~/.openclaw-data/ exists. The .openclaw directory is created in test setup (line 196), but .openclaw-data is never created anywhere. When shutil.copy2() executes without the parent directory, it will fail.

🛠️ Proposed fix for the test
 # Simulate what prepare_writable_config does
 assert os.path.isfile(immutable), f'Immutable config missing: {immutable}'
+os.makedirs(os.path.dirname(writable), exist_ok=True)
 shutil.copy2(immutable, writable)
 os.chmod(writable, 0o600)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Simulate what prepare_writable_config does
assert os.path.isfile(immutable), f'Immutable config missing: {immutable}'
shutil.copy2(immutable, writable)
os.chmod(writable, 0o600)
# Simulate what prepare_writable_config does
assert os.path.isfile(immutable), f'Immutable config missing: {immutable}'
os.makedirs(os.path.dirname(writable), exist_ok=True)
shutil.copy2(immutable, writable)
os.chmod(writable, 0o600)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e-test.sh` around lines 258 - 261, The test copies an immutable config
to writable using shutil.copy2(immutable, writable) but never ensures the parent
directory (~/.openclaw-data) exists, causing copy to fail; update the test
before the copy (around the prepare_writable_config() simulation) to create the
writable file's parent directory (os.path.dirname(writable) or the
~/.openclaw-data path) if it doesn't exist (mkdir -p semantics) and set proper
permissions, then proceed to assert the immutable exists and copy via
shutil.copy2; this mirrors prepare_writable_config() behavior and prevents the
missing-directory error for variables immutable and writable.


# 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}"
Expand Down