Skip to content

fix(hermes): precreate .hermes_history so shields-up doesn't lock TUI#4708

Merged
cv merged 5 commits into
mainfrom
fix/2432-hermes-history-shields-up
Jun 3, 2026
Merged

fix(hermes): precreate .hermes_history so shields-up doesn't lock TUI#4708
cv merged 5 commits into
mainfrom
fix/2432-hermes-history-shields-up

Conversation

@laitingsheng

@laitingsheng laitingsheng commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Hermes TUI uses prompt_toolkit.FileHistory rooted at $HERMES_HOME/.hermes_history. Upstream hardcodes the path with no env override. Under shields up, /sandbox/.hermes is re-owned to root:root 0755 and the sandbox user can no longer create the missing history file, so every keypress emits "Permission denied" and blocks /exit. Precreating the file as a sandbox-owned regular file means the lockdown only restricts the parent dir, while file-level perms still let open("ab") succeed.

Related Issue

Fixes #2432

Changes

  • Precreate /sandbox/.hermes/.hermes_history as sandbox:sandbox 0660 in Dockerfile.base + the Hermes derived Dockerfile.
  • Add ensure_hermes_history_file() in agents/hermes/start.sh and wire it into repair_hermes_startup_layout so runtime repair restores the file when it's missing or has drifted ownership. The helper refuses symlinks, non-regular paths, and hard-linked targets (a hard link to config.yaml / .env would let the subsequent chmod 660 walk the shared inode and silently undo the shields-up root:root 0444 lock after verify_config_integrity has already passed).
  • Repair runs in both shields-down and shields-up paths so legacy sandboxes built before the precreate landed self-heal on the next gateway start. Unsafe state under a locked root (symlink, non-regular, hard-link, create failure) is a hard stop — start.sh exits non-zero rather than continuing with a broken or attacker-controlled history file.
  • Extend test/hermes-start.test.ts with cases covering: unlocked + create, pre-existing regular file mode re-asserted, symlink refused with no write-through, directory refused, locked-root + create, locked-root + symlink, locked-root + directory, locked-root + hard-link-to-config.yaml (asserts config.yaml perms + content unchanged).
  • Add hermes/01-history-writable.sh E2E probe in the hermes-specific suite. Reproduces the exact prompt_toolkit open("ab") call against the running sandbox in both shields-down and shields-up states (forces shields-up when the scenario starts shields-down so the regression cannot slip through).

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: Tinson Lai tinsonl@nvidia.com

Summary by CodeRabbit

  • Bug Fixes

    • Ensure Hermes history file exists at container start and during startup repair with sandbox ownership and mode 660; repair will not follow or overwrite symlinks or directories and, when config root is locked, limits repair to history-only.
  • Tests

    • Added E2E checks: history-file creation and preservation, refusal for unsafe layouts or hardlinks, and writable verification across shield/config states.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@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: 91b52427-0a37-486b-be31-e649c8decb1b

📥 Commits

Reviewing files that changed from the base of the PR and between 794a2ca and 64b0ff0.

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

📝 Walkthrough

Walkthrough

This PR ensures /sandbox/.hermes/.hermes_history exists with sandbox:sandbox ownership and 660 permissions at image build and at startup; adds ensure_hermes_history_file and invokes it during repair; adds an E2E history-writable probe and wires it into suites; and extends unit tests to cover pre-existing history states.

Changes

Hermes History File Safety and Writability

Layer / File(s) Summary
Docker build initialization of history file
agents/hermes/Dockerfile, agents/hermes/Dockerfile.base
Both Dockerfile variants now create/truncate /sandbox/.hermes/.hermes_history, chown sandbox:sandbox, and chmod 660 during the sandbox setup RUN block.
Runtime history file safety and repair
agents/hermes/start.sh
Adds ensure_hermes_history_file that rejects symlinks/non-regular existing paths, prevents hard-linked targets, creates the file if missing, sets sandbox:sandbox ownership when running as root, and applies mode 660. Integrated into repair_hermes_startup_layout in locked and normal flows.
E2E validation script for history writability
test/e2e-scenario/validation_suites/hermes/01-history-writable.sh
New script verifies the history file is a sandbox-owned regular file with mode 660, appends a marker via sandbox-executed Python to confirm writability, and probes before/after shields toggles as needed.
E2E test suite integration
test/e2e-scenario/scenarios/assertions/registry.ts, test/e2e-scenario/scenarios/migration-inventory.ts, test/e2e-scenario/validation_suites/suites.yaml
Wires the new history-writable assertion into the Hermes-specific validation suite and the migration inventory so it runs as part of E2E validation.
Unit test harness for history file scenarios
test/hermes-start.test.ts
Extends runHermesGatewayRuntimeCleanup to set up pre-existing .hermes_history states (regular, symlink, directory, hardlink-to-config), include ensure_hermes_history_file in generated run.sh, inspect post-cleanup historyKind/historyMode/historyContent, and assert repaired/preserved/refused behaviors (including locked-root cases).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

fix, Sandbox, Docker, v0.0.57

Suggested reviewers

  • ericksoa
  • cv

"I dug through shells and Docker layers deep,
Found a history file that could not sleep.
I guarded its owner, set mode with care,
Now Hermes writes safely — nibble and share. 🐇"

🚥 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): precreate .hermes_history so shields-up doesn't lock TUI' accurately describes the main change: precreating the history file to prevent permission errors that block the Hermes TUI.
Linked Issues check ✅ Passed The PR fully addresses all objectives from issue #2432: precreates .hermes_history in Dockerfiles with correct ownership/mode, implements ensure_hermes_history_file() helper for runtime repair, adds unit and E2E tests covering both shields-down and shields-up states, and prevents unsafe targets like symlinks.
Out of Scope Changes check ✅ Passed All changes directly support the core objective of fixing the Hermes TUI permission error: Docker image setup, runtime repair logic, test infrastructure, and validation probes are all focused on ensuring .hermes_history is accessible to the sandbox user.

✏️ 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/2432-hermes-history-shields-up

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

E2E Advisor Recommendation

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

Auto-dispatched E2E: hermes-root-entrypoint-smoke-e2e, hermes-e2e via nightly-e2e.yaml at 73faca1a904e2dc66c88719de0c06c5914f33a97nightly run

Workflow run

Full advisor summary

E2E Recommendation Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E

  • hermes-root-entrypoint-smoke-e2e (medium): Directly exercises the real Hermes image and root entrypoint startup path impacted by the Dockerfile and start.sh changes, including layout repair and legacy PID/state migration.
  • hermes-e2e (high): Runs the primary Hermes install → onboard → health → live inference user flow against the changed image/startup behavior.
  • scenario:ubuntu-repo-cloud-hermes (high): Runs the typed Hermes cloud scenario whose hermes-specific suite now includes runtime.hermes.history-writable, validating the new scenario assertion wiring and the shields down/up history write regression probe.

Optional E2E

  • hermes-onboard-security-posture-e2e (high): Useful extra confidence for the locked/shields-up security posture touched by the new history repair logic, especially around preserving sealed runtime/config files.
  • rebuild-hermes-e2e (high): Optional coverage for existing Hermes sandboxes moving through rebuild/upgrade paths where the new startup repair may need to create .hermes_history for legacy layouts.

New E2E recommendations

  • hermes-startup-security-hardlink-regression (high): Unit tests cover the .hermes_history hard-link refusal, but there is no real-container E2E that constructs an adversarial locked-root history path and proves Hermes startup refuses without weakening config.yaml/.env permissions.
    • Suggested test: Add a Hermes root-entrypoint smoke variant that starts from a crafted locked-root layout where /sandbox/.hermes/.hermes_history is a symlink or hard link to a sealed config file and asserts startup fails safely without modifying config ownership/mode.
  • hermes-image-history-file-contract (medium): The existing hermes-root-entrypoint-smoke-e2e verifies Hermes layout directories and PID migration, but it does not appear to assert .hermes_history owner/mode/writability in the clean and legacy image variants.
    • Suggested test: Extend test/e2e/test-hermes-root-entrypoint-smoke.sh to assert /sandbox/.hermes/.hermes_history is a sandbox:sandbox regular file with mode 660 and appendable by the sandbox user in both clean and legacy startup variants.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

E2E Scenario Advisor Recommendation

Required scenario E2E: e2e-scenarios-all
Optional scenario E2E: None

Dispatch required scenario E2E:

  • gh workflow run e2e-scenarios-all.yaml --ref <pr-head-ref>

Workflow run

Full scenario advisor summary

E2E Scenario Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required scenario E2E

  • e2e-scenarios-all: Scenario suite catalog metadata changed in test/e2e-scenario/validation_suites/suites.yaml, and the assertion registry changed to add the new hermes-specific history-writable step. Per scenario E2E policy, suite catalog/assertion metadata changes require the all-scenarios fan-out. Hermes image and startup changes are also directly exercised by the Hermes scenario coverage included in the fan-out.
    • Dispatch: gh workflow run e2e-scenarios-all.yaml --ref <pr-head-ref>

Optional scenario E2E

  • None.

Relevant changed files

  • agents/hermes/Dockerfile
  • agents/hermes/Dockerfile.base
  • agents/hermes/start.sh
  • test/e2e-scenario/scenarios/assertions/registry.ts
  • test/e2e-scenario/validation_suites/hermes/01-history-writable.sh
  • test/e2e-scenario/validation_suites/suites.yaml

@laitingsheng laitingsheng added integration: hermes Hermes integration behavior area: onboarding Onboarding FSM, provider setup, sandbox launch, or first-run flow bug-fix PR fixes a bug or regression labels Jun 3, 2026
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 26875066276
Target ref: 1fba578ebdc37784695c1efa3df071deff8017c2
Workflow ref: main
Requested jobs: hermes-root-entrypoint-smoke-e2e
Summary: 1 passed, 0 failed, 0 skipped

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

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor

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

Review findings

🛠️ Needs attention

  • History repair still has a root check-then-use race on a sandbox-writable path (agents/hermes/start.sh:397): The helper now rejects existing symlinks, non-regular files, and hard-linked files, but it validates /sandbox/.hermes/.hermes_history and then later creates, chowns, and chmods the same pathname. In the mutable layout, /sandbox/.hermes is sandbox-writable, and the history file is sandbox-owned, so a sandbox-controlled process can replace the path after validation but before the privileged root operations. That can make root chmod/chown operate on an attacker-selected replacement after config integrity has already passed.
    • Recommendation: Avoid privileged check-then-use on a sandbox-writable pathname. Prefer creating/preparing the file before the directory becomes sandbox-writable, or use a no-follow/open-by-fd primitive and validate the actual opened inode immediately before fchown/fchmod. Add an adversarial replacement or race-oriented negative test if feasible.
    • Evidence: agents/hermes/start.sh:368-403 checks -L/-f/-e and hard-link count, then uses : >"$file", chown, and chmod by pathname; agents/hermes/start.sh:427 calls it in the mutable repair path after ensure_hermes_config_root_mode makes HERMES_DIR sandbox:sandbox 3770.
  • Linked issue acceptance for Hermes TUI /exit is still only proxy-tested (test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:74): The new e2e probe reproduces prompt_toolkit's open(filename, "ab") failure path under shields-down and shields-up, which is useful, but the linked issue's user-facing acceptance explicitly requires the Hermes TUI to accept input without errors and /exit to cleanly exit. The PR does not launch the Hermes TUI, send input, or exercise /exit.
    • Recommendation: Add a lightweight Hermes TUI smoke that sends /exit and verifies clean exit, or explicitly document that the open("ab") probe is the accepted proxy for this issue. If practical, include a legacy locked-root sandbox missing .hermes_history followed by sandbox-user append validation.
    • Evidence: Issue [station][Agent&Skills] Hermes TUI spams "Permission denied: /sandbox/.hermes/.hermes_history" on every interaction, blocking /exit #2432 Expected Behavior says "TUI accepts input and displays responses without errors" and "`/exit` cleanly exits the Hermes chat"; test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:74-78 runs python3 -c "open('/sandbox/.hermes/.hermes_history', 'ab')..." instead of interacting with Hermes.

🔎 Worth checking

  • Source-of-truth review needed: agents/hermes/start.sh ensure_hermes_history_file and locked-root history repair: The advisor marked localized patch analysis as needs_followup.
    • Recommendation: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
    • Evidence: agents/hermes/start.sh:368-427; agents/hermes/Dockerfile:303-305; agents/hermes/Dockerfile.base:134-136; test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:7-16; src/lib/shields/state-dir-lock.ts documents other carve-outs but not .hermes_history.
  • Writable .hermes_history exception is not recorded in the central shields policy contract (agents/hermes/start.sh:411): This PR intentionally leaves /sandbox/.hermes/.hermes_history writable by the sandbox user under shields-up inside an otherwise lockable Hermes config root. The reason is documented locally in start.sh and the e2e probe, but the central shields/state-dir source of truth and Hermes manifest/policy contract still do not describe this exception, how status/config-integrity should treat it, or when it can be removed.
    • Recommendation: Add a central policy or manifest note documenting that .hermes_history is intentionally sandbox:sandbox 0660 under shields-up, why prompt_toolkit requires it, whether it is durable/user state, and the removal condition if upstream supports redirecting or disabling file history.
    • Evidence: agents/hermes/start.sh:411-417 and test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:7-16 document the local reason; src/lib/shields/state-dir-lock.ts documents other runtime carve-outs but not .hermes_history; agents/hermes/policy-additions.yaml still describes /sandbox/.hermes broadly as lockable config; agents/hermes/manifest.yaml state_files omits the history file.

🌱 Nice ideas

  • None.
Since last review details

Current findings:

  • Source-of-truth review needed: agents/hermes/start.sh ensure_hermes_history_file and locked-root history repair: The advisor marked localized patch analysis as needs_followup.
    • Recommendation: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
    • Evidence: agents/hermes/start.sh:368-427; agents/hermes/Dockerfile:303-305; agents/hermes/Dockerfile.base:134-136; test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:7-16; src/lib/shields/state-dir-lock.ts documents other carve-outs but not .hermes_history.
  • History repair still has a root check-then-use race on a sandbox-writable path (agents/hermes/start.sh:397): The helper now rejects existing symlinks, non-regular files, and hard-linked files, but it validates /sandbox/.hermes/.hermes_history and then later creates, chowns, and chmods the same pathname. In the mutable layout, /sandbox/.hermes is sandbox-writable, and the history file is sandbox-owned, so a sandbox-controlled process can replace the path after validation but before the privileged root operations. That can make root chmod/chown operate on an attacker-selected replacement after config integrity has already passed.
    • Recommendation: Avoid privileged check-then-use on a sandbox-writable pathname. Prefer creating/preparing the file before the directory becomes sandbox-writable, or use a no-follow/open-by-fd primitive and validate the actual opened inode immediately before fchown/fchmod. Add an adversarial replacement or race-oriented negative test if feasible.
    • Evidence: agents/hermes/start.sh:368-403 checks -L/-f/-e and hard-link count, then uses : >"$file", chown, and chmod by pathname; agents/hermes/start.sh:427 calls it in the mutable repair path after ensure_hermes_config_root_mode makes HERMES_DIR sandbox:sandbox 3770.
  • Writable .hermes_history exception is not recorded in the central shields policy contract (agents/hermes/start.sh:411): This PR intentionally leaves /sandbox/.hermes/.hermes_history writable by the sandbox user under shields-up inside an otherwise lockable Hermes config root. The reason is documented locally in start.sh and the e2e probe, but the central shields/state-dir source of truth and Hermes manifest/policy contract still do not describe this exception, how status/config-integrity should treat it, or when it can be removed.
    • Recommendation: Add a central policy or manifest note documenting that .hermes_history is intentionally sandbox:sandbox 0660 under shields-up, why prompt_toolkit requires it, whether it is durable/user state, and the removal condition if upstream supports redirecting or disabling file history.
    • Evidence: agents/hermes/start.sh:411-417 and test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:7-16 document the local reason; src/lib/shields/state-dir-lock.ts documents other runtime carve-outs but not .hermes_history; agents/hermes/policy-additions.yaml still describes /sandbox/.hermes broadly as lockable config; agents/hermes/manifest.yaml state_files omits the history file.
  • Linked issue acceptance for Hermes TUI /exit is still only proxy-tested (test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:74): The new e2e probe reproduces prompt_toolkit's open(filename, "ab") failure path under shields-down and shields-up, which is useful, but the linked issue's user-facing acceptance explicitly requires the Hermes TUI to accept input without errors and /exit to cleanly exit. The PR does not launch the Hermes TUI, send input, or exercise /exit.
    • Recommendation: Add a lightweight Hermes TUI smoke that sends /exit and verifies clean exit, or explicitly document that the open("ab") probe is the accepted proxy for this issue. If practical, include a legacy locked-root sandbox missing .hermes_history followed by sandbox-user append validation.
    • Evidence: Issue [station][Agent&Skills] Hermes TUI spams "Permission denied: /sandbox/.hermes/.hermes_history" on every interaction, blocking /exit #2432 Expected Behavior says "TUI accepts input and displays responses without errors" and "`/exit` cleanly exits the Hermes chat"; test/e2e-scenario/validation_suites/hermes/01-history-writable.sh:74-78 runs python3 -c "open('/sandbox/.hermes/.hermes_history', 'ab')..." instead of interacting with Hermes.

Workflow run details

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

@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: 1

🧹 Nitpick comments (2)
test/hermes-start.test.ts (1)

462-475: ⚡ Quick win

Assert the symlink target was not written through.

The symlink refusal test only checks historyKind === "symlink" and the stderr message. But a regression where ensure_hermes_history_file followed the symlink (e.g. opening/truncating the target) would still leave the path classified as a symlink, so this case would pass while the actual security property—no write-through to the attacker-controlled target—is broken. Capturing and asserting the target content closes that detection gap.

♻️ Capture and assert the symlink target content

In runHermesGatewayRuntimeCleanup, hoist the target path and return its content:

   const historyPath = path.join(hermesHome, ".hermes_history");
+  const historyTarget = path.join(tmpDir, "history-target");
   if (opts.preExistingHistory === "regular") {
     fs.writeFileSync(historyPath, "pre-existing\n", { mode: 0o600 });
   } else if (opts.preExistingHistory === "symlink") {
-    const target = path.join(tmpDir, "history-target");
-    fs.writeFileSync(target, "attacker\n");
-    fs.symlinkSync(target, historyPath);
+    fs.writeFileSync(historyTarget, "attacker\n");
+    fs.symlinkSync(historyTarget, historyPath);
   } else if (opts.preExistingHistory === "directory") {
     fs.mkdirSync(historyPath);
   }
       historyMode,
       historyKind,
       historyContent,
+      historySymlinkTargetContent: fs.existsSync(historyTarget)
+        ? fs.readFileSync(historyTarget, "utf-8")
+        : "",

Then assert in the symlink test:

     expect(run.historyKind).toBe("symlink");
+    expect(run.historySymlinkTargetContent).toBe("attacker\n");
     expect(run.result.stderr).toContain(
       "Refusing Hermes layout repair because",
     );
🤖 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/hermes-start.test.ts` around lines 462 - 475, The symlink test must also
assert the symlink target was not written-through: modify the helper
runHermesGatewayRuntimeCleanup to record/return the symlink target path and its
file content (or absence) when preExistingHistory === "symlink", then in the
test that calls runHermesGatewayRuntimeCleanup (the case with
preExistingHistory: "symlink" and expecting historyKind === "symlink") add an
assertion that the target file's content equals the original sentinel content
(or remains untouched/uncreated) to ensure ensure_hermes_history_file did not
follow/truncate the symlink; reference runHermesGatewayRuntimeCleanup and
ensure_hermes_history_file to locate the code to change.
test/e2e-scenario/validation_suites/hermes/01-history-writable.sh (1)

46-46: 💤 Low value

Prefer the canonical e2e_sandbox_exec helper over direct openshell sandbox exec.

test/e2e-scenario/validation_suites/sandbox-exec.sh exists specifically to absorb the recurring drift (quoting, exit-code propagation, dry-run handling) of hand-rolled sandbox exec calls across migrated suite steps. These three direct invocations reintroduce that drift. Sourcing and using e2e_sandbox_exec "${sandbox_name}" -- <cmd> would keep this step on the shared contract.

This is optional given dry-run is already short-circuited at the top of the script.

Also applies to: 70-71, 79-80

🤖 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/e2e-scenario/validation_suites/hermes/01-history-writable.sh` at line
46, Replace the direct calls to "openshell sandbox exec" in this script with the
canonical helper "e2e_sandbox_exec" to ensure consistent quoting, exit-code
propagation, and dry-run handling; specifically, where the script currently
captures metadata into the "meta" variable using "openshell sandbox exec --name
\"${sandbox_name}\" -- stat -c '%F|%U:%G|%a' \"${HISTORY_PATH}\"" and the two
subsequent direct invocations, call e2e_sandbox_exec "${sandbox_name}" -- stat
-c '%F|%U:%G|%a' "${HISTORY_PATH}" and adapt other similar lines to use
e2e_sandbox_exec "${sandbox_name}" -- <original command>, preserving variable
usage (sandbox_name, HISTORY_PATH) and capturing output into the same variables.
🤖 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 `@test/e2e-scenario/validation_suites/hermes/01-history-writable.sh`:
- Around line 91-116: The shields_status_state helper currently swallows command
errors with "|| true" and returns "unknown" on parse failure; change it so the
nemoclaw call is not suppressed (remove "|| true") so failures propagate, and
after computing initial_state check for "unknown" and fail loudly (print an
explanatory error to stderr and exit 1) before calling probe_history_writable;
reference shields_status_state and initial_state to locate the change and ensure
the script exits when shields parsing fails rather than continuing silently.

---

Nitpick comments:
In `@test/e2e-scenario/validation_suites/hermes/01-history-writable.sh`:
- Line 46: Replace the direct calls to "openshell sandbox exec" in this script
with the canonical helper "e2e_sandbox_exec" to ensure consistent quoting,
exit-code propagation, and dry-run handling; specifically, where the script
currently captures metadata into the "meta" variable using "openshell sandbox
exec --name \"${sandbox_name}\" -- stat -c '%F|%U:%G|%a' \"${HISTORY_PATH}\""
and the two subsequent direct invocations, call e2e_sandbox_exec
"${sandbox_name}" -- stat -c '%F|%U:%G|%a' "${HISTORY_PATH}" and adapt other
similar lines to use e2e_sandbox_exec "${sandbox_name}" -- <original command>,
preserving variable usage (sandbox_name, HISTORY_PATH) and capturing output into
the same variables.

In `@test/hermes-start.test.ts`:
- Around line 462-475: The symlink test must also assert the symlink target was
not written-through: modify the helper runHermesGatewayRuntimeCleanup to
record/return the symlink target path and its file content (or absence) when
preExistingHistory === "symlink", then in the test that calls
runHermesGatewayRuntimeCleanup (the case with preExistingHistory: "symlink" and
expecting historyKind === "symlink") add an assertion that the target file's
content equals the original sentinel content (or remains untouched/uncreated) to
ensure ensure_hermes_history_file did not follow/truncate the symlink; reference
runHermesGatewayRuntimeCleanup and ensure_hermes_history_file to locate the code
to change.
🪄 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: 36dc62b4-a126-4be6-a94c-c2b78afde323

📥 Commits

Reviewing files that changed from the base of the PR and between 325ed77 and 1fba578.

📒 Files selected for processing (8)
  • agents/hermes/Dockerfile
  • agents/hermes/Dockerfile.base
  • agents/hermes/start.sh
  • test/e2e-scenario/scenarios/assertions/registry.ts
  • test/e2e-scenario/scenarios/migration-inventory.ts
  • test/e2e-scenario/validation_suites/hermes/01-history-writable.sh
  • test/e2e-scenario/validation_suites/suites.yaml
  • test/hermes-start.test.ts

Comment thread test/e2e-scenario/validation_suites/hermes/01-history-writable.sh
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
…fig seal

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@laitingsheng laitingsheng added the v0.0.58 Release target label Jun 3, 2026
@wscurran wscurran requested a review from cv June 3, 2026 16:46
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 26904869296
Target ref: 73faca1a904e2dc66c88719de0c06c5914f33a97
Workflow ref: main
Requested jobs: hermes-root-entrypoint-smoke-e2e,hermes-e2e
Summary: 2 passed, 0 failed, 0 skipped

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

@cv cv self-assigned this Jun 3, 2026
@cv cv merged commit 093eb78 into main Jun 3, 2026
27 checks passed
@cv cv deleted the fix/2432-hermes-history-shields-up branch June 3, 2026 19:46
@cv cv mentioned this pull request Jun 3, 2026
12 tasks
cv added a commit that referenced this pull request Jun 3, 2026
## 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, `fchown`s, and `fchmod`s 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 #2432.

## 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)

## Verification
- [x] `npx prek run --all-files` passes
- [x] `npm test` passes
- [x] Tests added or updated for new or changed behavior
- [x] 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](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: Carlos Villela <cvillela@nvidia.com>

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

## 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.

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

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: onboarding Onboarding FSM, provider setup, sandbox launch, or first-run flow bug-fix PR fixes a bug or regression integration: hermes Hermes integration behavior v0.0.58 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[station][Agent&Skills] Hermes TUI spams "Permission denied: /sandbox/.hermes/.hermes_history" on every interaction, blocking /exit

3 participants