Skip to content

fix(hermes): harden history repair contract#4722

Merged
cv merged 1 commit into
mainfrom
fix/hermes-history-followup-hardening
Jun 3, 2026
Merged

fix(hermes): harden history repair contract#4722
cv merged 1 commit into
mainfrom
fix/hermes-history-followup-hardening

Conversation

@cv

@cv cv commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Hardens the Hermes .hermes_history repair added in #4708 so startup no longer performs privileged check-then-use mutations on a sandbox-writable pathname. It also records the shields-up history-file exception in the central Hermes/shields contracts and preserves history through Hermes state backups.

Related Issue

Follow-up to #4708; refs #2432.

Changes

  • Replace path-based .hermes_history repair in agents/hermes/start.sh with an O_NOFOLLOW fd-based Python helper that validates, fchowns, and fchmods the opened inode.
  • Document the intentional sandbox:sandbox 0660 .hermes_history exception in src/lib/shields/state-dir-lock.ts, agents/hermes/manifest.yaml, and agents/hermes/policy-additions.yaml.
  • Add .hermes_history to Hermes state_files and update snapshot coverage so history is backed up and restored.
  • Clarify that the scenario E2E history append probe is the accepted proxy for the /exit symptom from [station][Agent&Skills] Hermes TUI spams "Permission denied: /sandbox/.hermes/.hermes_history" on every interaction, blocking /exit #2432.

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)

Verification

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • 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)

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

Summary by CodeRabbit

Release Notes

  • New Features

    • Hermes history file is now included in backup and restore operations, ensuring command history is preserved across sessions.
  • Bug Fixes

    • Enhanced reliability of history file handling with improved validation and robustness during repair operations.
  • Tests

    • Expanded test coverage for history file backup and restore scenarios.

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
@cv cv added security Potential vulnerability, unsafe behavior, or access risk area: security Security controls, permissions, secrets, or hardening labels Jun 3, 2026
@cv cv self-assigned this Jun 3, 2026
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5a53ee6b-cfef-4571-9ea9-664aea77973f

📥 Commits

Reviewing files that changed from the base of the PR and between 5fcfcd9 and 09378ce.

📒 Files selected for processing (6)
  • agents/hermes/manifest.yaml
  • agents/hermes/policy-additions.yaml
  • agents/hermes/start.sh
  • src/lib/shields/state-dir-lock.ts
  • test/e2e-scenario/validation_suites/hermes/01-history-writable.sh
  • test/snapshot.test.ts

📝 Walkthrough

Walkthrough

This PR introduces .hermes_history as a persistent agent state file, adds hardened O_NOFOLLOW-based layout repair logic, documents its shields-up exemption, and validates full backup/restore behavior with comprehensive snapshot tests.

Changes

Hermes History Persistence and Repair

Layer / File(s) Summary
State file declaration and filesystem policy
agents/hermes/manifest.yaml, agents/hermes/policy-additions.yaml
Hermes manifest declares .hermes_history as durable state alongside SOUL.md, with documentation of prompt-toolkit TUI append semantics. Filesystem policy expands the /sandbox/.hermes allowlist comment to explain the intentional writable exception for .hermes_history during shields-up.
O_NOFOLLOW layout repair for .hermes_history
agents/hermes/start.sh
Complete rewrite of ensure_hermes_history_file from shell symlink/stat checks to Python os.open(O_NOFOLLOW) descriptor-based repair. Validates regular file status, rejects hard-linked targets via st_nlink != 1, applies fchown/fchmod on open descriptor, and re-verifies inode identity after modification to prevent path-swap attacks.
Shields-up state exclusion documentation
src/lib/shields/state-dir-lock.ts
Documentation clarified that .hermes_history is intentionally excluded from shields-up lock during runtime, with notes on precreation/repair ownership and conditional removal if upstream options redirect or disable FileHistory.
Test scenario and snapshot backup/restore
test/e2e-scenario/validation_suites/hermes/01-history-writable.sh, test/snapshot.test.ts
Test scenario documentation clarifies append probe must succeed under both shields states. Snapshot test creates .hermes_history in setup, fake ssh harness handles backup inspection (cat --) and restore (.nemoclaw-restore + .hermes_history), and assertions verify .hermes_history in backup.manifest.stateFiles and correctly restored with original contents after mutation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • NVIDIA/NemoClaw#4708: Overlaps on ensure_hermes_history_file hardening in agents/hermes/start.sh and the startup repair behavior for .hermes_history during shields-up lock scenarios.

Suggested labels

integration: hermes, v0.0.58, bug-fix

Poem

🐰 A history file now persists and survives,
With O_NOFOLLOW guards keeping inode safe,
Through shield-ups and backups, the data thrives,
No symlinks shall fool this rabbit's escape! 🔐

🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(hermes): harden history repair contract' accurately summarizes the main change: replacing a path-based repair with an O_NOFOLLOW fd-based approach to prevent check-then-use race conditions.
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.

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

✨ Finishing Touches
📝 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-history-followup-hardening

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

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor

Findings: 0 needs attention, 2 worth checking, 0 nice ideas
Since last review: 0 prior items resolved, 0 still apply, 2 new items found

Review findings

🛠️ Needs attention

  • None.

🔎 Worth checking

  • Snapshotting .hermes_history can preserve sensitive prompt contents (agents/hermes/manifest.yaml:98): Adding `.hermes_history` to Hermes durable `state_files` means rebuild snapshots now copy prompt_toolkit history. That history can contain pasted API keys, credentials, private prompts, or other sensitive user input. The existing backup sanitizer strips known credential filenames, `.env` key patterns, and JSON config content, but it does not scrub arbitrary history text.
    • Recommendation: Decide whether preserving prompt history is an intentional privacy tradeoff. If yes, document it in user-facing snapshot/rebuild notes; if not, add an opt-out or sanitizer for `.hermes_history` before writing the backup.
    • Evidence: The manifest adds `- path: .hermes_history`, and `test/snapshot.test.ts` now expects `.hermes_history` to be backed up and restored verbatim as `original history\n`.
  • Privileged fd-based history repair branch lacks direct coverage (agents/hermes/start.sh:439): The new repair helper is a sandbox-boundary root path: when the entrypoint runs as root it resolves the sandbox uid/gid and applies `os.fchown(fd, uid, gid)` plus `os.fchmod(fd, mode)` to the opened inode. The nearby unit harness stubs `id -u` to a non-root value and validates symlink, directory, and hardlink refusals, but it does not directly prove the root-only `fchown` branch preserves locked config files or that the post-open inode recheck catches a path swap.
    • Recommendation: Add a focused privileged-path test or identify runtime validation that exercises repair as root against a locked config root, including a malicious hardlink/path-swap-style case where feasible.
    • Evidence: `ensure_hermes_history_file` conditionally calls `os.fchown` only when `os.geteuid() == 0`; existing tests cover unsafe path shapes but the extracted shell harness stubs `id -u` to `1000` and does not force the Python helper through the root branch.

🌱 Nice ideas

  • None.
Since last review details

Current findings:

  • Snapshotting .hermes_history can preserve sensitive prompt contents (agents/hermes/manifest.yaml:98): Adding `.hermes_history` to Hermes durable `state_files` means rebuild snapshots now copy prompt_toolkit history. That history can contain pasted API keys, credentials, private prompts, or other sensitive user input. The existing backup sanitizer strips known credential filenames, `.env` key patterns, and JSON config content, but it does not scrub arbitrary history text.
    • Recommendation: Decide whether preserving prompt history is an intentional privacy tradeoff. If yes, document it in user-facing snapshot/rebuild notes; if not, add an opt-out or sanitizer for `.hermes_history` before writing the backup.
    • Evidence: The manifest adds `- path: .hermes_history`, and `test/snapshot.test.ts` now expects `.hermes_history` to be backed up and restored verbatim as `original history\n`.
  • Privileged fd-based history repair branch lacks direct coverage (agents/hermes/start.sh:439): The new repair helper is a sandbox-boundary root path: when the entrypoint runs as root it resolves the sandbox uid/gid and applies `os.fchown(fd, uid, gid)` plus `os.fchmod(fd, mode)` to the opened inode. The nearby unit harness stubs `id -u` to a non-root value and validates symlink, directory, and hardlink refusals, but it does not directly prove the root-only `fchown` branch preserves locked config files or that the post-open inode recheck catches a path swap.
    • Recommendation: Add a focused privileged-path test or identify runtime validation that exercises repair as root against a locked config root, including a malicious hardlink/path-swap-style case where feasible.
    • Evidence: `ensure_hermes_history_file` conditionally calls `os.fchown` only when `os.geteuid() == 0`; existing tests cover unsafe path shapes but the extracted shell harness stubs `id -u` to `1000` and does not force the Python helper through the root branch.

Workflow run details

This is an automated advisory review. A human maintainer must make the final merge decision.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

E2E Advisor Recommendation

Required E2E: ubuntu-repo-cloud-hermes, hermes-root-entrypoint-smoke-e2e, rebuild-hermes-e2e
Optional E2E: hermes-onboard-security-posture-e2e, shields-config-e2e

Dispatch hint: ubuntu-repo-cloud-hermes

Auto-dispatched E2E: hermes-root-entrypoint-smoke-e2e, rebuild-hermes-e2e via nightly-e2e.yaml at 09378ce6f17fe7fcb8fc62c533967cd42d0a1de2nightly run

Workflow run

Full advisor summary

E2E Recommendation Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E

  • ubuntu-repo-cloud-hermes (high): Runs the typed Hermes scenario with smoke, live cloud inference, and the hermes-specific suite, including runtime.hermes.history-writable. This is the most direct existing E2E for the changed .hermes_history contract under shields down/up and for ensuring the Hermes user flow still starts and serves inference.
  • hermes-root-entrypoint-smoke-e2e (medium): Exercises the real Hermes image/start entrypoint and layout repair path. The start.sh Python no-follow fd repair is startup-critical and security-sensitive; this job catches syntax/runtime failures and regressions in Hermes layout repair, gateway health, privilege separation, and config protection.
  • rebuild-hermes-e2e (high): Hermes manifest state_files changed to include .hermes_history, which participates in backup/restore during rebuild. This is the closest existing E2E for Hermes rebuild state preservation and credential-exclusion behavior after state contract changes.

Optional E2E

  • hermes-onboard-security-posture-e2e (high): Useful additional confidence for Hermes onboard security posture because this PR touches root/sandbox ownership, startup repair, and security-boundary comments, but it does not directly assert the .hermes_history happy path.
  • shields-config-e2e (medium): Adjacent coverage for the shared shields up/down lifecycle and immutable config behavior. It is OpenClaw-oriented, so it is optional rather than merge-blocking for a Hermes-specific history carve-out.

New E2E recommendations

  • hermes-history-security-negative-paths (high): Existing Hermes history coverage checks the writable happy path, but the start.sh change is specifically a symlink/hard-link/TOCTOU defense. Add an E2E that seeds unsafe .hermes_history shapes under /sandbox/.hermes, runs startup/repair under locked-root conditions, and asserts config.yaml/.env ownership and mode are not downgraded.
    • Suggested test: Add a Hermes locked-root history repair negative E2E for symlink, non-regular file, and hard-link-to-config/.env cases.
  • hermes-snapshot-history-durability (high): The manifest now marks .hermes_history as a durable state_file, but the existing live snapshot/rebuild tests do not appear to assert a unique history marker is captured and restored for Hermes.
    • Suggested test: Extend or add a Hermes snapshot/rebuild E2E that writes a unique .hermes_history marker, snapshots or rebuilds, mutates/deletes it, restores, and verifies the marker returns while credentials remain excluded.
  • hermes-shields-specific-layout (medium): The shared shields E2E is OpenClaw-centric. Hermes now has a special top-level file that must remain sandbox-writable while the parent config root and config secrets are locked.
    • Suggested test: Add a Hermes shields layout E2E that asserts /sandbox/.hermes root/config/.env lock posture and .hermes_history sandbox:sandbox 0660 writability before and after shields up/down.

Dispatch hint

  • Workflow: .github/workflows/e2e-scenarios.yaml
  • jobs input: ubuntu-repo-cloud-hermes

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

E2E Scenario Advisor Recommendation

Required scenario E2E: ubuntu-repo-cloud-hermes
Optional scenario E2E: ubuntu-repo-cloud-hermes-discord, ubuntu-repo-cloud-hermes-slack

Dispatch required scenario E2E:

  • gh workflow run e2e-scenarios.yaml --ref <pr-head-ref> --field scenarios=ubuntu-repo-cloud-hermes

Workflow run

Full scenario advisor summary

E2E Scenario Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required scenario E2E

  • ubuntu-repo-cloud-hermes: The PR changes Hermes manifest/state-file contract, sandbox policy, Hermes startup layout repair for .hermes_history, shields state-dir locking comments/behavior, and the hermes-specific history-writable scenario script. This scenario is the smallest routed scenario that runs the Hermes cloud onboarding path and includes the hermes-specific suite containing hermes/01-history-writable.sh.
    • Dispatch: gh workflow run e2e-scenarios.yaml --ref <pr-head-ref> --field scenarios=ubuntu-repo-cloud-hermes

Optional scenario E2E

  • ubuntu-repo-cloud-hermes-discord: Optional adjacent Hermes onboarding variant. It does not run the hermes-specific history-writable step, but can provide extra coverage that Hermes startup/policy/manifest changes do not regress the Discord messaging profile.
    • Dispatch: gh workflow run e2e-scenarios.yaml --ref <pr-head-ref> --field scenarios=ubuntu-repo-cloud-hermes-discord
  • ubuntu-repo-cloud-hermes-slack: Optional adjacent Hermes onboarding variant. It does not run the hermes-specific history-writable step, but can provide extra coverage that Hermes startup/policy/manifest changes do not regress the Slack messaging profile.
    • Dispatch: gh workflow run e2e-scenarios.yaml --ref <pr-head-ref> --field scenarios=ubuntu-repo-cloud-hermes-slack

Relevant changed files

  • agents/hermes/manifest.yaml
  • agents/hermes/policy-additions.yaml
  • agents/hermes/start.sh
  • src/lib/shields/state-dir-lock.ts
  • test/e2e-scenario/validation_suites/hermes/01-history-writable.sh

@cjagwani cjagwani 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.

Lgtm

@cv cv merged commit dbc6547 into main Jun 3, 2026
33 checks passed
@cv cv deleted the fix/hermes-history-followup-hardening branch June 3, 2026 20:06
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 26909722830
Target ref: 09378ce6f17fe7fcb8fc62c533967cd42d0a1de2
Workflow ref: main
Requested jobs: hermes-root-entrypoint-smoke-e2e,rebuild-hermes-e2e
Summary: 2 passed, 0 failed, 0 skipped

Job Result
hermes-root-entrypoint-smoke-e2e ✅ success
rebuild-hermes-e2e ✅ success

@wscurran wscurran added the bug-fix PR fixes a bug or regression label Jun 8, 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 security Potential vulnerability, unsafe behavior, or access risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants