Skip to content

fix(hermes): keep the locked config root writable by the gateway - #7876

Merged
prekshivyas merged 5 commits into
mainfrom
fix/hermes-shields-locked-config-root-7865
Jul 30, 2026
Merged

fix(hermes): keep the locked config root writable by the gateway#7876
prekshivyas merged 5 commits into
mainfrom
fix/hermes-shields-locked-config-root-7865

Conversation

@yanyunl1991

@yanyunl1991 yanyunl1991 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

shields up left the Hermes config root root-owned with no group write, which took away
the directory the gateway needs to create its own top-level runtime state. Every later
gateway restart failed its health check and the in-sandbox supervisor quarantined relaunch
until the sandbox was recreated. This PR locks that root the way the surrounding code
already locks its parent — root-owned in the sandbox group, keeping the set-id and sticky
bits — so the gateway keeps working under lockdown while the seal is unchanged.

Closes #7865.

Reproduction

Run on our Ubuntu 24.04 x86_64 test host (no GPU), against a Hermes sandbox onboarded from
main in this run:

# 1. fresh Hermes sandbox, shields in the default mutable state
nemoclaw <name> gateway restart      # exit 0, twice in a row
# 2. shields up — the only change
nemoclaw <name> shields up
# 3. restart the gateway
nemoclaw <name> gateway restart
nemoclaw <name> recover

Environment

  • Test machine: our Ubuntu 24.04 x86_64 test host (no GPU)
  • Linux 6.14 x86_64, Node v22.22.2, Docker 28.2.2, OpenShell 0.0.85
  • Hermes Agent v0.18.0 (repro) / v0.19.0 (verification rebuild), provider ollama-local
  • NemoClaw main eeab81cc5 at repro time; this branch includes current main at 25de83d37

Observed on main (before fix)

Step 1 succeeds repeatedly. shields up then reports success, and the config integrity
still matches — but the config root changed shape:

before:  drwx------  sandbox sandbox  /sandbox/.hermes
after:   drwxr-xr-x  root    root     /sandbox/.hermes

Step 3, the same command that just succeeded twice:

  Failure layer: health timeout - gateway restart failed for '<name>'.
  NEMOCLAW_START_LOG=[gateway] Hermes replacement gateway failed listener or health validation; stopping the exact child
  NEMOCLAW_START_LOG=[gateway] CRITICAL: 5 exits in 60s window — Hermes relaunch is quarantined until sandbox recreation; check /tmp/gateway.log

/tmp/gateway.log inside the sandbox:

WARNING gateway.drain_control: drain-control: failed to read /sandbox/.hermes/.drain_request.json: [Errno 13] Permission denied
WARNING gateway.platforms.base: Failed to write runtime status (disconnected) for api_server: [Errno 13] Permission denied: '/sandbox/.hermes/.gateway_state_dxl_j9xy.tmp'
ERROR gateway.run: PID file race lost to another gateway instance. Exiting.

recover cannot clear it and the sandbox is left without a gateway.

Observed on fix/... (after fix)

1775 root:sandbox  /sandbox
3770 root:sandbox  /sandbox/.hermes
444 root:root      /sandbox/.hermes/config.yaml
444 root:root      /sandbox/.hermes/.env

  Shields: UP (lockdown active)
RESTART_EXIT=0    ✓ Gateway restarted; health passed; forwards checked/recovered
RESTART2_EXIT=0   ✓ Gateway restarted; health passed; forwards checked/recovered
RECOVER_EXIT=0    Probe complete: Hermes Agent gateway is running
                  Hermes Agent: running

Zero quarantine, permission-denied, or refusal lines in the boot. The security contract was
re-checked against the live locked tree with the ordinary sandbox identity:

create_runtime_state: OK
unlink_own:           OK
unlink_sealed:  rm: cannot remove '/sandbox/.hermes/config.yaml': Operation not permitted
rename_sealed:  mv: cannot move  '/sandbox/.hermes/config.yaml': Operation not permitted
unlink_hash:    rm: cannot remove '/sandbox/.hermes/.config-hash': Operation not permitted
modify_sealed:  cannot create '/sandbox/.hermes/config.yaml': Permission denied
/sandbox/.hermes/config.yaml: OK
/sandbox/.hermes/.env: OK

A full shields down → mutable restart → shields up → restart round trip also passes, and
the mutable posture is unchanged (3770 sandbox:sandbox, config 640 sandbox:sandbox).

Analysis

_configure_shields_target_metadata in agents/hermes/runtime-config-guard.py set the
locked config root to root:root 0755. The Hermes config root is not only configuration:
auth.json, .drain_request.json, and the temporary files behind every atomic
gateway_state.json / gateway.pid replace are created directly in /sandbox/.hermes. An
atomic replace must stage its temporary file in the target's own directory, so the existing
-> runtime/ symlinks do not help. In the OpenShell-managed topology the gateway runs as
the sandbox user, so a root-owned root without group write means no gateway can start.
Five failed launches inside 60s trip the crash budget and
recover_hermes_gateway_current_user quarantines relaunch. rebuild re-applies the saved
lockdown, so a sandbox that had ever been shielded reached this state on its next restart.

This posture contradicts what the surrounding code already does and already states:

  • The same function locks the parent as root:<sandbox gid> 01775 — root-owned, sticky
    — commented as keeping /sandbox "a usable home" while preventing the sandbox identity
    from renaming the lock root out from under the protected files.
  • src/lib/shields/index.ts documents the Hermes root as staying "group-writable + sticky
    so Hermes can create top-level runtime state while the gateway UID cannot remove
    sandbox-owned config files" — implemented for the mutable posture only.
  • agents/hermes/start.sh already expects root:sandbox 1775 for the parent in
    hermes_locked_parent_is_protected, and comments at its locked branch that "the
    locked-root posture seals config.yaml/.env, not the dir".
  • The 2026-05-26 changelog describes making /sandbox/.hermes "sticky group-writable so the
    gateway user can create runtime state without removing sandbox-owned config files".

Worth noting for the threat model: chattr +i is not available on the overlay filesystem
these sandboxes use (lsattr shows only the extent flag), so the durable protection for the
sealed files is directory-level. Before this change that came from the root being
unwritable; now it comes from the sticky bit, which is equally binding and was verified
above against a live tree.

Fix

The locked root becomes root:<sandbox gid> 03770; only the owner differs between postures
now, and the locked root additionally loses the world bits 0755 granted. The seal itself
is untouched at 444 root:root. Every site that encodes the locked posture is updated, and
each keeps accepting the previous root:root 0755 so an already-shielded sandbox still
boots, rolls back, and re-locks rather than bricking on upgrade:

Site Change
runtime-config-guard.py _configure_shields_target_metadata locked root target posture
runtime-config-guard.py _is_locked_hermes_root (new) one predicate for locked-root detection, reused by the restart-seal and shields-seal paths that previously inlined root:root 0755
validate-env-secret-boundary.py boot-time trusted-posture allowlist; without this an upgraded CLI refuses startup on every already-shielded sandbox
start.sh hermes_config_root_is_locked entrypoint locked-root detection, which gates four locked-aware branches
start.sh refresh_hermes_runtime_config_hashes skip the compat refresh under a locked root
src/lib/shields/verify-lock.ts agent-aware expected locked dir posture
src/lib/shields/index.ts legacy Hermes transition lock branch

Two things the live verification forced out, both required for the fix to be complete:

  1. The compat hash refresh. prepare_hermes_nonroot_runtime unconditionally runs
    refresh_hermes_runtime_config_hashes compat, which atomically replaces .config-hash
    as the sandbox identity. Under lockdown that file is sealed and root-owned, so the sticky
    root now refuses the rename and the launch still failed. It is also meaningless work: the
    sealed inputs cannot drift, and the lock transaction already wrote a coherent hash. The
    guard sits in the function so all three compat callers are covered; strict/both
    (root entrypoint) are untouched, and the inspect_hermes_mcp_integrity that follows
    still validates the sealed hash, so an incoherent locked tree keeps failing closed.
  2. Applied-transition resume. The pending phase clamps the root to a transient root-only
    mode. That clamp used to equal the final locked mode, so nothing noticed that the
    applied-resume branch never re-applies the recorded root posture. With the two values now
    different, an interruption between publishing the applied phase and restoring the seal
    would leave the clamp in place and finish would refuse the drifted root, wedging the
    transaction. Resume now re-applies the recorded posture behind the existing inode pin, so
    it converges.

Not changed, deliberately: the OpenClaw locked posture stays 755 root:root (a new test
pins it); the sealed file modes, the strict/compat hash contracts, the /sandbox parent
posture, and the unavailable-config root-only clamp are all untouched. No refusal is
relaxed.

Upgrade path. The host verifier reports a pre-#7865 755 root:root root as drift, so
shields status surfaces it and the caller's re-lock repairs it; rolling back to locked
re-derives the target rather than replaying the captured original, so a down/up cycle also
heals it. Verified on a sandbox that started from the old posture.

One behavioural note: a gateway restart issued immediately after a shields up
transition can still report health timeout while the transition settles; the next restart
succeeds and the sandbox is never quarantined. That is visible in the verification log and
is unrelated to the permission failure this PR fixes.

Alternative if the posture change is not acceptable

This PR widens one capability: under lockdown the sandbox identity can now create new
top-level entries in /sandbox/.hermes. The reasoning for why that is not a meaningful
weakening is in Analysis above — the sealed entries keep both of their guarantees, and
the rest of the tree under a locked root is already sandbox-writable
(logs, sessions, cache, runtime, bin, kanban, SOUL.md, gateway.pid are all
sandbox-owned and writable while shields are up), so the old root-owned root was not a
containment boundary for the subtree — it only blocked the one operation Hermes needs on
every launch.

If you would rather keep that capability denied, the honest alternative is not a smaller
version of this change. Relocating the writes is not available to us: the failing paths are
Hermes' own (gateway.run, gateway.drain_control, gateway.platforms.base), and the
-> runtime/ symlinks NemoClaw already installs cannot cover them, because an atomic
replace must stage its temporary file in the target's own directory.

The alternative is therefore to make shields up refuse on a Hermes sandbox whose gateway
runs as the sandbox identity
(the OpenShell-managed non-root topology), with an explicit
error naming the reason, instead of producing a sandbox whose next gateway restart
quarantines it. The direct root-entrypoint topology, where the gateway runs as its own
gateway uid, would keep working with today's posture.

Trade-offs, stated plainly:

  • It removes a documented capability from managed sandboxes rather than fixing it, so
    shields up stops being available on the default onboarding topology.
  • It is a much smaller and more conservative diff: no posture change, no boot-time allowlist
    change, no entrypoint changes. Most of this PR would be dropped.
  • Existing shielded managed sandboxes stay broken until they are recreated; this PR repairs
    them on the next shields up instead.

I did not take that route because the code and history read as if the group-writable, sticky
root is the intended posture for this directory and the locked branch simply never adopted
it (the four citations in Analysis). If that reading is wrong and the root-owned root was
deliberate, say so and I will replace this PR with the refusal.

Changes

  • agents/hermes/runtime-config-guard.py: locked root target posture, _is_locked_hermes_root, applied-resume posture repair
  • agents/hermes/validate-env-secret-boundary.py: boot-time trusted-posture allowlist
  • agents/hermes/start.sh: locked-root detection, compat hash refresh guard
  • agents/hermes/Dockerfile: pinned validator SHA-256 refreshed
  • src/lib/shields/verify-lock.ts: agent-aware expected locked dir posture
  • src/lib/shields/index.ts: legacy Hermes transition lock branch
  • test/hermes-runtime-config-guard-topology.test.ts: new root-container capability test; rollback expectation
  • test/hermes-restart-config-seal-{transition,recovery,hostile-input}.test.ts: locked-posture expectations; legacy-posture regression lock
  • src/lib/shields/{verify-lock,legacy-hermes-transition,legacy-hermes-compat}.test.ts: locked-posture expectations, drift-on-legacy and OpenClaw-unchanged cases
  • ci/source-shape-test-budget.json: security contract exception for the new root-container test
  • docs/reference/troubleshooting.mdx: symptom, expected posture, and repair

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: Codex Desktop security review passed all nine required categories with no findings; it verified the agent-specific lock posture, sticky-bit sealed-file protection, legacy upgrade handling, inode-pinned transition recovery, and kernel capability coverage.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Documentation Writer Review

  • Documentation writer subagent reviewed the completed changes
  • Result: docs-updated
  • Evidence: Updated docs/reference/troubleshooting.mdx for legacy Hermes locked-configuration-root drift, pre-quarantine repair order, acceptance checks, and the relaunch-quarantine rebuild route. Independent review covered all PR text, verified commands and security claims against source and tests, and inspected the OpenClaw, Hermes, and Deep Agents renders. npm run docs, npm run source-shape:check, and targeted hooks passed.
  • Agent: Codex Desktop

DGX Station Hardware Evidence

  • Tested on DGX Station — not applicable; scripts/prepare-dgx-station-host.sh is unchanged.
  • Tested commit: Not applicable
  • Station profile/scenario: Not applicable
  • Result: Not applicable
  • Supporting evidence: Not applicable

Verification

  • PR description includes a Signed-off-by: line and every commit appears as Verified in GitHub
  • Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run validate:pr passed after refreshing origin/main when hooks were skipped or unavailable
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — npx vitest run --project cli src/lib/shields/verify-lock.test.ts: 24 passed; npm run source-shape:check: passed
  • Applicable broad gate passed — fresh current-head CI and E2E are running
  • 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) — 0 errors; two unrelated repository/environment warnings remain for unauthenticated redirect checking and the existing light-mode accent contrast
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only) — not applicable; no page was added

AI Disclosure

  • AI-assisted — tools: Claude Code, Codex Desktop

Signed-off-by: Yanyun Liao yanyunl@nvidia.com

Summary by CodeRabbit

  • Bug Fixes
    • Improved Hermes shields-transition restart/resume by restoring the runtime directory attributes needed to continue safely.
    • Strengthened Hermes runtime config guard and locked-posture drift handling, with more robust legacy compatibility.
  • Security
    • Refreshed integrity verification for the Hermes environment-secret boundary validator.
    • Hardened Hermes locked config directory posture to consistently use root:sandbox with setgid/sticky mode (3770).
  • Tests
    • Updated and added Hermes lock/restart/seal, topology, and verify-lock tests to match the new locked posture expectations.
  • Documentation
    • Updated Hermes troubleshooting guidance for configuration-root drift and recovery, including the expected 3770 root:sandbox permissions.

`shields up` moved /sandbox/.hermes to root:root 0755. That directory is
not only configuration: Hermes creates auth.json, the drain request, and
the temporary files behind every atomic gateway_state/pid replace
directly inside it, so the sandbox identity lost the ability to start a
gateway at all. Every restart failed its health check, five exits inside
60s tripped the crash budget, and the supervisor quarantined relaunch
until the sandbox was recreated. Because rebuild re-applies the saved
lockdown, any sandbox that had ever been shielded reached that state on
its next gateway restart.

Lock the root the way the code already locks its parent and already
documents this root: root-owned in the sandbox group, keeping the set-id
and sticky bits, so the gateway manages its own runtime state while the
sticky bit stops the sandbox identity from unlinking or renaming the
sealed root-owned config. The seal itself is unchanged at 444 root:root,
and the locked root loses the world bits 0755 used to grant.

Teach every site that encodes the locked posture: the guard's shields
target and its locked-root predicate, the boot-time trusted-posture
allowlist, the entrypoint's locked-root detection, the host verifier,
and the legacy transition. Each keeps accepting the previous root:root
0755 root so an already-shielded sandbox still boots, rolls back, and
re-locks; the host verifier reports it as drift so `shields up` repairs
it. Skip the compat hash refresh under a locked root, which the sandbox
identity can neither perform nor need, and re-apply the recorded root
posture when an applied shields transition resumes, so an interruption
converges instead of wedging on a drifted root.

Fixes #7865

Signed-off-by: Yanyun Liao <yanyunl@nvidia.com>
@yanyunl1991 yanyunl1991 added bug-fix PR fixes a bug or regression platform: ubuntu Affects Ubuntu Linux environments v0.0.98 labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Hermes shields locking now establishes a root:sandbox 3770 config root, preserves sealed-file protection, supports legacy posture classification, restores metadata during transition resume, and updates verification, startup handling, tests, documentation, and image security pins.

Changes

Hermes locked-root posture

Layer / File(s) Summary
Locked-root contract and verification
src/lib/shields/verify-lock.ts, src/lib/shields/index.ts, src/lib/shields/*test.ts, test/e2e/live/*
Hermes verification and legacy lock transitions now expect root:sandbox with mode 3770; OpenClaw retains root:root with mode 755.
Runtime transition and resume handling
agents/hermes/runtime-config-guard.py, test/hermes-restart-config-seal-*.test.ts, test/hermes-runtime-config-guard-topology.test.ts, ci/source-shape-test-budget.json
Runtime posture classification, locked metadata application, and applied-transition resumption preserve sandbox ownership and locked permissions, with tests covering restart, rollback, resume, and sealed-file access.
Startup compatibility and validation
agents/hermes/start.sh, agents/hermes/validate-env-secret-boundary.py, agents/hermes/Dockerfile, docs/reference/troubleshooting.mdx
Startup compatibility recognizes the new locked root, skips incompatible hash refreshes, directory validation accepts the posture, image integrity checks are refreshed and expanded, and troubleshooting documents legacy permissions and quarantine recovery.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: integration: hermes, bug-fix, v0.0.99

Suggested reviewers: cv, apurvvkumaria

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant RuntimeConfigGuard
  participant HermesRoot
  participant HermesGateway
  CLI->>RuntimeConfigGuard: shields up
  RuntimeConfigGuard->>HermesRoot: set root:sandbox 3770 posture
  RuntimeConfigGuard->>HermesRoot: seal protected config files
  HermesGateway->>HermesRoot: write runtime state
  HermesGateway->>HermesRoot: attempt sealed-file mutation
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes match #7865 by switching /sandbox/.hermes to root:sandbox 3770 while keeping sealed files read-only and preserving restart/recovery.
Out of Scope Changes check ✅ Passed The diff stays focused on Hermes lock posture, validation, tests, docs, and related build pins; no unrelated code paths stand out.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: enabling the gateway to write runtime state in the locked Hermes config root while preserving sealed config protection.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hermes-shields-locked-config-root-7865

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

@github-code-quality

github-code-quality Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit e4509a5 in the fix/hermes-shields-l... branch remains at 96%, unchanged from commit 25de83d in the main branch.

TypeScript / code-coverage/cli

The overall coverage in commit e4509a5 in the fix/hermes-shields-l... branch remains at 81%, unchanged from commit 25de83d in the main branch.

Show a code coverage summary of the most impacted files.
File main 25de83d fix/hermes-shields-l... e4509a5 +/-
src/lib/onboard...ndbox-create.ts 83% 33% -50%
src/lib/onboard...-create-plan.ts 88% 75% -13%
src/lib/onboard...ndbox-create.ts 91% 83% -8%
src/lib/domain/.../connect-env.ts 97% 89% -8%
src/lib/sandbox...rce-identity.ts 88% 88% 0%
src/lib/tunnel/services.ts 73% 73% 0%
src/lib/shields/verify-lock.ts 96% 100% +4%

Updated July 30, 2026 18:16 UTC

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — No blocking findings reported

Advisor assessment: No blocking advisor findings reported
Next action: Review the warnings below.
Findings: 0 blockers · 1 warning · 1 suggestion

Model lanes

  • GPT-5.6 Terra (primary): Completed · medium confidence · 0 blockers · 1 warning · 1 suggestion
  • Nemotron 3 Ultra (second opinion): Completed · high confidence · 0 blockers · 0 warnings · 0 suggestions
  • Model comparison: normalized findings differ; normalized E2E selections match; Nemotron reported the same number of blockers, 1 fewer warning, 1 fewer suggestion.

Nemotron output stays in workflow artifacts and does not change the assessment above.

E2E guidance

Advisory only. E2E / PR Gate selects and runs jobs independently.

Recommended E2E: cloud-inference, cloud-onboard, full-e2e, hermes-e2e, security-posture, hermes-shields-config

1 optional E2E recommendation
  • mcp-bridge
1 warning · 1 suggestion

Warnings

Warnings do not block.

PRA-2 Warning — Cover the new locked-root shell posture

  • Location: test/hermes-start.test.ts:473
  • Category: tests
  • Problem: The start-script fixture models only legacy `755 root:root`, although `hermes_config_root_is_locked` now accepts deployed `3770 root:sandbox`. No shell-function regression proves the new posture takes the locked branch and avoids mutable-layout repair.
  • Impact: A future change can break recognition of the new locked root in `start.sh` while guard and verifier tests still pass, causing startup to alter a sealed root or fail after Shields locks it.
  • Recommendation: Add a `3770 root:sandbox` stat fixture and assert `hermes_config_root_is_locked`, `ensure_hermes_config_root_mode`, and `apply_shields_up_runtime_env` retain the locked behavior. Keep the `755 root:root` fixture for legacy compatibility.
  • Verification: Read `LOCKED_HERMES_CONFIG_STAT_MOCK` and the changed cases in `hermes_config_root_is_locked`; confirm a test invokes the functions with `root:sandbox 3770`.
  • Test coverage: A start.sh shell-fixture test that supplies `3770 root:sandbox` with sealed `444 root:root` config files, then verifies no chmod/chown repair occurs and the Shields-up runtime branch is selected.
  • Evidence: agents/hermes/start.sh changes `hermes_config_root_is_locked` to accept `root:sandbox 3770`. test/hermes-start.test.ts:473-491 defines the reused locked-root stat mock exclusively as `root:root` and `755`. test/hermes-start.test.ts:1413-1458 uses that legacy mock for Shields-up runtime behavior.

Suggestions

No response is required.

PRA-1 Suggestion — Bound the legacy locked-root compatibility path

  • Location: agents/hermes/runtime-config-guard.py:2915
  • Category: architecture
  • Problem: The guard continues to classify the pre-[Ubuntu 24.04][Hermes] shields up leaves the Hermes gateway unable to restart and quarantines the sandbox #7865 `root:root 0755` directory as locked so existing sandboxes can be repaired, but the compatibility path has no recorded supported-version boundary or removal condition.
  • Impact: The legacy posture remains accepted indefinitely in restart sealing logic, increasing the long-term state space for a security-sensitive transition.
  • Suggestion: Document the oldest supported locked sandbox or release boundary for this compatibility case and the condition that permits removing the `root:root 0755` branch.
  • Verification: Inspect `_is_locked_hermes_root` and the recovery test for the legacy `0755` posture; confirm the supported-version boundary and removal condition are recorded.
  • Test coverage: Keep or add a transition test that proves the documented supported legacy posture is classified as locked until its stated removal boundary.
  • Evidence: agents/hermes/runtime-config-guard.py:2915-2932 accepts `root:root 0755` as a locked root for older sandboxes. test/hermes-restart-config-seal-recovery.test.ts:311-341 verifies preserving the legacy posture. The source-of-truth review found no recorded removal condition or supported-version boundary in the changed compatibility code.

Workflow run details

This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge.

@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

🧹 Nitpick comments (1)
src/lib/shields/verify-lock.test.ts (1)

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

Move the issue reference to the required title suffix.

Use a behavior-oriented title ending in (#7865) instead of embedding #7865 in pre-#7865.

As per coding guidelines, test titles must put local issue references in a final (#1234) suffix.

Proposed title
-  it("reports the pre-#7865 root-owned Hermes config root as drift", async () => {
+  it("reports the legacy root-owned Hermes config root as drift (`#7865`)", async () => {
🤖 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 `@src/lib/shields/verify-lock.test.ts` at line 112, Update the test title in
the `it` block to describe the reported behavior without embedding the issue
number in the wording, and append the required `(`#7865`)` suffix at the end.

Source: Coding guidelines

🤖 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 `@docs/reference/troubleshooting.mdx`:
- Line 2699: Replace the hard-coded nemohermes host CLI tokens in the affected
troubleshooting examples with the shared $$nemoclaw placeholder, including both
referenced command lines, so generated pages use the correct agent variant.
- Around line 2694-2695: Update the troubleshooting documentation sentence
describing lockdown to explicitly state that it changes both the directory owner
and mode to 3770, including set-id and sticky-bit protection. Remove the claim
that lockdown “only changes its owner,” while preserving the explanation of
gateway runtime-file management and sandbox unlink/rename protection.

---

Nitpick comments:
In `@src/lib/shields/verify-lock.test.ts`:
- Line 112: Update the test title in the `it` block to describe the reported
behavior without embedding the issue number in the wording, and append the
required `(`#7865`)` suffix at the end.
🪄 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: 7aa9442d-e394-4a1e-acb1-db1557f6cd0e

📥 Commits

Reviewing files that changed from the base of the PR and between da1b103 and 907866e.

📒 Files selected for processing (15)
  • agents/hermes/Dockerfile
  • agents/hermes/runtime-config-guard.py
  • agents/hermes/start.sh
  • agents/hermes/validate-env-secret-boundary.py
  • ci/source-shape-test-budget.json
  • docs/reference/troubleshooting.mdx
  • src/lib/shields/index.ts
  • src/lib/shields/legacy-hermes-compat.test.ts
  • src/lib/shields/legacy-hermes-transition.test.ts
  • src/lib/shields/verify-lock.test.ts
  • src/lib/shields/verify-lock.ts
  • test/hermes-restart-config-seal-hostile-input.test.ts
  • test/hermes-restart-config-seal-recovery.test.ts
  • test/hermes-restart-config-seal-transition.test.ts
  • test/hermes-runtime-config-guard-topology.test.ts

Comment thread docs/reference/troubleshooting.mdx Outdated
Comment thread docs/reference/troubleshooting.mdx Outdated
The CLI shard and both live E2E checks still pinned the pre-#7865
root:root 0755 config root. Move them to the group-writable, sticky
locked root the guard now applies, so the mocked transaction and the
real sandbox assertions describe the same posture.

Signed-off-by: Yanyun Liao <yanyunl@nvidia.com>
@cjagwani

Copy link
Copy Markdown
Collaborator

I completed the maintainer/security pass on exact head 07da1d77bf51a797b8979ba380d258119c2f6e39. The permission design is sound: the locked root remains root:sandbox 03770, the sealed files stay root-owned/read-only, the sticky parent prevents sandbox-group replacement, legacy posture is classified conservatively, and applied-transition recovery remains descriptor/inode pinned. Focused evidence is green (CLI 40/40; integration 36 passed/5 skipped; CLI typecheck; Python and shell syntax).

Before I authorize the selected credentialed E2E plan, please address the three still-valid convention findings on this exact head:

  • In the shared troubleshooting source, replace the two new hard-coded nemohermes host commands with the variant-aware $$nemoclaw placeholder.
  • Describe the locked-root transition precisely as root:sandbox mode 3770 with set-id/sticky protection; it changes more than only the owner.
  • Move #7865 to the required final suffix in the test title (for example, ... (#7865)).

This is a plain review comment, not a Changes Requested review. I will re-check the replacement head after its one-hour quiet window and then launch the exact selected E2E plan if it remains clean.

…epair

Review findings on the maintainer/security pass:

- The two new host commands in the shared troubleshooting source hard-coded
  `nemohermes`; use the variant-aware `$$nemoclaw` placeholder so the page
  renders correctly for every agent variant.
- The locked-root description said lockdown only changes the directory's
  owner. It moves the root to `root:sandbox` mode `3770` and keeps the
  set-id and sticky bits, so state it that way.
- Move the issue reference to the required trailing suffix in the drift
  test title.

Signed-off-by: Yanyun Liao <yanyunl@nvidia.com>
@yanyunl1991

Copy link
Copy Markdown
Contributor Author

Thanks for the pass — all three findings are addressed on head 05b0ce817.

  • The two new host commands in the troubleshooting section now use the variant-aware
    $$nemoclaw placeholder instead of a hard-coded nemohermes.
  • The locked-root description now states the transition precisely: lockdown moves the root
    to root:sandbox mode 3770 and keeps the set-id and sticky bits, rather than claiming
    only the owner changes.
  • The drift test title carries the issue reference as its trailing suffix.

No behaviour changed, so the earlier evidence still stands on this head. Ready for the
credentialed E2E plan whenever the quiet window closes.

@yanyunl1991

Copy link
Copy Markdown
Contributor Author

@cjagwani — this PR is parked on E2E / PR Gate Coordination with Maintainer approval required to run E2E. No E2E job ran and no repository secret was exposed. I hold write on this repo, and the controller requires maintain/admin, so I cannot dispatch it myself. Posting the context here so you can decide whether it is worth approving; entirely your call, and I am happy to leave it parked or restructure the change if you would rather not spend a review on it.

Why the gate fired. From the risk plan, three tier-3 families match on the runtime files alone, independent of the two test/e2e/live/ files in the diff:

Family Matched files Required jobs
sandbox-boundary agents/hermes/{Dockerfile,runtime-config-guard.py,start.sh,validate-env-secret-boundary.py} full-e2e, hermes-e2e, security-posture
credentials-security agents/hermes/validate-env-secret-boundary.py, src/lib/shields/{index.ts,verify-lock.ts} cloud-inference, security-posture
platform-install agents/hermes/Dockerfile cloud-onboard

So this is not incidental to a test edit — the fix is in the shields lockdown path itself, and I do not see a version of it that avoids these files.

The security-relevant delta, for review. Hermes lockdown moved /sandbox/.hermes to root:root mode 0755. That directory is not configuration-only: it also holds auth.json, the drain request, and the temp files backing every atomic gateway_state.json and gateway.pid replace. The gateway runs as the sandbox identity, so it could no longer write there, every restart failed its health check, and the supervisor quarantined relaunch (#7865).

The change moves that directory to root:sandbox mode 3770, keeping the set-id and sticky bits. Concretely:

  • The config root becomes group-writable by the sandbox group. That is the actual widening, and it is what the gateway needs to manage its own runtime state.
  • The sticky bit still prevents the sandbox identity from unlinking or renaming the sealed root-owned configuration.
  • The sealed files themselves are untouched and stay 444 root:root.
  • agents/hermes/validate-env-secret-boundary.py adds (0, sandbox_gid, 0o3770) to the trusted-posture allowlist. Without it, an upgraded CLI refuses startup on every already-shielded sandbox.
  • shields status reports the previous 755 root:root root as drift, so existing sandboxes repair with shields up rather than a rebuild.
  • The two test/e2e/live/ files are updated because they assert the old posture and would otherwise fail once E2E does run.

Dispatch inputs, if you decide to approve:

workflow:          E2E / PR Gate Controller (.github/workflows/pr-e2e-gate.yaml, from main)
operation:         approve-e2e
pr_number:         7876
expected_head_sha: 05b0ce817a7ce7f856cc5073e88747989d27b0b9
expected_base_sha: da1b103121039fb2e056d41b901322f05489e479

Risk plan: https://github.com/NVIDIA/NemoClaw/actions/runs/30526949975 (plan hash c6dfec9bdec0eb2f05fb55957dff250d9656827965b0cbd487c89eac4592324e).

Everything else on this PR is green. Verified on an Ubuntu 24.04 x86_64 test host: the unit suites alone were not sufficient here — three rounds on real hardware were needed, because two shell-side checks in start.sh hard-coded the old posture independently of the TypeScript path.

@cjagwani

Copy link
Copy Markdown
Collaborator

Maintainer follow-up at exact head 05b0ce817a7ce7f856cc5073e88747989d27b0b9 / base da1b103121039fb2e056d41b901322f05489e479: the three prior findings are fixed, and the code/security review is clean. The remaining block is control-plane compatibility, not PR code. The existing coordination check 90817569420 was created by an older controller and requires a manual approve-e2e dispatch for this internal PR. I reviewed its deterministic plan c6dfec9bdec0eb2f05fb55957dff250d9656827965b0cbd487c89eac4592324e (cloud-inference, cloud-onboard, full-e2e, hermes-e2e, security-posture, hermes-shields-config), but current main rejects that operation with approve-e2e is only supported for fork pull requests (run 30561230640). The old coordination check remains in progress while the native E2E job is red, and it exposes no deployment approval. Please advise or perform the repository-supported retrigger/backfill for this same exact head/base under the current internal-PR controller. I am not asking for a branch/base refresh solely for currency, and I will continue monitoring the resulting gate.

@prekshivyas prekshivyas self-assigned this Jul 30, 2026
@prekshivyas
prekshivyas merged commit b1f67f7 into main Jul 30, 2026
97 of 99 checks passed
@prekshivyas
prekshivyas deleted the fix/hermes-shields-locked-config-root-7865 branch July 30, 2026 18:30
@sandl99 sandl99 mentioned this pull request Jul 31, 2026
23 tasks
sandl99 added a commit that referenced this pull request Jul 31, 2026
<!-- markdownlint-disable MD041 -->
## Summary

Adds the canonical July 30 release entry for `v0.0.99` before the
release tag is captured.
The entry covers all 37 merged PRs since `v0.0.98` and bounds
experimental or dormant work without presenting it as supported
behavior.

## Changes

- Adds `docs/changelog/2026-07-30.mdx` with the exact `## v0.0.99`
heading, parser-safe MDX SPDX comment, summary, detailed release
bullets, and published documentation routes.
- Records user-visible recovery, snapshot, shared-route, Hermes,
readiness, inference, image, documentation, and release E2E changes.
- States that the managed-image selection and startup-profile contracts
remain dormant and do not activate buildless onboarding.

Source summary:

- [#7972](#7972) ->
`docs/changelog/2026-07-30.mdx`: Records restored managed OpenClaw
configuration modes during recovery.
- [#7834](#7834) ->
`docs/changelog/2026-07-30.mdx`: Records clone-bound pairing
verification after snapshot restore.
- [#7975](#7975) ->
`docs/changelog/2026-07-30.mdx`: Records managed startup recovery
coverage.
- [#7960](#7960) ->
`docs/changelog/2026-07-30.mdx`: Records dormant startup-profile
coordination without activating a supported surface.
- [#7856](#7856) ->
`docs/changelog/2026-07-30.mdx`: Records persistence of the
credential-free OpenClaw startup command.
- [#7959](#7959) ->
`docs/changelog/2026-07-30.mdx`: Records dormant startup-profile
construction without changing onboarding.
- [#7946](#7946) ->
`docs/changelog/2026-07-30.mdx`: Records the internal startup-profile
schema and transport contract.
- [#7951](#7951) ->
`docs/changelog/2026-07-30.mdx`: Records platform-pull cleanup before
managed-image validation.
- [#7949](#7949) ->
`docs/changelog/2026-07-30.mdx`: Records rejection of retained Hermes
`uv` build cache metadata.
- [#7597](#7597) ->
`docs/changelog/2026-07-30.mdx`: Records separate command and agent
first-turn latency evidence.
- [#7931](#7931) ->
`docs/changelog/2026-07-30.mdx`: Records focused E2E replacement
evidence for retired selectors.
- [#7950](#7950) ->
`docs/changelog/2026-07-30.mdx`: Records exclusion of build-only
BuildKit telemetry from the Deep Agents Code probe.
- [#7665](#7665) ->
`docs/changelog/2026-07-30.mdx`: Records consolidated priority 2 E2E
coverage.
- [#7911](#7911) ->
`docs/changelog/2026-07-30.mdx`: Records the corrected NVIDIA DORI
installation pin.
- [#7934](#7934) ->
`docs/changelog/2026-07-30.mdx`: Records the staging image-family wait
before Brev Launchable deployment.
- [#7772](#7772) ->
`docs/changelog/2026-07-30.mdx`: Records dormant managed-image selection
contracts without activating buildless onboarding.
- [#7941](#7941) ->
`docs/changelog/2026-07-30.mdx`: Records corrected agent-specific
provider and policy guidance.
- [#7819](#7819) ->
`docs/changelog/2026-07-30.mdx`: Records removal of empty Deep Agents
Code provider-switch sections.
- [#7932](#7932) ->
`docs/changelog/2026-07-30.mdx`: Records independent
credential-generation E2E execution.
- [#7840](#7840) ->
`docs/changelog/2026-07-30.mdx`: Records shared-route preservation and
pre-delete peer validation during upgrades.
- [#7874](#7874) ->
`docs/changelog/2026-07-30.mdx`: Records the split between pre-tag
release entries and post-tag Announcements.
- [#7876](#7876) ->
`docs/changelog/2026-07-30.mdx`: Records the writable Hermes runtime
root within lockdown.
- [#7756](#7756) ->
`docs/changelog/2026-07-30.mdx`: Records validated multi-platform
managed-image publication.
- [#7914](#7914) ->
`docs/changelog/2026-07-30.mdx`: Records accepted `uv` version metadata
in Hermes image validation.
- [#7686](#7686) ->
`docs/changelog/2026-07-30.mdx`: Records the explicitly experimental
Microsoft Entra runtime identity reference.
- [#7869](#7869) ->
`docs/changelog/2026-07-30.mdx`: Records classified gateway relaunch
quarantine and rebuild guidance.
- [#7814](#7814) ->
`docs/changelog/2026-07-30.mdx`: Records state restore into replacement
sandboxes and SQLite write verification.
- [#7839](#7839) ->
`docs/changelog/2026-07-30.mdx`: Records quieter onboarding test
execution without a user-facing behavior claim.
- [#7854](#7854) ->
`docs/changelog/2026-07-30.mdx`: Records generalized agent-selection
guidance.
- [#7845](#7845) ->
`docs/changelog/2026-07-30.mdx`: Records isolated CDI test evidence
without a user-facing behavior claim.
- [#7843](#7843) ->
`docs/changelog/2026-07-30.mdx`: Records the corrected Omni sub-agent
model ID.
- [#7908](#7908) ->
`docs/changelog/2026-07-30.mdx`: Records reviewed Hermes and Deep Agents
Code dependency pins.
- [#7887](#7887) ->
`docs/changelog/2026-07-30.mdx`: Records rejection of a symlinked DGX
Station release marker.
- [#7747](#7747) ->
`docs/changelog/2026-07-30.mdx`: Records the internal compute-driver
separation without a user-facing behavior claim.
- [#7660](#7660) ->
`docs/changelog/2026-07-30.mdx`: Records atomic publication of rebuild
recovery manifests.
- [#7661](#7661) ->
`docs/changelog/2026-07-30.mdx`: Records bounded local inference
health-response retention.
- [#7654](#7654) ->
`docs/changelog/2026-07-30.mdx`: Records state preservation across
supervisor relaunch recovery.

## Type of Change

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

## Quality Gates

- [ ] Tests added or updated for changed behavior
- [x] Existing tests cover changed behavior — justification:
`test/changelog-docs.test.ts` validates the dated changelog contract,
SPDX comment, version heading, and published routes.
- [ ] Tests not applicable — justification:
- [x] 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:
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Documentation Writer Review

- [x] Documentation writer subagent reviewed the completed changes
- Result: `docs-updated`
- Evidence: `docs/changelog/2026-07-30.mdx`; the documentation-only diff
passed review against `WRITING.md`, the controlled word list, and
`docs/CONTRIBUTING.md`. The review covered terminology, structure,
active voice, release meaning, product-scope boundaries, and link and
code presentation. Changelog tests passed 6/6, and the docs build
reported 0 errors with 2 pre-existing warnings.
- Agent: Codex CLI
<!-- docs-review-head-sha: 200940f -->
<!-- docs-review-agents-blob-sha: c052d60 -->

## DGX Station Hardware Evidence

- [ ] Tested on DGX Station
- Tested commit:
- Station profile/scenario:
- Result:
- Supporting evidence:

## Verification

- [x] PR description includes a `Signed-off-by:` line and every commit
appears as `Verified` in GitHub
- [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or
`npm run validate:pr` passed after refreshing `origin/main` when hooks
were skipped or unavailable
- [x] Targeted behavior tests pass for the current change set, or tests
are marked not applicable above — command/result or justification: `npx
vitest run test/changelog-docs.test.ts` passed 6/6 tests.
- [ ] Applicable broad gate passed — `npm test` for broad
runtime/test-harness changes; `npm run check` for repo-wide
validation/coverage changes — command/result: Not applicable to this
documentation-only release entry.
- [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) —
result: Build passed with 0 errors and 2 pre-existing warnings.
- [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)

---
Signed-off-by: San Dang <sdang@nvidia.com>


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

## Summary by CodeRabbit

* **Documentation**
* Added release notes for v0.0.99 covering snapshot restoration, sandbox
recovery, gateway route upgrades, and Hermes security updates.
* Documented experimental Microsoft Entra runtime identity support and
enhanced readiness checks.
* Added details on managed image validation, trusted CI image promotion,
and end-to-end release evidence.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR fixes a bug or regression platform: ubuntu Affects Ubuntu Linux environments

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Ubuntu 24.04][Hermes] shields up leaves the Hermes gateway unable to restart and quarantines the sandbox

3 participants