Skip to content

fix(scripts): restore .openclaw perms after nemoclaw exec command - #6060

Merged
cv merged 17 commits into
mainfrom
fix/nemoclaw-cmd-perms-6047-v3
Jul 1, 2026
Merged

fix(scripts): restore .openclaw perms after nemoclaw exec command#6060
cv merged 17 commits into
mainfrom
fix/nemoclaw-cmd-perms-6047-v3

Conversation

@prekshivyas

@prekshivyas prekshivyas commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

openclaw doctor --fix can collapse mutable OpenClaw paths from NemoClaw's multi-UID 2770/660 contract to OpenClaw's single-user 700/600 defaults. This change restores that contract after both entrypoint one-shot commands and the documented nemoclaw <name> exec boundary, with deterministic child-versus-cleanup exit-status precedence.

Related Issue

Fixes #6047

Changes

  • Supervise entrypoint one-shot commands so TERM and INT are forwarded, the direct child is reaped, permission cleanup always runs, and the remote status is preserved when cleanup succeeds.
  • After public OpenShell exec returns, inspect registered OpenClaw sandboxes, repair only detected mutable-permission drift through the installed descriptor-safe normalizer while holding the timer-bound shields mutation lock, and require successful re-inspection. Hermes, custom agents, unregistered sandboxes, and active shields locks remain untouched.
  • Move mutable-tree normalization, baseline capture, and empty-config recovery into an installed root-trusted Python helper that operates through pinned, no-follow descriptors.
  • Authenticate the permanently privilege-dropped owner child with a private Unix socket, SO_PASSCRED, exact credentials, and SCM_RIGHTS; retain the exact directory/config descriptors across the privilege boundary.
  • Replace recovery baseline/config/hash entries with fresh inodes so hardlinks, symlink swaps, directory replacement, and inode-reuse races cannot turn root into a confused deputy.
  • Fail closed on missing trusted helper, unexpected ownership, unsafe links, metadata changes, malformed descriptor handoff, or incomplete verification.
  • Document host-side cleanup behavior, atomic recovery, failure precedence, and safe operator recovery guidance.
  • Add unit, integration, container, and live-target coverage for permission drift, signals, cleanup precedence, capability loss, hardlink safety, path swaps, protected symlinks, and trusted-helper selection.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Docs updated for user-facing behavior changes
  • Docs not applicable — justification:
  • Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging)
  • Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: independent review through runtime head 85b0744310553ec4a141bdc031cdc881e8c57422 confirmed the previously reproducible cross-phase ABA, external-hardlink ownership/mode mutation, path-based recovery TOCTOU, root helper-selection, and earlier-tree hardlink-alias issues are fixed. Follow-up 64d6234a9198e14e582155ebae4c75c362373f10 changes test control flow only. The owner child is permanently privilege-dropped before recursive mutation and root performs no work without an authenticated descriptor handoff. No runtime privilege-boundary blocker remains; the same-UID child retains only authority that the sandbox user already has over its own inode.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Verification

  • PR description includes the DCO sign-off declaration and every commit appears as Verified in GitHub
  • Git hooks passed during commit and push, or npx prek run --from-ref main --to-ref HEAD passes
  • Targeted tests pass for changed behavior
  • Full npm test passes (broad runtime changes only)
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Evidence

  • Signed commit and push hooks: repository checks, Biome, ShellCheck, Hadolint, gitleaks, CLI typecheck, full CLI test hook, source-shape budget, and test-size budget passed.
  • Focused host tests: the final runtime correction passed 67 CLI and 145 integration tests after the earlier focused suites; CLI type-checking passed.
  • Fresh production image: build passed; new E2E cases 30–30f all passed. The full script reported 40 passes and two unrelated stale base-image profile assertions.
  • Independent container probes passed normal repair, exact-config capture, empty-config recovery, hardlink/protected-target invariants, and the final 700/600 to 2770/660 production-image repair path.
  • Full CLI/coverage hooks, repository checks, source-shape budget, and test-size budget passed under the repository's expected umask 022.
  • Docs build completed with 0 errors and 2 pre-existing warnings; agent-variant synchronization and docs checks passed.
  • Security test-depth follow-ups remain non-blocking: malformed ancillary-message variants, an isolated missing-CAP_SETUID case, 16 MiB boundary/source-mutation/temp-cleanup cases, exact-image provenance when reusing an existing E2E tag, and a same-UID concurrent post-check hardlink race that cannot increase the child process's existing authority.

Signed-off-by: Prekshi Vyas prekshiv@nvidia.com
Signed-off-by: Aaron Erickson aerickson@nvidia.com
Signed-off-by: Carlos Villela cvillela@nvidia.com

Summary by CodeRabbit

  • New Features

    • Added safer one-shot command handling with post-command config permission cleanup.
    • Improved config recovery and permission repair behavior for OpenClaw environments.
    • Updated documentation to describe the new cleanup and recovery behavior more clearly.
  • Bug Fixes

    • Hardened config permission handling against symlinks, ownership mismatches, and concurrent changes.
    • Improved failure handling so unsafe or incomplete repairs now fail closed with clearer status reporting.

openclaw doctor --fix collapses /sandbox/.openclaw from 2770 to 700
and openclaw.json from 660 to 600 when run via `nemoclaw exec`. The
NEMOCLAW_CMD exec paths used bare `exec` to replace the shell process,
making any post-command cleanup impossible.

Replace `exec` with a regular call, capture the exit code, call
normalize_mutable_config_perms to restore 2770/660, then exit with
the original code. Covers both the non-root path and the root path
(via STEP_DOWN_PREFIX_SANDBOX).

Fixes #6047

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@prekshivyas prekshivyas self-assigned this Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c07f292c-48f0-4212-a290-55748f0ee263

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR replaces Bash-local mutable OpenClaw config permission repair/baseline logic with a descriptor-safe Python normalizer (normalize_mutable_config_perms.py), delegates start-script normalize/capture/recover operations and one-shot exec cleanup to it, updates sandbox exec/shield permission-inspection flows with structured skip reasons, and adds corresponding tests, Docker packaging, and documentation.

Changes

Mutable config cleanup pipeline

Layer / File(s) Summary
Normalizer and packaging
scripts/lib/normalize_mutable_config_perms.py, Dockerfile, src/lib/sandbox/build-context.ts, test/sandbox-build-context.test.ts, test/sandbox-provisioning-helper-permissions.test.ts
New Python script performs descriptor-safe owner-tree normalization, empty-config recovery, baseline capture/lock, root-supervisor IPC, and CLI entry; it is copied into the Docker image and staged build context, with provisioning tests verifying mode 555.
Sandbox exec cleanup
src/lib/shields/mutable-config-perms.ts, src/lib/shields/index.ts, src/lib/actions/sandbox/doctor-config-perms.ts, src/lib/actions/sandbox/exec.ts, test/*
Inspection results now include structured skipReasons; repairMutableConfigPerms invokes an in-sandbox identity lookup and the normalizer; exec.ts adds async child spawning, signal forwarding, and post-exec cleanup/verification with detailed completion reporting.
Start script delegation
scripts/nemoclaw-start.sh, test/nemoclaw-start-perms.test.ts, test/nemoclaw-start.test.ts, test/repro-2681-group-writable.test.ts, ci/test-file-size-budget.json
normalize_mutable_config_perms becomes an operation selector delegating to the Python helper; run_oneshot_command replaces exec for one-shot entrypoints with signal forwarding and cleanup; baseline/recovery bash logic is removed and tests reworked around the packaged helper.
Docs and E2E coverage
docs/reference/commands.mdx, docs/reference/commands-nemohermes.mdx, docs/reference/troubleshooting.mdx, test/e2e-gateway-isolation.sh, test/e2e/live/shields-config.test.ts
Documentation describes the new fail-closed exec cleanup and recovery contracts; E2E suites add tests for permission repair, baseline lock/capture rejection, symlink refusal, and doctor-fix gateway-write preservation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant execSandbox
  participant runSandboxExecCommand
  participant cleanupOpenClawAfterExec
  participant inspectMutableConfigPerms
  participant repairMutableConfigPerms
  execSandbox->>runSandboxExecCommand: start sandbox command
  runSandboxExecCommand->>cleanupOpenClawAfterExec: inspect after child exit
  cleanupOpenClawAfterExec->>inspectMutableConfigPerms: read mutable config state
  cleanupOpenClawAfterExec->>repairMutableConfigPerms: repair when needed
  runSandboxExecCommand-->>execSandbox: completion code + cleanup status
Loading
sequenceDiagram
  participant run_oneshot_command
  participant NEMOCLAW_CMD
  participant normalize_mutable_config_perms
  run_oneshot_command->>NEMOCLAW_CMD: start direct child
  run_oneshot_command->>NEMOCLAW_CMD: forward TERM/INT while waiting
  run_oneshot_command->>normalize_mutable_config_perms: cleanup after child exit
  normalize_mutable_config_perms-->>run_oneshot_command: cleanup status
  run_oneshot_command-->>run_oneshot_command: return cleanup failure or child exit code
Loading

Possibly related PRs

  • NVIDIA/NemoClaw#4610: Adds the original mutable OpenClaw config "doctor --fix" permission inspection/repair flow (doctor-config-perms + shields.inspectMutableConfigPerms/repairMutableConfigPerms) that this PR rewires to the descriptor-safe normalizer.

Suggested labels: area: sandbox, bug-fix

Suggested reviewers: cv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Title check ✅ Passed The title clearly matches the main change: restoring .openclaw permissions after nemoclaw exec command execution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nemoclaw-cmd-perms-6047-v3

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

@github-code-quality

github-code-quality Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in the fix/nemoclaw-cmd-per... branch is 96%. Coverage data for the main branch is not yet available.

Show a code coverage summary of the most covered files.
File main fix/nemoclaw-cmd-per... 64d6234 +/-
nemoclaw/src/se...cret-scanner.ts 100%
nemoclaw/src/commands/slash.ts 100%
nemoclaw/src/li...bprocess-env.ts 100%
nemoclaw/src/bl...eprint/state.ts 98%
nemoclaw/src/onboard/config.ts 98%
nemoclaw/src/bl...int/snapshot.ts 97%
nemoclaw/src/bl...print/runner.ts 95%
nemoclaw/src/co...ration-state.ts 94%
nemoclaw/src/bl...ate-networks.ts 94%
nemoclaw/src/index.ts 94%

TypeScript / code-coverage/cli

The overall coverage in the fix/nemoclaw-cmd-per... branch is 68%. Coverage data for the main branch is not yet available.

Show a code coverage summary of the most covered files.
File main fix/nemoclaw-cmd-per... 64d6234 +/-
src/lib/shields...nsition-lock.ts 86%
src/lib/actions...dbox/rebuild.ts 80%
src/lib/actions...all/run-plan.ts 80%
src/lib/state/o...oard-session.ts 80%
src/lib/state/sandbox.ts 72%
src/lib/shields/index.ts 69%
src/lib/onboard/preflight.ts 69%
src/lib/onboard...er-gpu-patch.ts 59%
src/lib/actions...licy-channel.ts 58%
src/lib/onboard.ts 20%

Updated July 01, 2026 18:26 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor (Nemotron Ultra) — Changes requested

Merge posture: Do not merge yet
Primary next action: Fix PRA-5: cleanupOpenClawAfterExec lacks concurrency protection for simultaneous exec calls on same sandbox; then add or justify PRA-T1.
Open items: 3 required · 13 warnings · 5 suggestions · 8 test follow-ups
Since last review: 0 prior items resolved · 10 still apply · 7 new items found

Action checklist

  • PRA-5 Fix: cleanupOpenClawAfterExec lacks concurrency protection for simultaneous exec calls on same sandbox in src/lib/actions/sandbox/exec.ts:100
  • PRA-6 Fix: lock_recovery_baseline silently ignores missing baseline — no debug log in scripts/lib/normalize_mutable_config_perms.py:809
  • PRA-7 Fix: shields/index.ts monolith grew by 23 lines (3249 -> 3272) — extract mutable config repair orchestration in src/lib/shields/index.ts:1
  • PRA-1 Resolve or justify: Source-of-truth review needed: run_oneshot_command (scripts/nemoclaw-start.sh:585)
  • PRA-2 Resolve or justify: Source-of-truth review needed: prepare_openclaw_config_for_write / restore_openclaw_config_after_write (scripts/nemoclaw-start.sh:670, 715)
  • PRA-3 Resolve or justify: Source-of-truth review needed: lock_recovery_baseline silent FileNotFoundError (scripts/lib/normalize_mutable_config_perms.py:809)
  • PRA-4 Resolve or justify: Source-of-truth review needed: runSandboxExecChild SIGINT hold (src/lib/actions/sandbox/exec.ts:175)
  • PRA-8 Resolve or justify: Source-of-truth review needed: prepare_openclaw_config_for_write / restore_openclaw_config_after_write shell symlink pre-checks in scripts/nemoclaw-start.sh:670
  • PRA-9 Resolve or justify: Source-of-truth review needed: run_oneshot_command wrapper (tracking issue [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047) in scripts/nemoclaw-start.sh:585
  • PRA-10 Resolve or justify: Source-of-truth review needed: PY_LOCK_CONFIG_BASELINE silent FileNotFoundError -> before = None in scripts/lib/normalize_mutable_config_perms.py:809
  • PRA-11 Resolve or justify: Source-of-truth review needed: cleanupOpenClawAfterExec concurrency in src/lib/actions/sandbox/exec.ts:100
  • PRA-12 Resolve or justify: Source-of-truth review needed: SIGINT held without forwarding in runSandboxExecChild in src/lib/actions/sandbox/exec.ts:175
  • PRA-13 Resolve or justify: runSandboxExecChild holds SIGINT without forwarding — assumption undocumented for non-terminal (CI) environments in src/lib/actions/sandbox/exec.ts:175
  • PRA-T1 Add or justify test follow-up: Runtime validation
  • PRA-T2 Add or justify test follow-up: Runtime validation
  • PRA-T3 Add or justify test follow-up: Runtime validation
  • PRA-T4 Add or justify test follow-up: Runtime validation
  • PRA-T5 Add or justify test follow-up: Runtime validation
  • PRA-T6 Add or justify test follow-up: Concurrent symlink swap test may not reliably trigger race on slow CI
  • PRA-T7 Add or justify test follow-up: E2E test is 400+ lines covering 11 phases — split into focused tests
  • PRA-T8 Add or justify test follow-up: Acceptance clause
  • PRA-17 In-scope improvement: permission_repair_prefix array can be inlined (shrink) in scripts/nemoclaw-start.sh:556
  • PRA-18 In-scope improvement: Dynamic require() in execSandbox — consider static imports in src/lib/actions/sandbox/exec.ts:292
  • PRA-19 In-scope improvement: Concurrent symlink swap test may not reliably trigger race on slow CI in test/nemoclaw-start-perms.test.ts:210
  • PRA-20 In-scope improvement: E2E test is 400+ lines covering 11 phases — split into focused tests in test/e2e/live/shields-config.test.ts:1
  • PRA-21 In-scope improvement: Improve error message to distinguish missing CAP_SETGID vs CAP_SETUID in scripts/lib/normalize_mutable_config_perms.py:1218

Findings index

ID Severity Category Location Required action
PRA-1 Resolve/justify architecture Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
PRA-2 Resolve/justify architecture Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
PRA-3 Resolve/justify architecture Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
PRA-4 Resolve/justify architecture Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
PRA-5 Required security src/lib/actions/sandbox/exec.ts:100 Add a per-sandbox mutex (file-based lock in /sandbox/.openclaw/.cleanup.lock or in-memory map keyed by sandboxName) around the inspect/repair/verify sequence in cleanupOpenClawAfterExec. Document that concurrent exec is serialized for cleanup.
PRA-6 Required security scripts/lib/normalize_mutable_config_perms.py:809 Add debug log in the FileNotFoundError handler: `print('[config] Baseline not found; lock phase skipped (first run or baseline deleted)', file=sys.stderr)`
PRA-7 Required architecture src/lib/shields/index.ts:1 Extract the mutable config repair orchestration (normalizeMutableOpenClawConfig and related dockerExec calls) into a new module shields/mutable-config-repair.ts, following the pattern of timer-control, verify-lock, relock-reconfirm, etc. Update index.ts to import from the new module.
PRA-8 Resolve/justify architecture scripts/nemoclaw-start.sh:670 Either remove the shell symlink checks and rely on the Python normalizer (which uses O_NOFOLLOW and descriptor-relative operations), or add a comment explaining why the shell check is acceptable in the controlled startup phase. Consider refactoring the override paths to use the same Python helper.
PRA-9 Resolve/justify architecture scripts/nemoclaw-start.sh:585 Document the invalid state (700/600 drift), source boundary (OpenClaw doctor --fix), why source cannot be fixed in this PR (upstream), what regression test proves the source cannot regress (test/nemoclaw-start-perms.test.ts), and when the workaround can be removed (when pinned OpenClaw preserves 2770/660 after every command).
PRA-10 Resolve/justify architecture scripts/lib/normalize_mutable_config_perms.py:809 Add the debug log as specified in PRA-7. This resolves both the source-of-truth review and the missing log.
PRA-11 Resolve/justify architecture src/lib/actions/sandbox/exec.ts:100 Add the per-sandbox mutex as specified in PRA-6. This resolves both the security finding and the source-of-truth review.
PRA-12 Resolve/justify architecture src/lib/actions/sandbox/exec.ts:175 Add explicit documentation comment about the process-group assumption. Consider if SIGINT should be forwarded when process.stdin.isTTY === false (non-terminal). The current behavior may be intentional for TTY correctness; document the tradeoff clearly.
PRA-13 Resolve/justify security src/lib/actions/sandbox/exec.ts:175 Document the process-group assumption explicitly. Consider forwarding SIGINT when not in a TTY context, or at minimum document why the current behavior is correct for the supported deployment scenarios.
PRA-14 Resolve/justify security scripts/nemoclaw-start.sh:585 Align SIGINT handling: either both forward after grace period, or both hold with clear documentation. Since runSandboxExecChild is the primary path for nemoclaw exec, make run_oneshot_command match it (hold without forwarding, rely on process group) and document the CI limitation.
PRA-15 Resolve/justify correctness scripts/nemoclaw-start.sh:670 Either remove the shell symlink checks and rely on the Python normalizer (which uses O_NOFOLLOW and descriptor-relative operations), or add a comment explaining why the shell check is acceptable in the controlled startup phase. Consider refactoring the override paths to use the same Python helper.
PRA-16 Resolve/justify correctness scripts/nemoclaw-start.sh:715 Same as PRA-11.
PRA-17 Improvement architecture scripts/nemoclaw-start.sh:556 Inline at the call site: `${ $( [ "$(id -u)" -eq 0 ] && printf '%s\n' "${STEP_DOWN_PREFIX_SANDBOX[@]}" || printf 'env' ) } python3 ...`
PRA-18 Improvement correctness src/lib/actions/sandbox/exec.ts:292 Evaluate if static imports are feasible. If dynamic require is intentional for lazy loading or circular dependency avoidance, add a comment explaining why. If not, convert to static imports.
PRA-19 Improvement tests test/nemoclaw-start-perms.test.ts:210 Add a synchronization mechanism (e.g., mutator writes to a ready file after first swap, test waits for it) or increase the test duration with a loop count check to verify the mutator ran N times.
PRA-20 Improvement tests test/e2e/live/shields-config.test.ts:1 Split into focused tests: shields-config-install, shields-config-mutable-default, shields-config-exec-doctor, shields-config-up-lock, shields-config-redaction, shields-config-tamper, shields-config-down, shields-config-audit, shields-config-auto-restore, shields-config-double-ops. Use shared fixtures/helpers for sandbox lifecycle.

🚨 Required before merge

Address these before merging unless a maintainer explicitly overrides the advisor with rationale.

PRA-5 Required — cleanupOpenClawAfterExec lacks concurrency protection for simultaneous exec calls on same sandbox

  • Location: src/lib/actions/sandbox/exec.ts:100
  • Category: security
  • Problem: Multiple concurrent `nemoclaw <sandbox> exec` commands can interleave inspect/repair/verify cycles in cleanupOpenClawAfterExec, causing redundant repairs, false-positive verification, or inconsistent config state. The sandbox gateway UID relies on the 2770/660 contract for config writes.
  • Impact: Concurrent exec calls on the same sandbox can cause permission repair to be skipped, incorrectly verified, or leave the config in an inconsistent state, breaking gateway config writes.
  • Required action: Add a per-sandbox mutex (file-based lock in /sandbox/.openclaw/.cleanup.lock or in-memory map keyed by sandboxName) around the inspect/repair/verify sequence in cleanupOpenClawAfterExec. Document that concurrent exec is serialized for cleanup.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run two concurrent nemoclaw exec commands that trigger cleanup (e.g., openclaw doctor --fix) and observe interleaved inspect/repair/verify logs. Add test in exec-openclaw-permission-cleanup.test.ts that spawns two concurrent runSandboxExecCommand calls with a shared mock tracking call order.
  • Missing regression test: Add test in exec-openclaw-permission-cleanup.test.ts: spawn two concurrent runSandboxExecCommand calls with a shared mock that tracks call order and verifies mutual exclusion or idempotent sequencing.
  • Done when: The required change is committed and verification passes: Run two concurrent nemoclaw exec commands that trigger cleanup (e.g., openclaw doctor --fix) and observe interleaved inspect/repair/verify logs. Add test in exec-openclaw-permission-cleanup.test.ts that spawns two concurrent runSandboxExecCommand calls with a shared mock tracking call order.
  • Evidence: cleanupOpenClawAfterExec at exec.ts:100 has no locking; it calls inspect -> repair -> re-inspect sequentially with no synchronization across callers.

PRA-6 Required — lock_recovery_baseline silently ignores missing baseline — no debug log

  • Location: scripts/lib/normalize_mutable_config_perms.py:809
  • Category: security
  • Problem: When baseline file is missing (first run or deleted), lock_recovery_baseline catches FileNotFoundError and returns without any log output. Previous review (PRA-14) requested a debug log for diagnostics.
  • Impact: Operators debugging permission issues have no visibility into whether the lock phase ran and found nothing, making it harder to diagnose first-run or baseline-deletion scenarios.
  • Required action: Add debug log in the FileNotFoundError handler: `print('[config] Baseline not found; lock phase skipped (first run or baseline deleted)', file=sys.stderr)`
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run the normalizer on a fresh sandbox without a baseline file and verify the debug message appears on stderr.
  • Missing regression test: Add unit test in a new test file for normalize_mutable_config_perms.py that calls lock_recovery_baseline with no baseline and captures stderr for the expected message.
  • Done when: The required change is committed and verification passes: Run the normalizer on a fresh sandbox without a baseline file and verify the debug message appears on stderr.
  • Evidence: lock_recovery_baseline at line 809 catches FileNotFoundError and sets before = None with no logging.

PRA-7 Required — shields/index.ts monolith grew by 23 lines (3249 -> 3272) — extract mutable config repair orchestration

  • Location: src/lib/shields/index.ts:1
  • Category: architecture
  • Problem: The shields/index.ts file grew by 23 lines with the addition of normalizeMutableOpenClawConfig function and MUTABLE_CONFIG_NORMALIZER constant. This follows the pattern of timer-control, verify-lock, relock-reconfirm, etc., which are extracted into focused modules.
  • Impact: Continues monolith growth in the shields coordinator. The mutable config repair orchestration belongs in a dedicated module like shields/mutable-config-repair.ts.
  • Required action: Extract the mutable config repair orchestration (normalizeMutableOpenClawConfig and related dockerExec calls) into a new module shields/mutable-config-repair.ts, following the pattern of timer-control, verify-lock, relock-reconfirm, etc. Update index.ts to import from the new module.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Check that shields/index.ts line count does not increase net of the extraction. Verify the new module exists and is imported.
  • Missing regression test: No new test needed; existing shields tests cover the behavior.
  • Done when: The required change is committed and verification passes: Check that shields/index.ts line count does not increase net of the extraction. Verify the new module exists and is imported.
  • Evidence: shields/index.ts added normalizeMutableOpenClawConfig function and MUTABLE_CONFIG_NORMALIZER constant; drift evidence shows 23 line growth.
Review findings by urgency: 3 required fixes, 13 items to resolve/justify, 5 in-scope improvements

⚠️ Resolve or justify before merge

Investigate these in the current review; either fix them, explain why they are not applicable, or document the accepted risk.

PRA-1 Resolve/justify — Source-of-truth review needed: run_oneshot_command (scripts/nemoclaw-start.sh:585)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: test/nemoclaw-start-perms.test.ts 'restores a real mutable config tree and preserves child exit status' + shields-config.test.ts phase 'documented exec doctor'
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: Comment above run_oneshot_command mentions issue [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047 but does not fully document all five source-of-truth elements

PRA-2 Resolve/justify — Source-of-truth review needed: prepare_openclaw_config_for_write / restore_openclaw_config_after_write (scripts/nemoclaw-start.sh:670, 715)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: nemoclaw-start-perms.test.ts 'refuses a child-planted config symlink' and 'refuses a symlinked config directory' — but these test the Python normalizer path, not the shell TOCTOU
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: Both functions check [ -L ] before chown/chmod; Python normalizer uses O_NOFOLLOW + descriptor-relative ops

PRA-3 Resolve/justify — Source-of-truth review needed: lock_recovery_baseline silent FileNotFoundError (scripts/lib/normalize_mutable_config_perms.py:809)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: None yet — PRA-7 requests debug log + test
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: lock_recovery_baseline catches FileNotFoundError and sets before = None with no logging

PRA-4 Resolve/justify — Source-of-truth review needed: runSandboxExecChild SIGINT hold (src/lib/actions/sandbox/exec.ts:175)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: Test 'does not deliver a second SIGINT when the terminal already signals the child' — only covers TTY case
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: runSandboxExecChild defines holdInt = () => {} and registers it for SIGINT

PRA-8 Resolve/justify — Source-of-truth review needed: prepare_openclaw_config_for_write / restore_openclaw_config_after_write shell symlink pre-checks

  • Location: scripts/nemoclaw-start.sh:670
  • Category: architecture
  • Problem: The shell functions prepare_openclaw_config_for_write and restore_openclaw_config_after_write use [ -L ... ] symlink checks before chown/chmod operations. These are TOCTOU-vulnerable path-based checks. The Python normalizer already provides descriptor-safe operations; the shell override paths should delegate to it.
  • Impact: An attacker who can swap a config file/directory for a symlink between the check and the chown/chmod could cause the privileged shell code to modify an arbitrary target.
  • Recommended action: Either remove the shell symlink checks and rely on the Python normalizer (which uses O_NOFOLLOW and descriptor-relative operations), or add a comment explaining why the shell check is acceptable in the controlled startup phase. Consider refactoring the override paths to use the same Python helper.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect prepare_openclaw_config_for_write and restore_openclaw_config_after_write in scripts/nemoclaw-start.sh for [ -L ] checks. Trace whether the Python normalizer is invoked in these paths.
  • Missing regression test: Add a test that attempts a symlink swap during the prepare/restore window and verifies the Python normalizer's descriptor safety rejects it (already covered by test/nemoclaw-start-perms.test.ts symlink tests).
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect prepare_openclaw_config_for_write and restore_openclaw_config_after_write in scripts/nemoclaw-start.sh for [ -L ] checks. Trace whether the Python normalizer is invoked in these paths.
  • Evidence: prepare_openclaw_config_for_write at line 670 and restore_openclaw_config_after_write at line 715 both check [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod.

PRA-9 Resolve/justify — Source-of-truth review needed: run_oneshot_command wrapper (tracking issue #6047)

  • Location: scripts/nemoclaw-start.sh:585
  • Category: architecture
  • Problem: The run_oneshot_command function wraps command execution with signal handling and post-command permission cleanup. This is a localized workaround for OpenClaw's doctor --fix tightening permissions. The invalid state (700/600) originates at the OpenClaw command boundary; NemoClaw owns restoring its multi-UID postcondition afterward.
  • Impact: This wrapper adds complexity to the shell entrypoint. The root cause is upstream OpenClaw behavior; the workaround should be removable when OpenClaw preserves 2770/660.
  • Recommended action: Document the invalid state (700/600 drift), source boundary (OpenClaw doctor --fix), why source cannot be fixed in this PR (upstream), what regression test proves the source cannot regress (test/nemoclaw-start-perms.test.ts), and when the workaround can be removed (when pinned OpenClaw preserves 2770/660 after every command).
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check the comment above run_oneshot_command in scripts/nemoclaw-start.sh for the required source-of-truth documentation.
  • Missing regression test: The existing test/nemoclaw-start-perms.test.ts covers the cleanup behavior; ensure it remains passing.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check the comment above run_oneshot_command in scripts/nemoclaw-start.sh for the required source-of-truth documentation.
  • Evidence: Comment above run_oneshot_command mentions issue [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047 but does not fully document all five source-of-truth elements.

PRA-10 Resolve/justify — Source-of-truth review needed: PY_LOCK_CONFIG_BASELINE silent FileNotFoundError -> before = None

  • Location: scripts/lib/normalize_mutable_config_perms.py:809
  • Category: architecture
  • Problem: In lock_recovery_baseline, the FileNotFoundError for a missing baseline is caught and silently converted to before = None with no diagnostic output. This is the same issue as PRA-7 but framed as a source-of-truth review item.
  • Impact: Same as PRA-7 — operators have no visibility into missing baseline scenarios.
  • Recommended action: Add the debug log as specified in PRA-7. This resolves both the source-of-truth review and the missing log.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Same as PRA-7.
  • Missing regression test: Same as PRA-7.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Same as PRA-7.
  • Evidence: lock_recovery_baseline at line 809 catches FileNotFoundError and sets before = None with no logging.

PRA-11 Resolve/justify — Source-of-truth review needed: cleanupOpenClawAfterExec concurrency

  • Location: src/lib/actions/sandbox/exec.ts:100
  • Category: architecture
  • Problem: Same concurrency issue as PRA-6 but framed as a source-of-truth review. The inspect/repair/verify sequence has no synchronization across callers.
  • Impact: Same as PRA-6 — concurrent exec calls can interleave cleanup operations.
  • Recommended action: Add the per-sandbox mutex as specified in PRA-6. This resolves both the security finding and the source-of-truth review.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Same as PRA-6.
  • Missing regression test: Same as PRA-6.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Same as PRA-6.
  • Evidence: cleanupOpenClawAfterExec at exec.ts:100 has no locking; it calls inspect -> repair -> re-inspect sequentially with no synchronization across callers.

PRA-12 Resolve/justify — Source-of-truth review needed: SIGINT held without forwarding in runSandboxExecChild

  • Location: src/lib/actions/sandbox/exec.ts:175
  • Category: architecture
  • Problem: runSandboxExecChild registers a SIGINT handler that does nothing (holdInt), effectively suppressing Ctrl+C forwarding to the child. The comment says this is because a terminal Ctrl+C is already delivered to the foreground process group, but this assumption is undocumented for non-terminal (CI) environments.
  • Impact: In headless/CI environments where the process is not in a foreground process group, SIGINT sent to the parent will not reach the child, potentially leaving orphaned processes.
  • Recommended action: Add explicit documentation comment about the process-group assumption. Consider if SIGINT should be forwarded when process.stdin.isTTY === false (non-terminal). The current behavior may be intentional for TTY correctness; document the tradeoff clearly.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Review the SIGINT handler in runSandboxExecChild and the associated comment.
  • Missing regression test: Add a test that verifies SIGINT behavior in non-TTY mode (may require mocking process.stdin.isTTY).
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Review the SIGINT handler in runSandboxExecChild and the associated comment.
  • Evidence: runSandboxExecChild at exec.ts:175 defines holdInt = () => {} and registers it for SIGINT.

PRA-13 Resolve/justify — runSandboxExecChild holds SIGINT without forwarding — assumption undocumented for non-terminal (CI) environments

  • Location: src/lib/actions/sandbox/exec.ts:175
  • Category: security
  • Problem: Same as PRA-5 but categorized as security. The holdInt handler suppresses SIGINT forwarding based on the assumption that the terminal delivers SIGINT to the whole process group.
  • Impact: In CI/headless environments, a SIGINT sent to the parent process (e.g., via process.kill) will not reach the OpenShell child, breaking cancellation.
  • Recommended action: Document the process-group assumption explicitly. Consider forwarding SIGINT when not in a TTY context, or at minimum document why the current behavior is correct for the supported deployment scenarios.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check runSandboxExecChild SIGINT handler and comment at exec.ts:175.
  • Missing regression test: Test that verifies signal forwarding behavior matches documented assumptions in both TTY and non-TTY contexts.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check runSandboxExecChild SIGINT handler and comment at exec.ts:175.
  • Evidence: runSandboxExecChild at exec.ts:175 defines holdInt = () => {} and registers it for SIGINT.

PRA-14 Resolve/justify — run_oneshot_command SIGINT handling differs from exec.ts — may double-send or not forward in non-terminal

  • Location: scripts/nemoclaw-start.sh:585
  • Category: security
  • Problem: The shell run_oneshot_command traps SIGINT and forwards it to the child, while runSandboxExecChild holds SIGINT without forwarding. In a terminal, both the parent and child receive SIGINT from the process group, so the shell's explicit kill -INT could deliver a second signal. In non-terminal environments, the behavior may diverge.
  • Impact: Inconsistent signal handling between the two one-shot command paths (shell vs TypeScript) could cause double-delivery in terminals or missed delivery in CI.
  • Recommended action: Align SIGINT handling: either both forward after grace period, or both hold with clear documentation. Since runSandboxExecChild is the primary path for nemoclaw exec, make run_oneshot_command match it (hold without forwarding, rely on process group) and document the CI limitation.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Compare the SIGINT trap in run_oneshot_command (nemoclaw-start.sh) with the holdInt handler in runSandboxExecChild (exec.ts).
  • Missing regression test: Add cross-path signal handling tests that verify consistent behavior.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Compare the SIGINT trap in run_oneshot_command (nemoclaw-start.sh) with the holdInt handler in runSandboxExecChild (exec.ts).
  • Evidence: run_oneshot_command traps SIGINT and runs 'kill -INT' on child; runSandboxExecChild holdInt does nothing.

PRA-15 Resolve/justify — Shell symlink pre-check TOCTOU in prepare_openclaw_config_for_write

  • Location: scripts/nemoclaw-start.sh:670
  • Category: correctness
  • Problem: The prepare_openclaw_config_for_write function checks [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod. This is a classic TOCTOU: an attacker can replace a path with a symlink between the check and the chown/chmod.
  • Impact: Privileged shell code could be tricked into chown/chmoding an arbitrary target via a symlink swap.
  • Recommended action: Either remove the shell symlink checks and rely on the Python normalizer (which uses O_NOFOLLOW and descriptor-relative operations), or add a comment explaining why the shell check is acceptable in the controlled startup phase. Consider refactoring the override paths to use the same Python helper.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect prepare_openclaw_config_for_write at nemoclaw-start.sh:670 for the [ -L ] checks.
  • Missing regression test: The Python normalizer's symlink rejection is tested in test/nemoclaw-start-perms.test.ts; ensure the shell path is either removed or tested for the same property.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect prepare_openclaw_config_for_write at nemoclaw-start.sh:670 for the [ -L ] checks.
  • Evidence: prepare_openclaw_config_for_write at line 670 checks [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod.

PRA-16 Resolve/justify — Shell symlink pre-check TOCTOU in restore_openclaw_config_after_write

  • Location: scripts/nemoclaw-start.sh:715
  • Category: correctness
  • Problem: Same TOCTOU issue as PRA-11 in restore_openclaw_config_after_write.
  • Impact: Same as PRA-11.
  • Recommended action: Same as PRA-11.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect restore_openclaw_config_after_write at nemoclaw-start.sh:715 for [ -L ] checks.
  • Missing regression test: Same as PRA-11.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect restore_openclaw_config_after_write at nemoclaw-start.sh:715 for [ -L ] checks.
  • Evidence: restore_openclaw_config_after_write at line 715 checks [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod.

💡 In-scope improvements

These are lower-risk, not throwaway. Prefer fixing them in this PR when they are local to changed code; defer only with rationale or a linked follow-up.

PRA-17 Improvement — permission_repair_prefix array can be inlined (shrink)

  • Location: scripts/nemoclaw-start.sh:556
  • Category: architecture
  • Problem: The STEP_DOWN_PREFIX_SANDBOX array is only used in one place (ensure_mutable_openclaw_config_hash). It can be inlined at the call site.
  • Impact: Minor code complexity; inlining reduces indirection.
  • Suggested action: Inline at the call site: `${ $( [ "$(id -u)" -eq 0 ] && printf '%s\n' "${STEP_DOWN_PREFIX_SANDBOX[@]}" || printf 'env' ) } python3 ...`
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Check line 556 in nemoclaw-start.sh for the array definition and its single use site.
  • Missing regression test: No new test needed; behavior unchanged.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: STEP_DOWN_PREFIX_SANDBOX defined at line 556, used once in ensure_mutable_openclaw_config_hash.

PRA-18 Improvement — Dynamic require() in execSandbox — consider static imports

  • Location: src/lib/actions/sandbox/exec.ts:292
  • Category: correctness
  • Problem: execSandbox uses dynamic require() for CLI branding, openshell runtime adapter, state registry, and shields modules. This is likely for lazy loading or circular dependency avoidance.
  • Impact: Dynamic requires hinder static analysis and tree-shaking. If not needed for circular dependency avoidance, static imports are preferred.
  • Suggested action: Evaluate if static imports are feasible. If dynamic require is intentional for lazy loading or circular dependency avoidance, add a comment explaining why. If not, convert to static imports.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Check exec.ts:292 and surrounding lines for the require() calls.
  • Missing regression test: No new test needed; behavior unchanged.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: execSandbox uses require('../../cli/branding'), require('../../adapters/openshell/runtime'), require('../../state/registry'), require('../../shields') dynamically.

PRA-19 Improvement — Concurrent symlink swap test may not reliably trigger race on slow CI

  • Location: test/nemoclaw-start-perms.test.ts:210
  • Category: tests
  • Problem: The 'never chmods a protected target during background symlink swaps' test spawns a Node.js mutator that rapidly swaps a file for a symlink. On slow CI, the mutator may not run enough iterations to trigger the race.
  • Impact: Flaky test that may pass on fast machines but miss the race condition on slow CI.
  • Suggested action: Add a synchronization mechanism (e.g., mutator writes to a ready file after first swap, test waits for it) or increase the test duration with a loop count check to verify the mutator ran N times.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Review test/nemoclaw-start-perms.test.ts around line 210 for the mutator loop and synchronization.
  • Missing regression test: Modify the test to include a ready-file synchronization or iteration counter.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: Test spawns mutator with infinite loop but no synchronization; relies on 20ms sleep before running main test.

PRA-20 Improvement — E2E test is 400+ lines covering 11 phases — split into focused tests

  • Location: test/e2e/live/shields-config.test.ts:1
  • Category: tests
  • Problem: The shields-config live E2E test covers 11 phases in a single test function: install, mutable default, documented exec doctor, shields up lock, config get redaction, content drift detection, shields down mutable restore, audit trail, dead timer inline auto-restore, double operation rejection. This makes debugging failures harder and increases test runtime.
  • Impact: Harder to debug specific phase failures; longer test runtime; all-or-nothing pass/fail.
  • Suggested action: Split into focused tests: shields-config-install, shields-config-mutable-default, shields-config-exec-doctor, shields-config-up-lock, shields-config-redaction, shields-config-tamper, shields-config-down, shields-config-audit, shields-config-auto-restore, shields-config-double-ops. Use shared fixtures/helpers for sandbox lifecycle.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Review test/e2e/live/shields-config.test.ts structure.
  • Missing regression test: Split the test file into multiple focused test files with shared fixtures.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: Single test function with 11 distinct phases marked by artifactName prefixes.

PRA-21 Improvement — Improve error message to distinguish missing CAP_SETGID vs CAP_SETUID

  • Location: scripts/lib/normalize_mutable_config_perms.py:1218
  • Category: security
  • Problem: The drop_to_owner function prints a generic error '[SECURITY] CAP_SETGID and CAP_SETUID are required for sandbox-owned config repair' when either capability is missing. It doesn't indicate which one is actually missing.
  • Impact: Operators debugging capability issues in constrained environments (e.g., rootless containers) cannot tell which specific capability to grant.
  • Suggested action: Improve error message to check which capability is missing by testing setresgid/setresuid individually or reading /proc/self/status CapEff before the operation.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Check drop_to_owner function at normalize_mutable_config_perms.py:1218 and the read_capabilities helper.
  • Missing regression test: Add a unit test that mocks missing CAP_SETGID or CAP_SETUID individually and verifies the error message identifies the correct one.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: drop_to_owner catches PermissionError and prints generic message without checking which capability failed.
Simplification opportunities: 2 possible cuts, net -3 lines possible

These are safe simplification checks only. Do not remove validation, security controls, data-loss prevention, or required tests.

  • PRA-17 shrink (scripts/nemoclaw-start.sh:556): STEP_DOWN_PREFIX_SANDBOX array definition at line 556
    • Replacement: Inline ternary at call site in ensure_mutable_openclaw_config_hash
    • Net: -3 lines
    • Safety boundary: Must preserve the root-vs-non-root step-down logic exactly
  • PRA-18 stdlib (src/lib/actions/sandbox/exec.ts:292): Dynamic require() calls in execSandbox function
    • Replacement: Static imports at top of file (if no circular dependency) or add comment explaining why dynamic is needed
    • Net: 0 lines
    • Safety boundary: Must not introduce circular dependency; verify import graph
Test follow-ups to resolve or justify

If these cover changed behavior, prefer adding them in this PR; otherwise state why existing coverage is enough or link the follow-up.

  • PRA-T1 Runtime validation — Concurrent exec cleanup: Spawn 2 concurrent runSandboxExecCommand with shared mock tracking call order; verify mutual exclusion or idempotent sequencing (PRA-6). Runtime/sandbox/infrastructure paths need behavioral runtime validation: Dockerfile, ci/test-file-size-budget.json, docs/reference/commands-nemohermes.mdx, docs/reference/commands.mdx, docs/reference/troubleshooting.mdx, scripts/lib/normalize_mutable_config_perms.py, scripts/nemoclaw-start.sh, src/lib/actions/sandbox/doctor-config-perms.ts.
  • PRA-T2 Runtime validation — Missing baseline debug log: Call lock_recovery_baseline with no baseline; capture stderr for expected message (PRA-7). Runtime/sandbox/infrastructure paths need behavioral runtime validation: Dockerfile, ci/test-file-size-budget.json, docs/reference/commands-nemohermes.mdx, docs/reference/commands.mdx, docs/reference/troubleshooting.mdx, scripts/lib/normalize_mutable_config_perms.py, scripts/nemoclaw-start.sh, src/lib/actions/sandbox/doctor-config-perms.ts.
  • PRA-T3 Runtime validation — Signal handling in non-TTY: Mock process.stdin.isTTY = false; send SIGINT to parent; verify child receives it (or document why not) (PRA-8). Runtime/sandbox/infrastructure paths need behavioral runtime validation: Dockerfile, ci/test-file-size-budget.json, docs/reference/commands-nemohermes.mdx, docs/reference/commands.mdx, docs/reference/troubleshooting.mdx, scripts/lib/normalize_mutable_config_perms.py, scripts/nemoclaw-start.sh, src/lib/actions/sandbox/doctor-config-perms.ts.
  • PRA-T4 Runtime validation — Cross-path signal alignment: Verify shell run_oneshot_command and TS runSandboxExecChild have consistent SIGINT behavior in both TTY and non-TTY (PRA-9). Runtime/sandbox/infrastructure paths need behavioral runtime validation: Dockerfile, ci/test-file-size-budget.json, docs/reference/commands-nemohermes.mdx, docs/reference/commands.mdx, docs/reference/troubleshooting.mdx, scripts/lib/normalize_mutable_config_perms.py, scripts/nemoclaw-start.sh, src/lib/actions/sandbox/doctor-config-perms.ts.
  • PRA-T5 Runtime validation — Symlink swap test synchronization: Add ready-file or iteration counter to mutator in nemoclaw-start-perms.test.ts (PRA-15). Runtime/sandbox/infrastructure paths need behavioral runtime validation: Dockerfile, ci/test-file-size-budget.json, docs/reference/commands-nemohermes.mdx, docs/reference/commands.mdx, docs/reference/troubleshooting.mdx, scripts/lib/normalize_mutable_config_perms.py, scripts/nemoclaw-start.sh, src/lib/actions/sandbox/doctor-config-perms.ts.
  • PRA-T6 Concurrent symlink swap test may not reliably trigger race on slow CI — Add a synchronization mechanism (e.g., mutator writes to a ready file after first swap, test waits for it) or increase the test duration with a loop count check to verify the mutator ran N times.
  • PRA-T7 E2E test is 400+ lines covering 11 phases — split into focused tests — Split into focused tests: shields-config-install, shields-config-mutable-default, shields-config-exec-doctor, shields-config-up-lock, shields-config-redaction, shields-config-tamper, shields-config-down, shields-config-audit, shields-config-auto-restore, shields-config-double-ops. Use shared fixtures/helpers for sandbox lifecycle.
  • PRA-T8 Acceptance clause — Doctor changes panel only reports heartbeat replacement; perm collapse undocumented — add test evidence or identify existing coverage. Docs updated in troubleshooting.mdx to explain the drift and recovery via nemoclaw <sandbox> exec -- openclaw doctor --fix; but doctor output itself still doesn't report perm changes
Since last review details

Current findings, using the urgency labels above:

PRA-1 Resolve/justify — Source-of-truth review needed: run_oneshot_command (scripts/nemoclaw-start.sh:585)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: test/nemoclaw-start-perms.test.ts 'restores a real mutable config tree and preserves child exit status' + shields-config.test.ts phase 'documented exec doctor'
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: Comment above run_oneshot_command mentions issue [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047 but does not fully document all five source-of-truth elements

PRA-2 Resolve/justify — Source-of-truth review needed: prepare_openclaw_config_for_write / restore_openclaw_config_after_write (scripts/nemoclaw-start.sh:670, 715)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: nemoclaw-start-perms.test.ts 'refuses a child-planted config symlink' and 'refuses a symlinked config directory' — but these test the Python normalizer path, not the shell TOCTOU
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: Both functions check [ -L ] before chown/chmod; Python normalizer uses O_NOFOLLOW + descriptor-relative ops

PRA-3 Resolve/justify — Source-of-truth review needed: lock_recovery_baseline silent FileNotFoundError (scripts/lib/normalize_mutable_config_perms.py:809)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: None yet — PRA-7 requests debug log + test
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: lock_recovery_baseline catches FileNotFoundError and sets before = None with no logging

PRA-4 Resolve/justify — Source-of-truth review needed: runSandboxExecChild SIGINT hold (src/lib/actions/sandbox/exec.ts:175)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: Test 'does not deliver a second SIGINT when the terminal already signals the child' — only covers TTY case
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: runSandboxExecChild defines holdInt = () => {} and registers it for SIGINT

PRA-5 Required — cleanupOpenClawAfterExec lacks concurrency protection for simultaneous exec calls on same sandbox

  • Location: src/lib/actions/sandbox/exec.ts:100
  • Category: security
  • Problem: Multiple concurrent `nemoclaw <sandbox> exec` commands can interleave inspect/repair/verify cycles in cleanupOpenClawAfterExec, causing redundant repairs, false-positive verification, or inconsistent config state. The sandbox gateway UID relies on the 2770/660 contract for config writes.
  • Impact: Concurrent exec calls on the same sandbox can cause permission repair to be skipped, incorrectly verified, or leave the config in an inconsistent state, breaking gateway config writes.
  • Required action: Add a per-sandbox mutex (file-based lock in /sandbox/.openclaw/.cleanup.lock or in-memory map keyed by sandboxName) around the inspect/repair/verify sequence in cleanupOpenClawAfterExec. Document that concurrent exec is serialized for cleanup.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run two concurrent nemoclaw exec commands that trigger cleanup (e.g., openclaw doctor --fix) and observe interleaved inspect/repair/verify logs. Add test in exec-openclaw-permission-cleanup.test.ts that spawns two concurrent runSandboxExecCommand calls with a shared mock tracking call order.
  • Missing regression test: Add test in exec-openclaw-permission-cleanup.test.ts: spawn two concurrent runSandboxExecCommand calls with a shared mock that tracks call order and verifies mutual exclusion or idempotent sequencing.
  • Done when: The required change is committed and verification passes: Run two concurrent nemoclaw exec commands that trigger cleanup (e.g., openclaw doctor --fix) and observe interleaved inspect/repair/verify logs. Add test in exec-openclaw-permission-cleanup.test.ts that spawns two concurrent runSandboxExecCommand calls with a shared mock tracking call order.
  • Evidence: cleanupOpenClawAfterExec at exec.ts:100 has no locking; it calls inspect -> repair -> re-inspect sequentially with no synchronization across callers.

PRA-6 Required — lock_recovery_baseline silently ignores missing baseline — no debug log

  • Location: scripts/lib/normalize_mutable_config_perms.py:809
  • Category: security
  • Problem: When baseline file is missing (first run or deleted), lock_recovery_baseline catches FileNotFoundError and returns without any log output. Previous review (PRA-14) requested a debug log for diagnostics.
  • Impact: Operators debugging permission issues have no visibility into whether the lock phase ran and found nothing, making it harder to diagnose first-run or baseline-deletion scenarios.
  • Required action: Add debug log in the FileNotFoundError handler: `print('[config] Baseline not found; lock phase skipped (first run or baseline deleted)', file=sys.stderr)`
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run the normalizer on a fresh sandbox without a baseline file and verify the debug message appears on stderr.
  • Missing regression test: Add unit test in a new test file for normalize_mutable_config_perms.py that calls lock_recovery_baseline with no baseline and captures stderr for the expected message.
  • Done when: The required change is committed and verification passes: Run the normalizer on a fresh sandbox without a baseline file and verify the debug message appears on stderr.
  • Evidence: lock_recovery_baseline at line 809 catches FileNotFoundError and sets before = None with no logging.

PRA-7 Required — shields/index.ts monolith grew by 23 lines (3249 -> 3272) — extract mutable config repair orchestration

  • Location: src/lib/shields/index.ts:1
  • Category: architecture
  • Problem: The shields/index.ts file grew by 23 lines with the addition of normalizeMutableOpenClawConfig function and MUTABLE_CONFIG_NORMALIZER constant. This follows the pattern of timer-control, verify-lock, relock-reconfirm, etc., which are extracted into focused modules.
  • Impact: Continues monolith growth in the shields coordinator. The mutable config repair orchestration belongs in a dedicated module like shields/mutable-config-repair.ts.
  • Required action: Extract the mutable config repair orchestration (normalizeMutableOpenClawConfig and related dockerExec calls) into a new module shields/mutable-config-repair.ts, following the pattern of timer-control, verify-lock, relock-reconfirm, etc. Update index.ts to import from the new module.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Check that shields/index.ts line count does not increase net of the extraction. Verify the new module exists and is imported.
  • Missing regression test: No new test needed; existing shields tests cover the behavior.
  • Done when: The required change is committed and verification passes: Check that shields/index.ts line count does not increase net of the extraction. Verify the new module exists and is imported.
  • Evidence: shields/index.ts added normalizeMutableOpenClawConfig function and MUTABLE_CONFIG_NORMALIZER constant; drift evidence shows 23 line growth.

PRA-8 Resolve/justify — Source-of-truth review needed: prepare_openclaw_config_for_write / restore_openclaw_config_after_write shell symlink pre-checks

  • Location: scripts/nemoclaw-start.sh:670
  • Category: architecture
  • Problem: The shell functions prepare_openclaw_config_for_write and restore_openclaw_config_after_write use [ -L ... ] symlink checks before chown/chmod operations. These are TOCTOU-vulnerable path-based checks. The Python normalizer already provides descriptor-safe operations; the shell override paths should delegate to it.
  • Impact: An attacker who can swap a config file/directory for a symlink between the check and the chown/chmod could cause the privileged shell code to modify an arbitrary target.
  • Recommended action: Either remove the shell symlink checks and rely on the Python normalizer (which uses O_NOFOLLOW and descriptor-relative operations), or add a comment explaining why the shell check is acceptable in the controlled startup phase. Consider refactoring the override paths to use the same Python helper.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect prepare_openclaw_config_for_write and restore_openclaw_config_after_write in scripts/nemoclaw-start.sh for [ -L ] checks. Trace whether the Python normalizer is invoked in these paths.
  • Missing regression test: Add a test that attempts a symlink swap during the prepare/restore window and verifies the Python normalizer's descriptor safety rejects it (already covered by test/nemoclaw-start-perms.test.ts symlink tests).
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect prepare_openclaw_config_for_write and restore_openclaw_config_after_write in scripts/nemoclaw-start.sh for [ -L ] checks. Trace whether the Python normalizer is invoked in these paths.
  • Evidence: prepare_openclaw_config_for_write at line 670 and restore_openclaw_config_after_write at line 715 both check [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod.

PRA-9 Resolve/justify — Source-of-truth review needed: run_oneshot_command wrapper (tracking issue #6047)

  • Location: scripts/nemoclaw-start.sh:585
  • Category: architecture
  • Problem: The run_oneshot_command function wraps command execution with signal handling and post-command permission cleanup. This is a localized workaround for OpenClaw's doctor --fix tightening permissions. The invalid state (700/600) originates at the OpenClaw command boundary; NemoClaw owns restoring its multi-UID postcondition afterward.
  • Impact: This wrapper adds complexity to the shell entrypoint. The root cause is upstream OpenClaw behavior; the workaround should be removable when OpenClaw preserves 2770/660.
  • Recommended action: Document the invalid state (700/600 drift), source boundary (OpenClaw doctor --fix), why source cannot be fixed in this PR (upstream), what regression test proves the source cannot regress (test/nemoclaw-start-perms.test.ts), and when the workaround can be removed (when pinned OpenClaw preserves 2770/660 after every command).
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check the comment above run_oneshot_command in scripts/nemoclaw-start.sh for the required source-of-truth documentation.
  • Missing regression test: The existing test/nemoclaw-start-perms.test.ts covers the cleanup behavior; ensure it remains passing.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check the comment above run_oneshot_command in scripts/nemoclaw-start.sh for the required source-of-truth documentation.
  • Evidence: Comment above run_oneshot_command mentions issue [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047 but does not fully document all five source-of-truth elements.

PRA-10 Resolve/justify — Source-of-truth review needed: PY_LOCK_CONFIG_BASELINE silent FileNotFoundError -> before = None

  • Location: scripts/lib/normalize_mutable_config_perms.py:809
  • Category: architecture
  • Problem: In lock_recovery_baseline, the FileNotFoundError for a missing baseline is caught and silently converted to before = None with no diagnostic output. This is the same issue as PRA-7 but framed as a source-of-truth review item.
  • Impact: Same as PRA-7 — operators have no visibility into missing baseline scenarios.
  • Recommended action: Add the debug log as specified in PRA-7. This resolves both the source-of-truth review and the missing log.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Same as PRA-7.
  • Missing regression test: Same as PRA-7.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Same as PRA-7.
  • Evidence: lock_recovery_baseline at line 809 catches FileNotFoundError and sets before = None with no logging.

PRA-11 Resolve/justify — Source-of-truth review needed: cleanupOpenClawAfterExec concurrency

  • Location: src/lib/actions/sandbox/exec.ts:100
  • Category: architecture
  • Problem: Same concurrency issue as PRA-6 but framed as a source-of-truth review. The inspect/repair/verify sequence has no synchronization across callers.
  • Impact: Same as PRA-6 — concurrent exec calls can interleave cleanup operations.
  • Recommended action: Add the per-sandbox mutex as specified in PRA-6. This resolves both the security finding and the source-of-truth review.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Same as PRA-6.
  • Missing regression test: Same as PRA-6.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Same as PRA-6.
  • Evidence: cleanupOpenClawAfterExec at exec.ts:100 has no locking; it calls inspect -> repair -> re-inspect sequentially with no synchronization across callers.

PRA-12 Resolve/justify — Source-of-truth review needed: SIGINT held without forwarding in runSandboxExecChild

  • Location: src/lib/actions/sandbox/exec.ts:175
  • Category: architecture
  • Problem: runSandboxExecChild registers a SIGINT handler that does nothing (holdInt), effectively suppressing Ctrl+C forwarding to the child. The comment says this is because a terminal Ctrl+C is already delivered to the foreground process group, but this assumption is undocumented for non-terminal (CI) environments.
  • Impact: In headless/CI environments where the process is not in a foreground process group, SIGINT sent to the parent will not reach the child, potentially leaving orphaned processes.
  • Recommended action: Add explicit documentation comment about the process-group assumption. Consider if SIGINT should be forwarded when process.stdin.isTTY === false (non-terminal). The current behavior may be intentional for TTY correctness; document the tradeoff clearly.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Review the SIGINT handler in runSandboxExecChild and the associated comment.
  • Missing regression test: Add a test that verifies SIGINT behavior in non-TTY mode (may require mocking process.stdin.isTTY).
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Review the SIGINT handler in runSandboxExecChild and the associated comment.
  • Evidence: runSandboxExecChild at exec.ts:175 defines holdInt = () => {} and registers it for SIGINT.

PRA-13 Resolve/justify — runSandboxExecChild holds SIGINT without forwarding — assumption undocumented for non-terminal (CI) environments

  • Location: src/lib/actions/sandbox/exec.ts:175
  • Category: security
  • Problem: Same as PRA-5 but categorized as security. The holdInt handler suppresses SIGINT forwarding based on the assumption that the terminal delivers SIGINT to the whole process group.
  • Impact: In CI/headless environments, a SIGINT sent to the parent process (e.g., via process.kill) will not reach the OpenShell child, breaking cancellation.
  • Recommended action: Document the process-group assumption explicitly. Consider forwarding SIGINT when not in a TTY context, or at minimum document why the current behavior is correct for the supported deployment scenarios.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check runSandboxExecChild SIGINT handler and comment at exec.ts:175.
  • Missing regression test: Test that verifies signal forwarding behavior matches documented assumptions in both TTY and non-TTY contexts.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check runSandboxExecChild SIGINT handler and comment at exec.ts:175.
  • Evidence: runSandboxExecChild at exec.ts:175 defines holdInt = () => {} and registers it for SIGINT.

PRA-14 Resolve/justify — run_oneshot_command SIGINT handling differs from exec.ts — may double-send or not forward in non-terminal

  • Location: scripts/nemoclaw-start.sh:585
  • Category: security
  • Problem: The shell run_oneshot_command traps SIGINT and forwards it to the child, while runSandboxExecChild holds SIGINT without forwarding. In a terminal, both the parent and child receive SIGINT from the process group, so the shell's explicit kill -INT could deliver a second signal. In non-terminal environments, the behavior may diverge.
  • Impact: Inconsistent signal handling between the two one-shot command paths (shell vs TypeScript) could cause double-delivery in terminals or missed delivery in CI.
  • Recommended action: Align SIGINT handling: either both forward after grace period, or both hold with clear documentation. Since runSandboxExecChild is the primary path for nemoclaw exec, make run_oneshot_command match it (hold without forwarding, rely on process group) and document the CI limitation.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Compare the SIGINT trap in run_oneshot_command (nemoclaw-start.sh) with the holdInt handler in runSandboxExecChild (exec.ts).
  • Missing regression test: Add cross-path signal handling tests that verify consistent behavior.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Compare the SIGINT trap in run_oneshot_command (nemoclaw-start.sh) with the holdInt handler in runSandboxExecChild (exec.ts).
  • Evidence: run_oneshot_command traps SIGINT and runs 'kill -INT' on child; runSandboxExecChild holdInt does nothing.

PRA-15 Resolve/justify — Shell symlink pre-check TOCTOU in prepare_openclaw_config_for_write

  • Location: scripts/nemoclaw-start.sh:670
  • Category: correctness
  • Problem: The prepare_openclaw_config_for_write function checks [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod. This is a classic TOCTOU: an attacker can replace a path with a symlink between the check and the chown/chmod.
  • Impact: Privileged shell code could be tricked into chown/chmoding an arbitrary target via a symlink swap.
  • Recommended action: Either remove the shell symlink checks and rely on the Python normalizer (which uses O_NOFOLLOW and descriptor-relative operations), or add a comment explaining why the shell check is acceptable in the controlled startup phase. Consider refactoring the override paths to use the same Python helper.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect prepare_openclaw_config_for_write at nemoclaw-start.sh:670 for the [ -L ] checks.
  • Missing regression test: The Python normalizer's symlink rejection is tested in test/nemoclaw-start-perms.test.ts; ensure the shell path is either removed or tested for the same property.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect prepare_openclaw_config_for_write at nemoclaw-start.sh:670 for the [ -L ] checks.
  • Evidence: prepare_openclaw_config_for_write at line 670 checks [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod.

PRA-16 Resolve/justify — Shell symlink pre-check TOCTOU in restore_openclaw_config_after_write

  • Location: scripts/nemoclaw-start.sh:715
  • Category: correctness
  • Problem: Same TOCTOU issue as PRA-11 in restore_openclaw_config_after_write.
  • Impact: Same as PRA-11.
  • Recommended action: Same as PRA-11.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect restore_openclaw_config_after_write at nemoclaw-start.sh:715 for [ -L ] checks.
  • Missing regression test: Same as PRA-11.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect restore_openclaw_config_after_write at nemoclaw-start.sh:715 for [ -L ] checks.
  • Evidence: restore_openclaw_config_after_write at line 715 checks [ -L "$config_dir" ] || [ -L "$config_file" ] || [ -L "$hash_file" ] before chown/chmod.

PRA-17 Improvement — permission_repair_prefix array can be inlined (shrink)

  • Location: scripts/nemoclaw-start.sh:556
  • Category: architecture
  • Problem: The STEP_DOWN_PREFIX_SANDBOX array is only used in one place (ensure_mutable_openclaw_config_hash). It can be inlined at the call site.
  • Impact: Minor code complexity; inlining reduces indirection.
  • Suggested action: Inline at the call site: `${ $( [ "$(id -u)" -eq 0 ] && printf '%s\n' "${STEP_DOWN_PREFIX_SANDBOX[@]}" || printf 'env' ) } python3 ...`
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Check line 556 in nemoclaw-start.sh for the array definition and its single use site.
  • Missing regression test: No new test needed; behavior unchanged.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: STEP_DOWN_PREFIX_SANDBOX defined at line 556, used once in ensure_mutable_openclaw_config_hash.

PRA-18 Improvement — Dynamic require() in execSandbox — consider static imports

  • Location: src/lib/actions/sandbox/exec.ts:292
  • Category: correctness
  • Problem: execSandbox uses dynamic require() for CLI branding, openshell runtime adapter, state registry, and shields modules. This is likely for lazy loading or circular dependency avoidance.
  • Impact: Dynamic requires hinder static analysis and tree-shaking. If not needed for circular dependency avoidance, static imports are preferred.
  • Suggested action: Evaluate if static imports are feasible. If dynamic require is intentional for lazy loading or circular dependency avoidance, add a comment explaining why. If not, convert to static imports.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Check exec.ts:292 and surrounding lines for the require() calls.
  • Missing regression test: No new test needed; behavior unchanged.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: execSandbox uses require('../../cli/branding'), require('../../adapters/openshell/runtime'), require('../../state/registry'), require('../../shields') dynamically.

PRA-19 Improvement — Concurrent symlink swap test may not reliably trigger race on slow CI

  • Location: test/nemoclaw-start-perms.test.ts:210
  • Category: tests
  • Problem: The 'never chmods a protected target during background symlink swaps' test spawns a Node.js mutator that rapidly swaps a file for a symlink. On slow CI, the mutator may not run enough iterations to trigger the race.
  • Impact: Flaky test that may pass on fast machines but miss the race condition on slow CI.
  • Suggested action: Add a synchronization mechanism (e.g., mutator writes to a ready file after first swap, test waits for it) or increase the test duration with a loop count check to verify the mutator ran N times.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Review test/nemoclaw-start-perms.test.ts around line 210 for the mutator loop and synchronization.
  • Missing regression test: Modify the test to include a ready-file synchronization or iteration counter.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: Test spawns mutator with infinite loop but no synchronization; relies on 20ms sleep before running main test.

PRA-20 Improvement — E2E test is 400+ lines covering 11 phases — split into focused tests

  • Location: test/e2e/live/shields-config.test.ts:1
  • Category: tests
  • Problem: The shields-config live E2E test covers 11 phases in a single test function: install, mutable default, documented exec doctor, shields up lock, config get redaction, content drift detection, shields down mutable restore, audit trail, dead timer inline auto-restore, double operation rejection. This makes debugging failures harder and increases test runtime.
  • Impact: Harder to debug specific phase failures; longer test runtime; all-or-nothing pass/fail.
  • Suggested action: Split into focused tests: shields-config-install, shields-config-mutable-default, shields-config-exec-doctor, shields-config-up-lock, shields-config-redaction, shields-config-tamper, shields-config-down, shields-config-audit, shields-config-auto-restore, shields-config-double-ops. Use shared fixtures/helpers for sandbox lifecycle.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Review test/e2e/live/shields-config.test.ts structure.
  • Missing regression test: Split the test file into multiple focused test files with shared fixtures.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: Single test function with 11 distinct phases marked by artifactName prefixes.

Workflow run details

This is an automated, non-binding review; it still expects maintainers and agents to respond to each required or warning item. Treat suggestions as current-PR improvements when they touch changed code; defer only with maintainer rationale or a linked follow-up. A human maintainer must make the final merge decision.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

E2E Advisor Recommendation

Required E2E: shields-config, cloud-onboard, sandbox-rebuild, test-e2e-gateway-isolation
Optional E2E: full-e2e, sandbox-survival, sandbox-operations

Dispatch hint: shields-config,cloud-onboard,sandbox-rebuild

Workflow run

Full advisor summary

E2E Recommendation Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E

  • shields-config (medium): Primary live coverage for this PR: it installs/onboards a real OpenClaw sandbox, verifies the mutable 2770/660 config contract, exercises the documented nemoclaw exec -- openclaw doctor --fix cleanup path, checks gateway write access after cleanup, and validates shields up/down locking behavior.
  • cloud-onboard (high): Required because Dockerfile, build-context, and entrypoint changes can affect full hosted onboarding and initial sandbox startup. This validates the real installer/onboard path with hosted inference rather than only local/unit permission probes.
  • sandbox-rebuild (high): Required because mutable config permission repair is used after OpenClaw doctor/structure repair during rebuild paths. This validates that rebuild preserves/restores config state and that post-upgrade permission repair does not break a real sandbox lifecycle.
  • test-e2e-gateway-isolation (medium): Required because the PR changes production image hardening and explicitly updates the gateway-isolation test to verify the installed normalizer helper mode, gateway/sandbox group boundary, mutable config writability, and root-only control boundary in the built image.

Optional E2E

  • full-e2e (high): Useful end-user confidence that a freshly onboarded OpenClaw sandbox still supports a real assistant turn after the startup/config-permission changes. Not the most targeted permission regression, so keep optional if capacity is constrained.
  • sandbox-survival (medium): Useful adjacent coverage for restart/gateway survival and durable /sandbox/.openclaw state after changes to startup config normalization and recovery, but not as targeted as shields-config or sandbox-rebuild.
  • sandbox-operations (medium): Useful broader lifecycle confidence for sandbox exec/status/destroy flows after the host-side exec cleanup change, but shields-config covers the specific OpenClaw permission-cleanup scenario.

New E2E recommendations

  • descriptor-safe mutable config normalization (high): Existing live coverage checks the normal happy path and documented doctor cleanup, but the new normalizer contains complex fail-closed symlink, hardlink, owner, inode-race, and unsafe metadata handling that is security-sensitive. Add a live/container E2E that plants unsafe links or ownership drift under /sandbox/.openclaw and verifies startup/exec/doctor repair refuses without following or modifying the target.
    • Suggested test: Add a live or image-level E2E for unsafe mutable-config trees that validates fail-closed behavior for symlink/hardlink/owner drift during startup and host-side exec cleanup.
  • empty openclaw.json recovery (medium): The new normalizer includes recovery from empty openclaw.json using last-good or baseline config with .config-hash replacement. Existing required tests do not appear to exercise a real sandbox restart with an emptied config and both safe and unsafe recovery sources.
    • Suggested test: Add a live E2E that empties /sandbox/.openclaw/openclaw.json, restarts the sandbox, verifies recovery from a trusted last-good/baseline source, and separately verifies unsafe baseline/source metadata is refused.

Dispatch hint

  • Workflow: .github/workflows/e2e.yaml
  • jobs input: shields-config,cloud-onboard,sandbox-rebuild

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Changes requested

Merge posture: Do not merge yet
Primary next action: Resolve or justify PRA-1: Add negative coverage for privileged fd-handoff and capability-drop edges.
Open items: 0 required · 1 warning · 0 suggestions · 5 test follow-ups
Since last review: 3 prior items resolved · 0 still apply · 1 new item found

Action checklist

  • PRA-1 Resolve or justify: Add negative coverage for privileged fd-handoff and capability-drop edges in scripts/lib/normalize_mutable_config_perms.py:1161
  • PRA-T1 Add or justify test follow-up: Runtime validation
  • PRA-T2 Add or justify test follow-up: Runtime validation
  • PRA-T3 Add or justify test follow-up: Runtime validation
  • PRA-T4 Add or justify test follow-up: Runtime validation
  • PRA-T5 Add or justify test follow-up: Runtime validation

Findings index

ID Severity Category Location Required action
PRA-1 Resolve/justify security scripts/lib/normalize_mutable_config_perms.py:1161 Add focused tests in this PR, or point to existing tests if they already cover these exact behaviors: malformed/truncated/extra `SCM_RIGHTS` and `SCM_CREDENTIALS` records are rejected; credential mismatch is rejected; root repair fails closed when `CAP_SETGID` setup succeeds but `CAP_SETUID` or capability clearing is incomplete; baseline capture/recovery accepts exactly 16 MiB valid JSON5 and rejects 16 MiB + 1.
Review findings by urgency: 0 required fixes, 1 item to resolve/justify, 0 in-scope improvements

⚠️ Resolve or justify before merge

Investigate these in the current review; either fix them, explain why they are not applicable, or document the accepted risk.

PRA-1 Resolve/justify — Add negative coverage for privileged fd-handoff and capability-drop edges

  • Location: scripts/lib/normalize_mutable_config_perms.py:1161
  • Category: security
  • Problem: The new root supervisor relies on authenticated Unix ancillary data and a permanently privilege-dropped owner child before root locks or captures mutable config descriptors. The implementation validates `SCM_RIGHTS`, `SCM_CREDENTIALS`, exact child credentials, UID/GID drops, and baseline size limits, but the changed tests do not directly exercise malformed ancillary records, credential mismatches, incomplete capability drops, or the exact `MAX_BASELINE_BYTES` boundary.
  • Impact: A regression in this path could turn a future refactor of the root-supervisor handshake or privilege drop into a confused-deputy permission repair bug without a failing test, even though the current code review did not identify an exploitable bypass.
  • Recommended action: Add focused tests in this PR, or point to existing tests if they already cover these exact behaviors: malformed/truncated/extra `SCM_RIGHTS` and `SCM_CREDENTIALS` records are rejected; credential mismatch is rejected; root repair fails closed when `CAP_SETGID` setup succeeds but `CAP_SETUID` or capability clearing is incomplete; baseline capture/recovery accepts exactly 16 MiB valid JSON5 and rejects 16 MiB + 1.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Read `scripts/lib/normalize_mutable_config_perms.py` around `receive_owner_fd`, `drop_to_owner`, and `MAX_BASELINE_BYTES`, then grep changed tests for `SCM_RIGHTS`, `SCM_CREDENTIALS`, `CAP_SETUID`, `CAP_SETGID`, and `MAX_BASELINE_BYTES`.
  • Missing regression test: Add behavior-specific tests named along these lines: `root supervisor rejects truncated SCM_RIGHTS handoff`, `root supervisor rejects extra SCM_CREDENTIALS record`, `root supervisor rejects child credential mismatch`, `root supervisor fails closed when CAP_SETUID drop is incomplete`, and `baseline capture enforces the 16 MiB source-size boundary`.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Read `scripts/lib/normalize_mutable_config_perms.py` around `receive_owner_fd`, `drop_to_owner`, and `MAX_BASELINE_BYTES`, then grep changed tests for `SCM_RIGHTS`, `SCM_CREDENTIALS`, `CAP_SETUID`, `CAP_SETGID`, and `MAX_BASELINE_BYTES`.
  • Evidence: `receive_owner_fd` validates ancillary data at `scripts/lib/normalize_mutable_config_perms.py:1161`, `drop_to_owner` validates UID/GID/capability drop at `scripts/lib/normalize_mutable_config_perms.py:761`, and `MAX_BASELINE_BYTES` is enforced in the helper. Current changed tests cover symlink/race/hardening and cleanup precedence, but the static test inventory does not show direct tests for malformed ancillary records, credential mismatch, capability-drop failure, or exact size boundaries.

💡 In-scope improvements

These are lower-risk, not throwaway. Prefer fixing them in this PR when they are local to changed code; defer only with rationale or a linked follow-up.

  • None.
Test follow-ups to resolve or justify

If these cover changed behavior, prefer adding them in this PR; otherwise state why existing coverage is enough or link the follow-up.

  • PRA-T1 Runtime validation — Root supervisor rejects truncated, extra, or mismatched `SCM_RIGHTS` / `SCM_CREDENTIALS` ancillary records.. The unit and integration tests are broad and behavior-focused, but this PR changes Dockerfile installation, sandbox entrypoint lifecycle, host-side exec cleanup, and a privileged descriptor/fd-passing helper. Those trust-boundary paths warrant targeted negative runtime validation beyond ordinary mocked contract tests.
  • PRA-T2 Runtime validation — Root supervisor rejects a credential mismatch between the forked owner child PID/UID/GID and the received socket credentials.. The unit and integration tests are broad and behavior-focused, but this PR changes Dockerfile installation, sandbox entrypoint lifecycle, host-side exec cleanup, and a privileged descriptor/fd-passing helper. Those trust-boundary paths warrant targeted negative runtime validation beyond ordinary mocked contract tests.
  • PRA-T3 Runtime validation — Root supervisor fails closed when `CAP_SETGID` setup succeeds but `CAP_SETUID` or capability clearing is incomplete.. The unit and integration tests are broad and behavior-focused, but this PR changes Dockerfile installation, sandbox entrypoint lifecycle, host-side exec cleanup, and a privileged descriptor/fd-passing helper. Those trust-boundary paths warrant targeted negative runtime validation beyond ordinary mocked contract tests.
  • PRA-T4 Runtime validation — Baseline capture and empty-config recovery accept exactly 16 MiB valid JSON5 content and reject a 16 MiB + 1 byte source.. The unit and integration tests are broad and behavior-focused, but this PR changes Dockerfile installation, sandbox entrypoint lifecycle, host-side exec cleanup, and a privileged descriptor/fd-passing helper. Those trust-boundary paths warrant targeted negative runtime validation beyond ordinary mocked contract tests.
  • PRA-T5 Runtime validation — Recovery or capture failure leaves no matched staged `*.nemoclaw-*.tmp` file behind after failed verification or replacement.. The unit and integration tests are broad and behavior-focused, but this PR changes Dockerfile installation, sandbox entrypoint lifecycle, host-side exec cleanup, and a privileged descriptor/fd-passing helper. Those trust-boundary paths warrant targeted negative runtime validation beyond ordinary mocked contract tests.
Since last review details

Current findings, using the urgency labels above:

PRA-1 Resolve/justify — Add negative coverage for privileged fd-handoff and capability-drop edges

  • Location: scripts/lib/normalize_mutable_config_perms.py:1161
  • Category: security
  • Problem: The new root supervisor relies on authenticated Unix ancillary data and a permanently privilege-dropped owner child before root locks or captures mutable config descriptors. The implementation validates `SCM_RIGHTS`, `SCM_CREDENTIALS`, exact child credentials, UID/GID drops, and baseline size limits, but the changed tests do not directly exercise malformed ancillary records, credential mismatches, incomplete capability drops, or the exact `MAX_BASELINE_BYTES` boundary.
  • Impact: A regression in this path could turn a future refactor of the root-supervisor handshake or privilege drop into a confused-deputy permission repair bug without a failing test, even though the current code review did not identify an exploitable bypass.
  • Recommended action: Add focused tests in this PR, or point to existing tests if they already cover these exact behaviors: malformed/truncated/extra `SCM_RIGHTS` and `SCM_CREDENTIALS` records are rejected; credential mismatch is rejected; root repair fails closed when `CAP_SETGID` setup succeeds but `CAP_SETUID` or capability clearing is incomplete; baseline capture/recovery accepts exactly 16 MiB valid JSON5 and rejects 16 MiB + 1.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Read `scripts/lib/normalize_mutable_config_perms.py` around `receive_owner_fd`, `drop_to_owner`, and `MAX_BASELINE_BYTES`, then grep changed tests for `SCM_RIGHTS`, `SCM_CREDENTIALS`, `CAP_SETUID`, `CAP_SETGID`, and `MAX_BASELINE_BYTES`.
  • Missing regression test: Add behavior-specific tests named along these lines: `root supervisor rejects truncated SCM_RIGHTS handoff`, `root supervisor rejects extra SCM_CREDENTIALS record`, `root supervisor rejects child credential mismatch`, `root supervisor fails closed when CAP_SETUID drop is incomplete`, and `baseline capture enforces the 16 MiB source-size boundary`.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Read `scripts/lib/normalize_mutable_config_perms.py` around `receive_owner_fd`, `drop_to_owner`, and `MAX_BASELINE_BYTES`, then grep changed tests for `SCM_RIGHTS`, `SCM_CREDENTIALS`, `CAP_SETUID`, `CAP_SETGID`, and `MAX_BASELINE_BYTES`.
  • Evidence: `receive_owner_fd` validates ancillary data at `scripts/lib/normalize_mutable_config_perms.py:1161`, `drop_to_owner` validates UID/GID/capability drop at `scripts/lib/normalize_mutable_config_perms.py:761`, and `MAX_BASELINE_BYTES` is enforced in the helper. Current changed tests cover symlink/race/hardening and cleanup precedence, but the static test inventory does not show direct tests for malformed ancillary records, credential mismatch, capability-drop failure, or exact size boundaries.

Workflow run details

This is an automated, non-binding review; it still expects maintainers and agents to respond to each required or warning item. Treat suggestions as current-PR improvements when they touch changed code; defer only with maintainer rationale or a linked follow-up. A human maintainer must make the final merge decision.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

E2E Target Recommendation

Required E2E targets: shields-config
Optional E2E targets: None

Dispatch required E2E targets:

  • gh workflow run e2e.yaml --ref <pr-head-ref> --field jobs=shields-config

Workflow run

Full E2E target advisor summary

E2E Target Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E targets

  • shields-config: Focused free-standing E2E job wired for changed live test test/e2e/live/shields-config.test.ts.
    • Dispatch: gh workflow run e2e.yaml --ref <pr-head-ref> --field jobs=shields-config

Optional E2E targets

  • None.

Relevant changed files

  • Dockerfile
  • scripts/lib/normalize_mutable_config_perms.py
  • scripts/nemoclaw-start.sh
  • src/lib/actions/sandbox/doctor-config-perms.ts
  • src/lib/actions/sandbox/exec.ts
  • src/lib/sandbox/build-context.ts
  • src/lib/shields/index.ts
  • src/lib/shields/mutable-config-perms.ts
  • test/e2e/live/shields-config.test.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/nemoclaw-start.sh`:
- Around line 3981-3984: The one-shot command path in nemoclaw-start.sh is no
longer forwarding termination signals because it exits after running
NEMOCLAW_CMD without the gateway loop’s trap handling. Update the one-shot
branch around the NEMOCLAW_CMD execution so it either uses exec or installs an
explicit trap/forward-and-wait wrapper consistent with cleanup_on_signal,
ensuring SIGTERM/SIGINT from docker stop are propagated to the child before
exiting with its status.
- Around line 3981-3984: The one-shot command path in nemoclaw-start.sh can
bypass cleanup because the NEMOCLAW_CMD invocation is running under errexit, so
the exit code capture and normalize_mutable_config_perms may never run. Update
the one-shot flow around the NEMOCLAW_CMD execution so failures cannot skip the
cleanup/restore step, using the existing _nemoclaw_cmd_rc and
normalize_mutable_config_perms flow as the place to preserve. If you keep the
shell as PID 1 instead of execing the child, also ensure TERM/INT are forwarded
to the child process; otherwise restore exec and relocate the permission-fix
logic outside the post-run path.
🪄 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: Enterprise

Run ID: d7d86225-04c1-4fc2-9e37-1488bf415ad2

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfaf8f and c8328e7.

📒 Files selected for processing (1)
  • scripts/nemoclaw-start.sh

Comment thread scripts/nemoclaw-start.sh Outdated
openclaw doctor --fix collapses /sandbox/.openclaw from 2770 to 700
and openclaw.json from 660 to 600 when run via `nemoclaw exec`. The
NEMOCLAW_CMD exec paths used bare `exec` to replace the shell process,
making any post-command cleanup impossible.

Replace `exec` with a regular call in both NEMOCLAW_CMD paths. Use
`_nemoclaw_cmd_rc=0; cmd || _nemoclaw_cmd_rc=$?` to capture the exit
code safely under `set -e`, call normalize_mutable_config_perms to
restore 2770/660, then exit with the original code. Covers both the
non-root path and the root path (via STEP_DOWN_PREFIX_SANDBOX).

Add ORDER-marker tests in test/nemoclaw-start-perms.test.ts to verify
the call sequence (command → normalize → exit with captured code) in
both paths, including that a non-zero exit from NEMOCLAW_CMD is
preserved through the normalize call.

Fixes #6047

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
test/nemoclaw-start-perms.test.ts (1)

16-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Marker-based source-text extraction is brittle implementation lock-in.

extractBlock slices raw bash text out of nemoclaw-start.sh using exact literal markers (including specific indentation, e.g. " if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then\n" and "\n fi\n"). Any reformatting of the script (reindentation, comment rewording, shfmt) will throw "marker not found" and fail the test for reasons unrelated to the permission-ordering regression being verified, rather than exercising an observable behavioral contract.

Since the script has no callable functions to test directly, this approach may be unavoidable, but consider isolating the two NEMOCLAW_CMD blocks behind clearly-delimited, stable comment sentinels in nemoclaw-start.sh itself (e.g. # BEGIN/END NEMOCLAW_CMD_NONROOT) so the test's contract is decoupled from incidental formatting in the surrounding code. As per path instructions, tests should "prefer observable outcomes through the public boundary over source-text, private-shape ... assertions."

Also applies to: 29-34, 56-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/nemoclaw-start-perms.test.ts` around lines 16 - 22, The test helper
extractBlock is coupled to exact bash formatting, so reindentation or shfmt
changes will break the test for the wrong reason. Update nemoclaw-start.sh to
add stable sentinel comments around the NEMOCLAW_CMD sections, and then change
extractBlock and the related assertions in nemoclaw-start-perms.test.ts to slice
between those sentinels instead of brittle literal indentation markers. Keep the
test focused on the ordering behavior of the NEMOCLAW_CMD blocks rather than raw
source-text shape.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/nemoclaw-start-perms.test.ts`:
- Around line 16-22: The test helper extractBlock is coupled to exact bash
formatting, so reindentation or shfmt changes will break the test for the wrong
reason. Update nemoclaw-start.sh to add stable sentinel comments around the
NEMOCLAW_CMD sections, and then change extractBlock and the related assertions
in nemoclaw-start-perms.test.ts to slice between those sentinels instead of
brittle literal indentation markers. Keep the test focused on the ordering
behavior of the NEMOCLAW_CMD blocks rather than raw source-text shape.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 52ecaead-99eb-4c19-81c9-d921630dc945

📥 Commits

Reviewing files that changed from the base of the PR and between c8328e7 and 4639f7a.

📒 Files selected for processing (2)
  • scripts/nemoclaw-start.sh
  • test/nemoclaw-start-perms.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/nemoclaw-start.sh

…-shape asserts

The codebase-growth-guardrails check bans `if (` in changed test files and
caps source-shape assertion cases at 0. Move the script-block extraction into
module-scope helpers (assertions there are not counted as source-shape cases)
and drop the inline if/throw guards, keeping the test bodies linear.

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ✅ All requested jobs passed

Run: 28528148197
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: (default — all supported)
Requested jobs: runtime-overrides
Summary: 1 passed, 0 failed, 0 cancelled, 0 skipped

Job Result
runtime-overrides ✅ success

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ✅ All selected jobs passed

Run: 28528151221
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: ubuntu-repo-cloud-openclaw
Requested jobs: (default — all default-enabled free-standing jobs; explicit-only jobs openshell-gateway-auth-contract, jetson-nvmap-gpu, and sandbox-rlimits-connect are skipped unless selected)
Summary: 1 passed, 0 failed, 0 cancelled, 0 skipped

Job Result
live ✅ success

ericksoa added 2 commits July 1, 2026 09:05
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ✅ All requested jobs passed

Run: 28531132233
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: (default — all supported)
Requested jobs: runtime-overrides,shields-config
Summary: 2 passed, 0 failed, 0 cancelled, 0 skipped

Job Result
runtime-overrides ✅ success
shields-config ✅ success

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ✅ All selected jobs passed

Run: 28531133994
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: ubuntu-repo-cloud-openclaw,network-policy
Requested jobs: (default — all default-enabled free-standing jobs; explicit-only jobs openshell-gateway-auth-contract, jetson-nvmap-gpu, and sandbox-rlimits-connect are skipped unless selected)
Summary: 2 passed, 0 failed, 0 cancelled, 0 skipped

Job Result
live ✅ success
network-policy ✅ success

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ❌ Some jobs failed

Run: 28531609217
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: (default — all supported)
Requested jobs: runtime-overrides,shields-config
Summary: 1 passed, 1 failed, 0 cancelled, 0 skipped

Job Result
runtime-overrides ✅ success
shields-config ❌ failure

Failed jobs: shields-config. Check run artifacts for logs.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ✅ All selected jobs passed

Run: 28531611172
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: ubuntu-repo-cloud-openclaw,network-policy
Requested jobs: (default — all default-enabled free-standing jobs; explicit-only jobs openshell-gateway-auth-contract, jetson-nvmap-gpu, and sandbox-rlimits-connect are skipped unless selected)
Summary: 2 passed, 0 failed, 0 cancelled, 0 skipped

Job Result
live ✅ success
network-policy ✅ success

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ❌ Some jobs failed

Run: 28532745190
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: (default — all supported)
Requested jobs: runtime-overrides,shields-config
Summary: 1 passed, 1 failed, 0 cancelled, 0 skipped

Job Result
runtime-overrides ✅ success
shields-config ❌ failure

Failed jobs: shields-config. Check run artifacts for logs.

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ⚠️ Run cancelled — no signal

Run: 28532747192
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: ubuntu-repo-cloud-openclaw,network-policy
Requested jobs: (default — all default-enabled free-standing jobs; explicit-only jobs openshell-gateway-auth-contract, jetson-nvmap-gpu, and sandbox-rlimits-connect are skipped unless selected)
Summary: 0 passed, 0 failed, 2 cancelled, 0 skipped

Job Result
live ⚠️ cancelled
network-policy ⚠️ cancelled

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@cv Thanks for the update — I'll review the current head at 85b0744310553ec4a141bdc031cdc881e8c57422, including the test-suite split, root-runner fixture correction, fixed-file hardlink preflight, and the live-E2E change to invoke the installed descriptor-safe normalizer directly.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@cv Understood — I'll review the current head at 85b0744310553ec4a141bdc031cdc881e8c57422, covering the test-suite split, root-runner fixture correction, fixed-file hardlink preflight, and the live-E2E change to invoke the installed descriptor-safe normalizer directly.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
@cv

cv commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai review

Growth-guard follow-up is now at 64d6234a9: test-only conditionals were converted to equivalent switch setup so the current head satisfies the no-new-test-ifs guard. The runtime implementation is unchanged from the prior review request.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@cv Got it — I'll review the current head at 64d6234a9, focusing on the growth-guard follow-up (test-only conditionals converted to switch statements to satisfy the no-new-test-ifs guard), noting the runtime implementation itself is unchanged from the previous review.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ✅ All requested jobs passed

Run: 28538535223
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: (default — all supported)
Requested jobs: shields-config
Summary: 1 passed, 0 failed, 0 cancelled, 0 skipped

Job Result
shields-config ✅ success

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Vitest E2E Target Results — ✅ All requested jobs passed

Run: 28538710787
Workflow ref: fix/nemoclaw-cmd-perms-6047-v3
Requested targets: (default — all supported)
Requested jobs: shields-config
Summary: 1 passed, 0 failed, 0 cancelled, 0 skipped

Job Result
shields-config ✅ success

@cv
cv merged commit b8330ae into main Jul 1, 2026
126 of 127 checks passed
@cv
cv deleted the fix/nemoclaw-cmd-perms-6047-v3 branch July 1, 2026 18:40
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@cv Got it — I'll review the current head at 64d6234a9, which converts the test-only conditionals into equivalent switch setup to satisfy the no-new-test-ifs guard, noting the runtime implementation is unchanged from the prior review request.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cjagwani added a commit that referenced this pull request Jul 1, 2026
Merge origin/main into fix/5980-exec-newline-guidance. Resolved:
- src/lib/actions/sandbox/exec.ts: keep main's async spawn dispatch
  (runSandboxExecChild + signal forwarding + OpenClaw permission cleanup,
  #6060); re-apply the PR's pre-dispatch multiline-argv guard on top, threaded
  through main's deps seam. The guard exits 2 before dispatch and never echoes
  argument contents.
- docs/reference/commands.mdx: place the PR's agent-agnostic multiline guidance
  after main's AgentOnly exit-code blocks.
- src/lib/actions/sandbox/exec.multiline-guard.test.ts: update the
  default-runner stdio-inherit test to assert main's async spawn
  (spawn(..., { stdio: 'inherit' })) instead of the retired spawnSync default.

Signed-off-by: Charan Jagwani <cjagwani@nvidia.com>

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apurvvkumaria pushed a commit that referenced this pull request Jul 1, 2026
## Summary

Five pre-existing test failures on the `macos-vitest` CI workflow
(confirmed present before #6060) traced to bash 3.x incompatibilities, a
macOS UTF-8 locale issue, and a missing fixture sync. This PR fixes all
five root causes.

Supersedes #6137 (that PR's branch was accidentally cut from a security
feature branch, pulling in unrelated files into the diff; this is a
clean rebase onto main with the same two commits plus a CodeRabbit fix).

## Related Issue

Investigation of CI run
[28539883104](https://github.com/NVIDIA/NemoClaw/actions/runs/28539883104);
failures also present in run
[28534214317](https://github.com/NVIDIA/NemoClaw/actions/runs/28534214317)
(before #6060), confirming #6060 is not the cause.

## Changes

- **`agents/hermes/start.sh`** — three bash 3.2 compatibility fixes:
- Replace bash 4.1+ named-FD `exec {var}<file` with a `{ } < file`
grouped redirect; variables assigned inside `{}` remain in function
scope
- Replace `mapfile -d '' -t` (bash 4.x only) with `while IFS= read -r -d
"" elem; do arr+=("$elem"); done`
- Guard `${_HERMES_GUARD_TIMEOUT[@]}` with `${arr[@]+"${arr[@]}"}` so
`set -u` does not abort the script when the array is empty (bash 3.2
treats empty `[@]` as unbound)
- **`scripts/gateway-control.sh`** — add `export LC_ALL=C` so `[a-f]`
character-class ranges in `case` patterns are byte-exact; macOS
`en_US.UTF-8` makes `[a-f]` case-insensitive, allowing uppercase hex
nonces to pass the `*[!0-9a-f]*` check
- **`scripts/lib/gateway-supervisor.sh`** — same `LC_ALL=C` fix for the
sourced library's nonce validation path
- **`test/gateway-supervisor-control.test.ts`** — pin
`NEMOCLAW_TEST_GATEWAY_CONTROL_CALLER_UID=0` in the nonce-rejection test
so it does not depend on the CI runner's UID; tighten macOS bash 3.2
SIGTERM filter from broad word-match to exact `Terminated: <digits>` /
`Killed: <digits>` format so unrelated stderr still fails the assertion
- **`test/e2e/fixtures/redaction.ts`** — add `tvly-` Tavily token
pattern missing since #6134, fixing the `e2e-redaction-parity`
`Array(16)` vs `Array(17)` mismatch

## Type of Change

- [x] Code change (feature, bug fix, or refactor)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [x] Docs not applicable — justification: shell compatibility and test
fixes, no user-facing behavior change
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging) —
`gateway-control.sh` and `gateway-supervisor.sh` handle nonce validation
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — the `LC_ALL=C` fix tightens nonce validation (rejects
uppercase hex on macOS that was previously accepted);
`gateway_control_stop_tracked_pid` behavior is unchanged

## Verification

- [x] Git hooks passed during commit and push
- [x] Targeted tests pass for changed behavior
  - `test/gateway-supervisor-control.test.ts`: 22/22 ✓
- `test/hermes-managed-exit-authorization.test.ts`: all ✓ (was 8
failures before)
  - `test/e2e/support/e2e-redaction-parity.test.ts`: 3/3 ✓
- `test/hermes-gateway-supervisor-recovery.test.ts`: 41/42 (1 local
flake — PID 4242 alive on dev machine; unrelated to these changes,
passes on CI fresh runners)
- [x] No secrets, API keys, or credentials committed

**Remaining failures not addressed in this PR** (different root class,
need separate investigation):
- `install-preflight.test.ts` — environment-specific
- `deepagents-code-tui-startup-check.test.ts` — needs investigation
- `platform-parity-cloud-experimental.test.ts` — needs investigation
- WSL `runtime-recovery-preload.test.ts`, `rebuild-config-hash.test.ts`
— different class of failure

---
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved gateway nonce validation to reliably accept only lowercase
hex characters, independent of locale and macOS environments.
* Fixed managed gateway startup, recovery, and live-status monitoring to
be compatible with older Bash versions.
* Tightened controller/marker parsing to reduce incorrect authorization
or status detection.
* Expanded secret redaction to cover additional Tavily-shaped tokens,
improving protection in logs and text output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
cv added a commit that referenced this pull request Jul 1, 2026
<!-- markdownlint-disable MD041 -->
## Summary

This follow-up extracts the direct mutable OpenClaw repair invocation
from the oversized shields module after #6060 merged. It preserves the
trusted-helper command, identity validation, timeouts, and runtime
behavior while documenting the existing cross-process lock and
first-start recovery-baseline semantics requested by automated review.

## Related Issue

Follow-up to #6060 and #6047.

## Changes

- Move sandbox UID/GID lookup and the trusted mutable-config normalizer
invocation into `src/lib/shields/mutable-config-repair.ts`.
- Keep runtime dependency lookup compatible with the existing Vitest
spies and module cache behavior.
- Document why the timer-bound host lock is the correct cross-process
serialization boundary for post-`exec` repair.
- Document why a missing recovery baseline is expected and intentionally
quiet before the first successful post-override capture.
- Pin the extracted privilege boundary with focused tests for sanitized
UID/GID probes, exact normalizer argv, invalid identity
short-circuiting, and Docker execution failures.

## Type of Change

- [x] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [x] Existing tests cover changed behavior — justification: the
existing shields-transition, mutable-permission, and post-`exec` cleanup
suites exercise the unchanged public path, identity validation, and
trusted-helper invocation.
- [ ] Tests not applicable — justification:
- [ ] Docs updated for user-facing behavior changes
- [x] Docs not applicable — justification: this is a behavior-preserving
extraction plus implementation comments; the #6060 command and
troubleshooting docs remain accurate.
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification: independent read-only
review found no security or correctness blocker; helper path, argv,
timeout, UID/GID validation, cross-process transition lock, and
fail-closed behavior are unchanged from merged #6060.
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Verification

- [x] PR description includes the DCO sign-off declaration and every
commit appears as `Verified` in GitHub
- [x] Git hooks passed during commit and push, or `npx prek run
--from-ref main --to-ref HEAD` passes
- [x] Targeted tests pass for changed behavior
- [ ] Full `npm test` passes (broad runtime changes only)
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [ ] `npm run docs` builds without warnings (doc changes only)
- [ ] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)

### Evidence

- `npx prek run --from-ref origin/main --to-ref HEAD` passed under the
repository-required `umask 022`, including the full CLI/integration
coverage hook.
- The normal push hook passed CLI type-checking and version
synchronization.
- Focused shields-transition, mutable-config-permission,
extracted-repair, and post-`exec` cleanup tests passed 49/49; CLI
type-checking, Biome, Python parsing, test-title style, file-size
budget, and `git diff --check` passed.
- The extracted-repair tests prove `stdin=false` and
`sanitizeEnvironment=true` on every privileged call, reject invalid
UID/GID values before normalization, assert forwarded Docker
argv/options, and propagate normalizer execution failures.
- The documentation-writer audit found no doc change necessary; `npm run
docs` completed with 0 errors and 2 pre-existing warnings.

---
Signed-off-by: Carlos Villela <cvillela@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Improved mutable configuration-permission repair to run the
normalization step inside a privileged sandbox, including sandbox
identity discovery and validation (UID/GID) before changes are applied.
- Added a time-bound execution wrapper around the normalization command
to prevent long-running operations.
- **Refactor**
- Moved the mutable-config permission normalization into a dedicated,
reusable module invoked by the existing repair workflow.
- **Tests**
- Added coverage for identity validation failures, error propagation,
and the exact privileged command invocation sequence.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
ericksoa pushed a commit that referenced this pull request Jul 2, 2026
## Summary
- Add the `v0.0.72` release-note section with links to the deeper docs
pages for installer recovery, command diagnostics, inference, policy,
and sandbox repair changes.
- Document the custom preset `allowed_ips` guard for user-authored
policy files.

## Related Issue
None.

## Source summary
- #6132 -> `docs/about/release-notes.mdx`: Summarizes installer and
upgrade recovery before generic onboarding, with links to quickstart and
lifecycle docs.
- #6087 -> `docs/network-policy/customize-network-policy.mdx`: Documents
that user-authored custom presets reject `allowed_ips` for ordinary
endpoints; also summarized in release notes.
- #5975 -> `docs/about/release-notes.mdx`: Summarizes safer curl-based
inference probes that keep API keys out of process arguments.
- #6044 -> `docs/about/release-notes.mdx`: Summarizes compact `channels
status` configuration reporting.
- #6096 -> `docs/about/release-notes.mdx`: Summarizes OpenClaw EC2
metadata discovery disablement and links to security guidance.
- #5980 and #5991 -> `docs/about/release-notes.mdx`: Summarizes `exec`
multiline argument rejection and recovery guidance.
- #6023 -> `docs/about/release-notes.mdx`: Summarizes
registered-provider diagnostics for `inference set` failures.
- #6074 -> `docs/about/release-notes.mdx`: Summarizes the refreshed
NVIDIA Endpoints featured-model selection behavior.
- #5969 -> `docs/about/release-notes.mdx`: Summarizes `credentials add`
provider credential registration.
- #6060 -> `docs/about/release-notes.mdx`: Summarizes mutable OpenClaw
config permission restoration after `exec`.
- #6134 -> `docs/about/release-notes.mdx`: Summarizes restored Tavily
access for managed Python workflows.
- #6089 -> `docs/about/release-notes.mdx`: Summarizes Hermes runtime
version-scheme comparison during upgrade checks.
- #6131 -> `docs/about/release-notes.mdx`: Summarizes OpenClaw gateway
watchdog recovery behavior.
- #5976 and #5990 -> `docs/about/release-notes.mdx`: Summarizes prompt
stdin EOF cancellation behavior during onboarding.
- #5540 -> `docs/about/release-notes.mdx`: Summarizes clarified
host-level and per-sandbox status command scope.
- #5978 and #6018 -> `docs/about/release-notes.mdx`: Summarizes
policy-denial log breadcrumbs in connect shells.

## Testing
- `npm run docs:sync-agent-variants`
- `npm run docs`
- Commit hooks passed during `git commit`, including commitlint and
gitleaks.
- Pre-push hook passed during `git push`, including TypeScript CLI and
package/tag version sync.

## Checklist
- [x] Documentation updated.
- [x] `npm run docs` completed with 0 errors and 1 existing Fern
warning.
- [x] No source code or generated build artifacts committed.

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added release notes for v0.0.72 covering improved installer recovery,
clearer CLI diagnostics, safer inference setup and provider switching,
better credential handling, stronger policy boundaries, and more robust
runtime repair behavior.
* Updated network policy guidance to clarify when `allowed_ips` can be
used, including a specific exception for the sandbox-to-host bridge
endpoint.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Hadar301 pushed a commit to Hadar301/NemoClaw-OpenShift that referenced this pull request Jul 12, 2026
…IDIA#6060)

<!-- markdownlint-disable MD041 -->
## Summary

`openclaw doctor --fix` can collapse mutable OpenClaw paths from
NemoClaw's multi-UID `2770/660` contract to OpenClaw's single-user
`700/600` defaults. This change restores that contract after both
entrypoint one-shot commands and the documented `nemoclaw <name> exec`
boundary, with deterministic child-versus-cleanup exit-status
precedence.

## Related Issue

Fixes NVIDIA#6047

## Changes

- Supervise entrypoint one-shot commands so `TERM` and `INT` are
forwarded, the direct child is reaped, permission cleanup always runs,
and the remote status is preserved when cleanup succeeds.
- After public OpenShell exec returns, inspect registered OpenClaw
sandboxes, repair only detected mutable-permission drift through the
installed descriptor-safe normalizer while holding the timer-bound
shields mutation lock, and require successful re-inspection. Hermes,
custom agents, unregistered sandboxes, and active shields locks remain
untouched.
- Move mutable-tree normalization, baseline capture, and empty-config
recovery into an installed root-trusted Python helper that operates
through pinned, no-follow descriptors.
- Authenticate the permanently privilege-dropped owner child with a
private Unix socket, `SO_PASSCRED`, exact credentials, and `SCM_RIGHTS`;
retain the exact directory/config descriptors across the privilege
boundary.
- Replace recovery baseline/config/hash entries with fresh inodes so
hardlinks, symlink swaps, directory replacement, and inode-reuse races
cannot turn root into a confused deputy.
- Fail closed on missing trusted helper, unexpected ownership, unsafe
links, metadata changes, malformed descriptor handoff, or incomplete
verification.
- Document host-side cleanup behavior, atomic recovery, failure
precedence, and safe operator recovery guidance.
- Add unit, integration, container, and live-target coverage for
permission drift, signals, cleanup precedence, capability loss, hardlink
safety, path swaps, protected symlinks, and trusted-helper selection.

## Type of Change

- [ ] Code change (feature, bug fix, or refactor)
- [x] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [ ] Existing tests cover changed behavior — justification:
- [ ] Tests not applicable — justification:
- [x] Docs updated for user-facing behavior changes
- [ ] Docs not applicable — justification:
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification: independent review
through runtime head `85b0744310553ec4a141bdc031cdc881e8c57422`
confirmed the previously reproducible cross-phase ABA, external-hardlink
ownership/mode mutation, path-based recovery TOCTOU, root
helper-selection, and earlier-tree hardlink-alias issues are fixed.
Follow-up `64d6234a9198e14e582155ebae4c75c362373f10` changes test
control flow only. The owner child is permanently privilege-dropped
before recursive mutation and root performs no work without an
authenticated descriptor handoff. No runtime privilege-boundary blocker
remains; the same-UID child retains only authority that the sandbox user
already has over its own inode.
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Verification

- [x] PR description includes the DCO sign-off declaration and every
commit appears as `Verified` in GitHub
- [x] Git hooks passed during commit and push, or `npx prek run
--from-ref main --to-ref HEAD` passes
- [x] Targeted tests pass for changed behavior
- [ ] Full `npm test` passes (broad runtime changes only)
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [ ] `npm run docs` builds without warnings (doc changes only)
- [x] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)

### Evidence

- Signed commit and push hooks: repository checks, Biome, ShellCheck,
Hadolint, gitleaks, CLI typecheck, full CLI test hook, source-shape
budget, and test-size budget passed.
- Focused host tests: the final runtime correction passed 67 CLI and 145
integration tests after the earlier focused suites; CLI type-checking
passed.
- Fresh production image: build passed; new E2E cases 30–30f all passed.
The full script reported 40 passes and two unrelated stale base-image
profile assertions.
- Independent container probes passed normal repair, exact-config
capture, empty-config recovery, hardlink/protected-target invariants,
and the final `700/600` to `2770/660` production-image repair path.
- Full CLI/coverage hooks, repository checks, source-shape budget, and
test-size budget passed under the repository's expected `umask 022`.
- Docs build completed with 0 errors and 2 pre-existing warnings;
agent-variant synchronization and docs checks passed.
- Security test-depth follow-ups remain non-blocking: malformed
ancillary-message variants, an isolated missing-`CAP_SETUID` case, 16
MiB boundary/source-mutation/temp-cleanup cases, exact-image provenance
when reusing an existing E2E tag, and a same-UID concurrent post-check
hardlink race that cannot increase the child process's existing
authority.

---

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added safer one-shot command handling with post-command config
permission cleanup.
* Improved config recovery and permission repair behavior for OpenClaw
environments.
* Updated documentation to describe the new cleanup and recovery
behavior more clearly.

* **Bug Fixes**
* Hardened config permission handling against symlinks, ownership
mismatches, and concurrent changes.
* Improved failure handling so unsafe or incomplete repairs now fail
closed with clearer status reporting.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Aaron Erickson <aerickson@nvidia.com>
Co-authored-by: Carlos Villela <cvillela@nvidia.com>
Hadar301 pushed a commit to Hadar301/NemoClaw-OpenShift that referenced this pull request Jul 12, 2026
## Summary

Five pre-existing test failures on the `macos-vitest` CI workflow
(confirmed present before NVIDIA#6060) traced to bash 3.x incompatibilities, a
macOS UTF-8 locale issue, and a missing fixture sync. This PR fixes all
five root causes.

Supersedes NVIDIA#6137 (that PR's branch was accidentally cut from a security
feature branch, pulling in unrelated files into the diff; this is a
clean rebase onto main with the same two commits plus a CodeRabbit fix).

## Related Issue

Investigation of CI run
[28539883104](https://github.com/NVIDIA/NemoClaw/actions/runs/28539883104);
failures also present in run
[28534214317](https://github.com/NVIDIA/NemoClaw/actions/runs/28534214317)
(before NVIDIA#6060), confirming NVIDIA#6060 is not the cause.

## Changes

- **`agents/hermes/start.sh`** — three bash 3.2 compatibility fixes:
- Replace bash 4.1+ named-FD `exec {var}<file` with a `{ } < file`
grouped redirect; variables assigned inside `{}` remain in function
scope
- Replace `mapfile -d '' -t` (bash 4.x only) with `while IFS= read -r -d
"" elem; do arr+=("$elem"); done`
- Guard `${_HERMES_GUARD_TIMEOUT[@]}` with `${arr[@]+"${arr[@]}"}` so
`set -u` does not abort the script when the array is empty (bash 3.2
treats empty `[@]` as unbound)
- **`scripts/gateway-control.sh`** — add `export LC_ALL=C` so `[a-f]`
character-class ranges in `case` patterns are byte-exact; macOS
`en_US.UTF-8` makes `[a-f]` case-insensitive, allowing uppercase hex
nonces to pass the `*[!0-9a-f]*` check
- **`scripts/lib/gateway-supervisor.sh`** — same `LC_ALL=C` fix for the
sourced library's nonce validation path
- **`test/gateway-supervisor-control.test.ts`** — pin
`NEMOCLAW_TEST_GATEWAY_CONTROL_CALLER_UID=0` in the nonce-rejection test
so it does not depend on the CI runner's UID; tighten macOS bash 3.2
SIGTERM filter from broad word-match to exact `Terminated: <digits>` /
`Killed: <digits>` format so unrelated stderr still fails the assertion
- **`test/e2e/fixtures/redaction.ts`** — add `tvly-` Tavily token
pattern missing since NVIDIA#6134, fixing the `e2e-redaction-parity`
`Array(16)` vs `Array(17)` mismatch

## Type of Change

- [x] Code change (feature, bug fix, or refactor)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [x] Docs not applicable — justification: shell compatibility and test
fixes, no user-facing behavior change
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging) —
`gateway-control.sh` and `gateway-supervisor.sh` handle nonce validation
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — the `LC_ALL=C` fix tightens nonce validation (rejects
uppercase hex on macOS that was previously accepted);
`gateway_control_stop_tracked_pid` behavior is unchanged

## Verification

- [x] Git hooks passed during commit and push
- [x] Targeted tests pass for changed behavior
  - `test/gateway-supervisor-control.test.ts`: 22/22 ✓
- `test/hermes-managed-exit-authorization.test.ts`: all ✓ (was 8
failures before)
  - `test/e2e/support/e2e-redaction-parity.test.ts`: 3/3 ✓
- `test/hermes-gateway-supervisor-recovery.test.ts`: 41/42 (1 local
flake — PID 4242 alive on dev machine; unrelated to these changes,
passes on CI fresh runners)
- [x] No secrets, API keys, or credentials committed

**Remaining failures not addressed in this PR** (different root class,
need separate investigation):
- `install-preflight.test.ts` — environment-specific
- `deepagents-code-tui-startup-check.test.ts` — needs investigation
- `platform-parity-cloud-experimental.test.ts` — needs investigation
- WSL `runtime-recovery-preload.test.ts`, `rebuild-config-hash.test.ts`
— different class of failure

---
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved gateway nonce validation to reliably accept only lowercase
hex characters, independent of locale and macOS environments.
* Fixed managed gateway startup, recovery, and live-status monitoring to
be compatible with older Bash versions.
* Tightened controller/marker parsing to reduce incorrect authorization
or status detection.
* Expanded secret redaction to cover additional Tavily-shaped tokens,
improving protection in logs and text output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Hadar301 pushed a commit to Hadar301/NemoClaw-OpenShift that referenced this pull request Jul 12, 2026
<!-- markdownlint-disable MD041 -->
## Summary

This follow-up extracts the direct mutable OpenClaw repair invocation
from the oversized shields module after NVIDIA#6060 merged. It preserves the
trusted-helper command, identity validation, timeouts, and runtime
behavior while documenting the existing cross-process lock and
first-start recovery-baseline semantics requested by automated review.

## Related Issue

Follow-up to NVIDIA#6060 and NVIDIA#6047.

## Changes

- Move sandbox UID/GID lookup and the trusted mutable-config normalizer
invocation into `src/lib/shields/mutable-config-repair.ts`.
- Keep runtime dependency lookup compatible with the existing Vitest
spies and module cache behavior.
- Document why the timer-bound host lock is the correct cross-process
serialization boundary for post-`exec` repair.
- Document why a missing recovery baseline is expected and intentionally
quiet before the first successful post-override capture.
- Pin the extracted privilege boundary with focused tests for sanitized
UID/GID probes, exact normalizer argv, invalid identity
short-circuiting, and Docker execution failures.

## Type of Change

- [x] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [x] Existing tests cover changed behavior — justification: the
existing shields-transition, mutable-permission, and post-`exec` cleanup
suites exercise the unchanged public path, identity validation, and
trusted-helper invocation.
- [ ] Tests not applicable — justification:
- [ ] Docs updated for user-facing behavior changes
- [x] Docs not applicable — justification: this is a behavior-preserving
extraction plus implementation comments; the NVIDIA#6060 command and
troubleshooting docs remain accurate.
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification: independent read-only
review found no security or correctness blocker; helper path, argv,
timeout, UID/GID validation, cross-process transition lock, and
fail-closed behavior are unchanged from merged NVIDIA#6060.
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Verification

- [x] PR description includes the DCO sign-off declaration and every
commit appears as `Verified` in GitHub
- [x] Git hooks passed during commit and push, or `npx prek run
--from-ref main --to-ref HEAD` passes
- [x] Targeted tests pass for changed behavior
- [ ] Full `npm test` passes (broad runtime changes only)
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [ ] `npm run docs` builds without warnings (doc changes only)
- [ ] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)

### Evidence

- `npx prek run --from-ref origin/main --to-ref HEAD` passed under the
repository-required `umask 022`, including the full CLI/integration
coverage hook.
- The normal push hook passed CLI type-checking and version
synchronization.
- Focused shields-transition, mutable-config-permission,
extracted-repair, and post-`exec` cleanup tests passed 49/49; CLI
type-checking, Biome, Python parsing, test-title style, file-size
budget, and `git diff --check` passed.
- The extracted-repair tests prove `stdin=false` and
`sanitizeEnvironment=true` on every privileged call, reject invalid
UID/GID values before normalization, assert forwarded Docker
argv/options, and propagate normalizer execution failures.
- The documentation-writer audit found no doc change necessary; `npm run
docs` completed with 0 errors and 2 pre-existing warnings.

---
Signed-off-by: Carlos Villela <cvillela@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Improved mutable configuration-permission repair to run the
normalization step inside a privileged sandbox, including sandbox
identity discovery and validation (UID/GID) before changes are applied.
- Added a time-bound execution wrapper around the normalization command
to prevent long-running operations.
- **Refactor**
- Moved the mutable-config permission normalization into a dedicated,
reusable module invoked by the existing repair workflow.
- **Tests**
- Added coverage for identity validation failures, error propagation,
and the exact privileged command invocation sequence.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Hadar301 pushed a commit to Hadar301/NemoClaw-OpenShift that referenced this pull request Jul 12, 2026
## Summary
- Add the `v0.0.72` release-note section with links to the deeper docs
pages for installer recovery, command diagnostics, inference, policy,
and sandbox repair changes.
- Document the custom preset `allowed_ips` guard for user-authored
policy files.

## Related Issue
None.

## Source summary
- NVIDIA#6132 -> `docs/about/release-notes.mdx`: Summarizes installer and
upgrade recovery before generic onboarding, with links to quickstart and
lifecycle docs.
- NVIDIA#6087 -> `docs/network-policy/customize-network-policy.mdx`: Documents
that user-authored custom presets reject `allowed_ips` for ordinary
endpoints; also summarized in release notes.
- NVIDIA#5975 -> `docs/about/release-notes.mdx`: Summarizes safer curl-based
inference probes that keep API keys out of process arguments.
- NVIDIA#6044 -> `docs/about/release-notes.mdx`: Summarizes compact `channels
status` configuration reporting.
- NVIDIA#6096 -> `docs/about/release-notes.mdx`: Summarizes OpenClaw EC2
metadata discovery disablement and links to security guidance.
- NVIDIA#5980 and NVIDIA#5991 -> `docs/about/release-notes.mdx`: Summarizes `exec`
multiline argument rejection and recovery guidance.
- NVIDIA#6023 -> `docs/about/release-notes.mdx`: Summarizes
registered-provider diagnostics for `inference set` failures.
- NVIDIA#6074 -> `docs/about/release-notes.mdx`: Summarizes the refreshed
NVIDIA Endpoints featured-model selection behavior.
- NVIDIA#5969 -> `docs/about/release-notes.mdx`: Summarizes `credentials add`
provider credential registration.
- NVIDIA#6060 -> `docs/about/release-notes.mdx`: Summarizes mutable OpenClaw
config permission restoration after `exec`.
- NVIDIA#6134 -> `docs/about/release-notes.mdx`: Summarizes restored Tavily
access for managed Python workflows.
- NVIDIA#6089 -> `docs/about/release-notes.mdx`: Summarizes Hermes runtime
version-scheme comparison during upgrade checks.
- NVIDIA#6131 -> `docs/about/release-notes.mdx`: Summarizes OpenClaw gateway
watchdog recovery behavior.
- NVIDIA#5976 and NVIDIA#5990 -> `docs/about/release-notes.mdx`: Summarizes prompt
stdin EOF cancellation behavior during onboarding.
- NVIDIA#5540 -> `docs/about/release-notes.mdx`: Summarizes clarified
host-level and per-sandbox status command scope.
- NVIDIA#5978 and NVIDIA#6018 -> `docs/about/release-notes.mdx`: Summarizes
policy-denial log breadcrumbs in connect shells.

## Testing
- `npm run docs:sync-agent-variants`
- `npm run docs`
- Commit hooks passed during `git commit`, including commitlint and
gitleaks.
- Pre-push hook passed during `git push`, including TypeScript CLI and
package/tag version sync.

## Checklist
- [x] Documentation updated.
- [x] `npm run docs` completed with 0 errors and 1 existing Fern
warning.
- [x] No source code or generated build artifacts committed.

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added release notes for v0.0.72 covering improved installer recovery,
clearer CLI diagnostics, safer inference setup and provider switching,
better credential handling, stronger policy boundaries, and more robust
runtime repair behavior.
* Updated network policy guidance to clarify when `allowed_ips` can be
used, including a specific exception for the sandbox-to-host bridge
endpoint.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@wscurran wscurran added area: security Security controls, permissions, secrets, or hardening bug-fix PR fixes a bug or regression integration: openclaw OpenClaw integration behavior labels Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: security Security controls, permissions, secrets, or hardening bug-fix PR fixes a bug or regression integration: openclaw OpenClaw integration behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract

5 participants