Skip to content

fix(sandbox): add missing exec-approvals.json symlink#1236

Open
TonyLuo-NV wants to merge 2 commits intoNVIDIA:mainfrom
TonyLuo-NV:fix/1027-exec-approvals-symlink
Open

fix(sandbox): add missing exec-approvals.json symlink#1236
TonyLuo-NV wants to merge 2 commits intoNVIDIA:mainfrom
TonyLuo-NV:fix/1027-exec-approvals-symlink

Conversation

@TonyLuo-NV
Copy link
Copy Markdown

@TonyLuo-NV TonyLuo-NV commented Apr 1, 2026

Summary

Fixes #1027GatewayRequestError: EACCES permission denied, open '/sandbox/.openclaw/exec-approvals.json'

Test plan

  • npm test — 696 tests passed (cli + plugin projects)
  • Full sandbox repro: built main branch image → confirmed exec-approvals.json missing and EACCES in dashboard → rebuilt with fix → confirmed symlink present and read/write works
  • Verified openclaw.json stays root:root 444 (no Agent can self-modify openclaw.json to bypass auth and CORS controls #514 regression)
  • Verified .config-hash stays root:root 444 (integrity check preserved)
  • Verified .openclaw/ directory stays root:root 755 (agent cannot create files or replace symlinks)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation

    • Clarified security notes: config files are protected while legitimate runtime writes occur to a separate writable state area; updated threat reference.
  • Chores

    • Added a dedicated writable runtime approvals state file and a symlink exposing it under the protected config directory.
  • Tests

    • Expanded isolation and provisioning tests to verify ownership/permissions, symlink-backed state resolution, and read/write behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

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: 04c4854a-d0d1-4eb2-b7d1-8ddb8ce892fb

📥 Commits

Reviewing files that changed from the base of the PR and between a1d8a94 and 6d015fb.

📒 Files selected for processing (3)
  • Dockerfile
  • test/e2e-gateway-isolation.sh
  • test/sandbox-provisioning.test.js
✅ Files skipped from review due to trivial changes (1)
  • Dockerfile
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/e2e-gateway-isolation.sh
  • test/sandbox-provisioning.test.js

📝 Walkthrough

Walkthrough

Adds a writable sandbox state file and symlink under .openclaw-data, updates Dockerfile comments to reflect runtime-modifiable config and DAC guidance, and expands tests to assert ownership/permissions and symlink targets for .openclaw and .openclaw-data entries.

Changes

Cohort / File(s) Summary
Dockerfile (comments)
Dockerfile
Updated comment block: acknowledges /sandbox/.openclaw/openclaw.json may be modified at runtime by the gateway, broadens DAC hardening guidance to protect the .openclaw directory/config files while runtime state lives in .openclaw-data, and updates issue reference from #514 to #1027. No executable changes.
Sandbox state provisioning
Dockerfile.base
Adds writable state file /sandbox/.openclaw-data/exec-approvals.json initialized to {} and creates symlink ln -s /sandbox/.openclaw-data/exec-approvals.json /sandbox/.openclaw/exec-approvals.json; included in existing chown -R sandbox:sandbox /sandbox/.openclaw /sandbox/.openclaw-data.
Shell e2e tests
test/e2e-gateway-isolation.sh
Replaces prior "sandbox cannot write" touch/echo checks with explicit stat assertions that /sandbox/.openclaw/openclaw.json and /sandbox/.openclaw/.config-hash are root:root mode 444; extends symlink target checks to include memory, update-check.json, and exec-approvals.json; adds tests to read and write exec-approvals.json via symlink, conditional check for openclaw.json.bak ownership, and asserts /sandbox/.openclaw is root:root mode 755.
Unit/regression tests
test/sandbox-provisioning.test.js
New Vitest file validating Dockerfile.base creates backing data file for exec-approvals.json and then the symlink, asserts symlink presence for update-check.json and others, and checks Dockerfile contains commands to set /sandbox/.openclaw/openclaw.json and /sandbox/.openclaw/.config-hash to chmod 444 and chown root:root, and /sandbox/.openclaw to chown root:root + chmod 755.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nested files where roots can’t pry,
A tiny symlink where permissions lie,
I guard the hashes, tuck approvals near,
Tests hop the paths and check what's clear,
The sandbox hums, secure and sly.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(sandbox): add missing exec-approvals.json symlink' clearly and specifically summarizes the main change—adding a missing symlink for exec-approvals.json in the sandbox provisioning.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/sandbox-provisioning.test.js (1)

68-83: Consider extracting fs.readFileSync to reduce redundant I/O.

The Dockerfile content is read separately in each parameterized test iteration. While this works correctly, reading once at describe-block scope would be cleaner and slightly faster.

♻️ Optional: Extract file read to describe scope
 describe("sandbox provisioning: all expected symlinks present in Dockerfile.base", () => {
   const EXPECTED_DIR_SYMLINKS = [
     "agents", "extensions", "workspace", "skills", "hooks",
     "identity", "devices", "canvas", "cron", "memory",
   ];
   const EXPECTED_FILE_SYMLINKS = ["update-check.json", "exec-approvals.json"];
+  const dockerfileBaseSrc = fs.readFileSync(DOCKERFILE_BASE, "utf-8");

   it.each(EXPECTED_DIR_SYMLINKS)("symlink for directory '%s' is created", (name) => {
-    const src = fs.readFileSync(DOCKERFILE_BASE, "utf-8");
-    expect(src).toContain(`ln -s /sandbox/.openclaw-data/${name} /sandbox/.openclaw/${name}`);
+    expect(dockerfileBaseSrc).toContain(`ln -s /sandbox/.openclaw-data/${name} /sandbox/.openclaw/${name}`);
   });

   it.each(EXPECTED_FILE_SYMLINKS)("symlink for file '%s' is created", (name) => {
-    const src = fs.readFileSync(DOCKERFILE_BASE, "utf-8");
-    expect(src).toContain(`ln -s /sandbox/.openclaw-data/${name} /sandbox/.openclaw/${name}`);
+    expect(dockerfileBaseSrc).toContain(`ln -s /sandbox/.openclaw-data/${name} /sandbox/.openclaw/${name}`);
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/sandbox-provisioning.test.js` around lines 68 - 83, Move the
fs.readFileSync(DOCKERFILE_BASE, "utf-8") call out of each it.each and into the
surrounding describe scope so the Dockerfile contents are read once; create a
local const (e.g. dockerfileSrc) in the describe block and replace the per-test
reads in the it.each blocks that reference EXPECTED_DIR_SYMLINKS and
EXPECTED_FILE_SYMLINKS to use dockerfileSrc instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/sandbox-provisioning.test.js`:
- Around line 68-83: Move the fs.readFileSync(DOCKERFILE_BASE, "utf-8") call out
of each it.each and into the surrounding describe scope so the Dockerfile
contents are read once; create a local const (e.g. dockerfileSrc) in the
describe block and replace the per-test reads in the it.each blocks that
reference EXPECTED_DIR_SYMLINKS and EXPECTED_FILE_SYMLINKS to use dockerfileSrc
instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d7b6ef09-ce6d-4b06-a7e3-0ab5405c0f28

📥 Commits

Reviewing files that changed from the base of the PR and between e06c147 and e4adb21.

📒 Files selected for processing (4)
  • Dockerfile
  • Dockerfile.base
  • test/e2e-gateway-isolation.sh
  • test/sandbox-provisioning.test.js

@TonyLuo-NV TonyLuo-NV force-pushed the fix/1027-exec-approvals-symlink branch from e4adb21 to 8edce8e Compare April 1, 2026 07:39
@TonyLuo-NV
Copy link
Copy Markdown
Author

CI failure analysis: test-e2e-gateway-isolation

The exec-approvals.json symlink test (test 9) fails because the CI uses the cached GHCR base image (ghcr.io/nvidia/nemoclaw/sandbox-base:latest) which does not contain the Dockerfile.base changes from this PR.

Root cause: .github/actions/resolve-sandbox-base-image/action.yaml always pulls the GHCR cached base image when available, without checking whether Dockerfile.base was modified in the PR. The base image is only rebuilt on main merges via base-image.yaml.

Suggested fix for the CI action: detect Dockerfile.base changes in the PR (e.g., git diff --name-only origin/main...HEAD -- Dockerfile.base) and force a local build when it has been modified. This would be an isolated change to .github/actions/resolve-sandbox-base-image/action.yaml.

The dco-check failure is expected — sign-off will be added after QA review.

@wscurran wscurran added the bug Something isn't working label Apr 1, 2026
@TonyLuo-NV TonyLuo-NV assigned TonyLuo-NV and unassigned TonyLuo-NV Apr 2, 2026
The OpenClaw gateway failed with EACCES when writing exec-approvals.json
because no symlink existed from .openclaw/ to the writable .openclaw-data/
directory. Add the symlink and backing file in Dockerfile.base so the
sandbox user can read/write exec approvals through the two-layer design.

Keep openclaw.json and .config-hash as root:root 444 to preserve the
NVIDIA#514 security mitigation (prevent agent from tampering with auth/CORS).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/e2e-gateway-isolation.sh (1)

193-206: Consider unifying the assertion style with Tests 2 and 5.

Test 9d splits the stat output with awk for separate owner/permissions checks, while Tests 2 and 5 use exact string matching. Both approaches are correct, but unifying them would improve maintainability.

♻️ Optional: unified approach matching Tests 2/5
 info "9d. /sandbox/.openclaw/ directory is owned by root:root with permissions 755"
 OUT=$(run_as_root "stat -c '%U:%G %a' /sandbox/.openclaw")
-OWNER=$(echo "$OUT" | awk '{print $1}')
-DIR_PERMS=$(echo "$OUT" | awk '{print $2}')
-if [ "$OWNER" = "root:root" ]; then
-  pass ".openclaw directory owned by root:root"
+if [ "$OUT" = "root:root 755" ]; then
+  pass ".openclaw directory is root:root 755"
 else
-  fail ".openclaw directory owner wrong: $OWNER (expected root:root)"
-fi
-if [ "$DIR_PERMS" = "755" ]; then
-  pass ".openclaw directory permissions are 755"
-else
-  fail ".openclaw directory permissions wrong: $DIR_PERMS (expected 755)"
+  fail ".openclaw directory wrong: $OUT (expected root:root 755)"
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e-gateway-isolation.sh` around lines 193 - 206, Unify Test 9d to use
an exact string match like Tests 2 and 5 by comparing the full stat output
stored in OUT (from run_as_root "stat -c '%U:%G %a' /sandbox/.openclaw") against
an expected string "root:root 755"; replace the separate OWNER/DIR_PERMS awk
parsing and two checks with a single if [ "$OUT" = "root:root 755" ] then pass
".openclaw owned and perms OK" else fail ".openclaw owner/perms wrong: $OUT
(expected root:root 755)" fi, updating the pass/fail messages and keeping
references to OUT, run_as_root, pass, and fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Dockerfile`:
- Around line 80-82: Update the misleading Dockerfile comment: clarify that
openclaw.json is immutable at runtime (root:root 444) and cannot be modified by
the gateway process, and state that runtime exec approvals are stored in
exec-approvals.json (symlinked) rather than openclaw.json; keep the note about
Landlock/root-owned directory as the primary security boundary but remove the
claim that the gateway updates openclaw.json for exec approvals.

---

Nitpick comments:
In `@test/e2e-gateway-isolation.sh`:
- Around line 193-206: Unify Test 9d to use an exact string match like Tests 2
and 5 by comparing the full stat output stored in OUT (from run_as_root "stat -c
'%U:%G %a' /sandbox/.openclaw") against an expected string "root:root 755";
replace the separate OWNER/DIR_PERMS awk parsing and two checks with a single if
[ "$OUT" = "root:root 755" ] then pass ".openclaw owned and perms OK" else fail
".openclaw owner/perms wrong: $OUT (expected root:root 755)" fi, updating the
pass/fail messages and keeping references to OUT, run_as_root, pass, and fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6bc6fb3d-9e3b-49e1-8856-70280dc9c8fc

📥 Commits

Reviewing files that changed from the base of the PR and between e4adb21 and 648be6c.

📒 Files selected for processing (4)
  • Dockerfile
  • Dockerfile.base
  • test/e2e-gateway-isolation.sh
  • test/sandbox-provisioning.test.js
✅ Files skipped from review due to trivial changes (1)
  • Dockerfile.base
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/sandbox-provisioning.test.js

Comment thread Dockerfile Outdated
@TonyLuo-NV TonyLuo-NV force-pushed the fix/1027-exec-approvals-symlink branch from 648be6c to a1d8a94 Compare April 2, 2026 09:19
- Correct misleading Dockerfile comment: openclaw.json is immutable at
  runtime (root:root 444), not updated by the gateway. Exec approvals
  live in exec-approvals.json via symlink.
- Extract fs.readFileSync to describe-block scope in sandbox-provisioning
  tests to eliminate redundant I/O per test iteration.
- Unify assertion style in e2e-gateway-isolation.sh Test 9d to use exact
  string matching consistent with Tests 2 and 5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wscurran
Copy link
Copy Markdown
Contributor

Thanks for the exec-approvals symlink fix. The codebase has changed significantly since April 1 — including a full TypeScript migration — so this will need a rebase on origin/main before we can review it. Please rebase and resolve any conflicts, and we'll take a look.

@wscurran wscurran added the status: rebase PR needs to be rebased against main before review can continue label Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working status: rebase PR needs to be rebased against main before review can continue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenClaw GatewayRequestError — EACCES permission denied

2 participants