Skip to content

Add audit fix plan for box new clean-checkout failures - #4

Merged
bmdavis419 merged 28 commits into
mainfrom
audit/2026-06-30-box-new-fixes
Jul 1, 2026
Merged

Add audit fix plan for box new clean-checkout failures#4
bmdavis419 merged 28 commits into
mainfrom
audit/2026-06-30-box-new-fixes

Conversation

@bmdavis419

@bmdavis419 bmdavis419 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a persisted audit and fix plan for the box new failure path on a clean checkout.
  • Documents the P0 blockers, including the stale Hermes installer pin and the silent verify_required failure.
  • Captures additional safety, coverage, and polish follow-ups identified during the audit.
  • Lays out a suggested execution order so the highest-impact fixes land first.

Testing

  • Not run: documentation-only change.
  • Reviewed the new audit plan for structure, scope, and references to the observed failure modes.
  • Confirmed the diff adds only docs/audit-2026-06-30-fixes.md and does not modify runtime code.

Note

Add failure diagnostics and audit fixes for box new clean-checkout failures

  • Adds --keep-on-failure/BOX_KEEP_ON_FAILURE=1 to box new and box load, preserving the VM for inspection when provisioning fails and saving a transcript under backups/.failed/
  • Extracts shared MCP handshake helpers into guest/lib-mcp.sh, sourced by both box and guest/hb, removing duplicate implementations
  • Adds flock-based daemon locks in guest/hb to serialize gateway and executor startup and avoid concurrent double-start races
  • Adds a large regression test suite across CLI surface, lock/rollback paths, doctor/MCP, hb workload, and tx9 host behaviors via new tests/regressions-*.sh scripts run by make test
  • Adds a GitHub Actions CI workflow (.github/workflows/check.yml) running make check on push and pull requests to main
  • Adds ops/tx9-backup-prune to bound backup growth by count and optional age, with supporting systemd timer and CIFS automount units
  • Fixes hermes-state extraction to enforce the MAX_UNCOMPRESSED cap on actual decompressed bytes rather than trusting zip metadata

Macroscope summarized f564a22.

Greptile Summary

This PR fixes the clean-checkout box new and box load failure path and adds supporting safeguards. The main changes are:

  • Adds --keep-on-failure and BOX_KEEP_ON_FAILURE=1 inspection paths.
  • Saves failed provisioning transcripts under backups/.failed/.
  • Fixes Hermes installer verification, launcher placement, and required-tool checks.
  • Adds flock-based guest daemon locking for Hermes gateway and Executor startup.
  • Hardens Hermes ZIP import extraction with actual decompressed byte limits.
  • Adds CI, backup retention tooling, systemd units, and broader regression tests.

Confidence Score: 5/5

The changes are merge-safe based on the documented scope and supporting safeguards.

The implementation is focused on improving failure observability, rollback control, installer verification, daemon locking, extraction bounds, and regression coverage without identified blocking issues.

T-Rex T-Rex Logs

What T-Rex did

  • The base state was inspected and found to lack the new contract, with generic errors observed.
  • The head state after changes showed provisioning failures reported as 'failed while provisioning' and VM preservation when keep flag/env is used, but host-side transcripts still show 'FAILED_LOG: <directory missing>'.
  • The CI regression coverage baseline was inspected, revealing no .github/workflows directory and that make -n check lists only tests/regressions.sh; cli-surface.sh is absent.
  • The CI regression coverage after state was inspected, showing workflow content for pushes to main, with jq installed and SHELLCHECK_VERSION pinned, and that make check now includes tests/cli-surface.sh and split regression scripts; make syntax test passes.
  • The provisioning before state was inspected, showing a stale pin and no 'check-hermes-pin' target, and verify_required_missing_hermes_exit=1 without a missing Hermes diagnostic.
  • The provisioning after state showed a newer pin, the check-hermes-pin target succeeding under stubs, verify_required logging a missing hermes tool, and focused syntax/static/regression commands exiting 0.
  • The mcp handshake before artifact captured the command, cwd, exit code, base inline helper location, regression pass, and full HTTP status/header/body snippets.
  • The mcp handshake after artifact captured the command, cwd, exit code, shared lib-mcp path sourcing, regression pass, and matching HTTP status/header/body snippets, with EXIT_CODE: 0.
  • The hb-locks supervision before artifact contained the generated validation script and output from /tmp/hbval-run.sh before, and showed all custom checks passed on HEAD^.
  • The hb-locks supervision after artifact contained the same script and the output from /tmp/hbval-run.sh after HEAD, showing all custom checks passed on head, plus the bundled regressions passing.
  • The hb-locks supervision diff artifact notes that no separate diff was created, but the executed output indicates HEAD^..HEAD changes only the box cmd_load and not the requested hb locking paths.
  • The audit plan docs before artifact captured /tmp/audit_check.sh <base> before, where docs/audit-2026-06-30-fixes.md does not exist and the result is FAIL.
  • The audit plan docs after artifact captured /tmp/audit_check.sh HEAD after, where the audit plan exists and all required checks PASS; the after run includes verbose grep for relevant headings and terms, including P0, Hermes installer pin, verify_required, P1/P2/P3, Reproduce/Fix, and Suggested execution order.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (7)

  1. General comment

    P1 PR scope claim says documentation-only, but diff modifies runtime and test files

    • Bug
      • The executed base-to-head diff contradicts the documented/testing scope claim that the PR only adds docs/audit-2026-06-30-fixes.md and does not modify runtime code. git diff --name-only ded8e51a1491f757f96f50106e38cbb883d5899e 92cf51ca375812f4115e609bc44162eccaa5d5d8 shows additional runtime files changed: Makefile, box.env, guest/hermes-state, and provision/provision.sh, plus test file tests/hermes-state.sh. The diffstat confirms these files contain actual modifications, not just documentation changes.
    • Cause
      • The PR description/scope contract is stale or inaccurate relative to the actual committed diff. Runtime and test changes were included while the documented claim remained documentation-only/no-runtime-code.
    • Fix
      • Update the PR description/testing claim to accurately disclose the runtime and test file changes, or remove the non-documentation changes from the PR if the intended contract is documentation-only.

    T-Rex Ran code and verified through T-Rex

  2. General comment

    P1 Failed provisioning transcripts are not saved for early provisioning failures

    • Bug
      • The user-visible contract says failed box new/box load save provisioning transcripts under backups/.failed/ before rollback. In the head run, both box new --keep-on-failure and BOX_KEEP_ON_FAILURE=1 ./box new fail during the provision-context streaming step, and both box load failure cases do the same, but every case reports failed_logs= with no transcript file created.
    • Cause
      • _provision_into only tees the second smolvm machine exec provisioning command to log_file; failures in the preceding tar-to-guest smolvm machine exec -i ... cat >/tmp/ctx.tgz return before anything is written to the log file. The later [[ -s "$log_file" ]] guard therefore skips creating backups/.failed/<name>-timestamp.log.
    • Fix
      • Capture the full _provision_into failure path, including the context streaming command, into the transcript file when a log path is provided, or create a transcript with at least the failing command/stage output even when the tee'd provisioning command is not reached.

    T-Rex Ran code and verified through T-Rex

  3. General comment

    P1 box load ... --keep-on-failure still rolls back the failed VM

    • Bug
      • The head README/help claims rollback can be skipped on a failed new/load; the CLI advertises --keep-on-failure for new and the PR objective says both box new and box load accept it. The executed head run of ./box load valid.tar.gz.gpg loadopt --keep-on-failure fails provisioning, runs rollback (machine delete --name loadopt -f), and leaves state_dir_exists=no.
    • Cause
      • cmd_load only initializes keep_on_failure from BOX_KEEP_ON_FAILURE; it does not parse or accept a third positional --keep-on-failure argument for load.
    • Fix
      • Update cmd_load argument parsing and usage to accept --keep-on-failure after the optional destination name, set keep_on_failure=1, and reject unknown extra arguments so typoed flags do not silently fall back to rollback.

    T-Rex Ran code and verified through T-Rex

  4. General comment

    P1 PR scope is not docs-only despite documentation-only claim

    • Bug
      • The requested validation contract allows a bug if the changed-files reality contradicts a docs-only/runtime-code-scope claim. The executed diff from base ded8e51a1491f757f96f50106e38cbb883d5899e to head 658c7cb032cf115e30201d5104beebdc45091fde shows many non-documentation/runtime-adjacent files changed, including box, box.env, guest/hb, guest/hermes-state, and provision/provision.sh, plus Makefile/workflow/test changes. This materially contradicts a docs-only PR description even though the audit-plan documentation itself is present and structured as claimed.
    • Cause
      • The PR includes implementation/runtime and test changes in addition to documentation and systemd unit hardening, but the user-visible scope claim says or implies docs-only.
    • Fix
      • Update the PR title/description to accurately state that runtime/provisioning/test files are changed, or split the runtime changes into a separate PR if this PR must remain documentation-only.

    T-Rex Ran code and verified through T-Rex

  5. General comment

    P1 Hermes gateway lock still allows double spawn before the new process is observable

    • Bug
      • The head runtime still allowed two concurrent _start_gateway calls to execute hermes gateway run --replace, producing hb_concurrent_spawn_count=2 in the after artifact. This violates the PR contract that the mkdir-based lock serializes concurrent gateway start attempts to prevent double spawn.
    • Cause
      • In guest/hb, the lock is held only around the check and immediate nohup hermes gateway run --replace spawn. _start_gateway writes $! and returns without waiting for _gateway_running/pgrep to observe the spawned gateway. A second caller can acquire the lock immediately after release, still observe no running gateway, and spawn another process. The relevant head code is guest/hb lines 104-115.
    • Fix
      • Keep the gateway lock until the spawned gateway is observable, or treat a fresh valid gateway.pid as an in-progress start while holding the lock. For example, after spawning, poll _gateway_running or the child PID for a short readiness/registration window before releasing the lock; subsequent callers should see the pending/running process and avoid spawning again.

    T-Rex Ran code and verified through T-Rex

  6. General comment

    P1 BOX_KEEP_ON_FAILURE warning is printed but rollback still deletes the failed VM and no transcript is saved

    • Bug
      • On head, a controlled failing box new with BOX_KEEP_ON_FAILURE=1 does not leave the VM available for inspection. The command exits 1 and prints inspection guidance, but the captured smolvm calls include machine delete --name failbox -f, the state listing contains only /tmp/kf-state, and the failed transcript listing is empty. This violates the claimed CLI/API contract that BOX_KEEP_ON_FAILURE=1 or --keep-on-failure skips rollback and saves a non-empty provisioning transcript under backups/.failed/<name>-<timestamp>.log.
    • Cause
      • The failure-handling path around cmd_new/_provision_into in box does not reliably suppress the EXIT-trap rollback or persist a transcript for this provisioning failure path. In the exercised path, cleanup still runs _rollback_created after the failure despite keep-on-failure being requested.
    • Fix
      • Update the cmd_new failure path in box so that when keep_on_failure is enabled it removes the box from the cleanup rollback list before any exit path can run, and ensure _provision_into captures a non-empty transcript for failures after the VM is created. Add a regression test using the smolvm fixture that asserts no machine delete call occurs, the VM state directory remains, guidance is printed, and backups/.failed/*.log exists and is non-empty.

    T-Rex Ran code and verified through T-Rex

  7. General comment

    P1 Provisioning failure transcripts are not saved under backups/.failed

    • Bug
      • The PR contract says failed box new/box load provisioning should save a host-side transcript under backups/.failed/<name>-<UTC>.log before rollback. In the head run, all failing provisioning cases report FAILED_LOG: <directory missing>, including default rollback cases and keep-on-failure cases for both new and load. The same run confirms other contract pieces are active: stage-specific errors and rollback skipping when requested.
    • Cause
      • _save_failed_transcript only copies when the temporary log is non-empty ([[ -s "$log_file" ]] || return 0). The exercised provisioning failure path fails before the transcript-producing tee "$log_file" command runs, so no log file is persisted even though the operation has a provisioning-stage transcript/diagnostic output and the user contract says failed new/load saves one.
    • Fix
      • Ensure failed provisioning attempts always create and persist a transcript file for the stage, including early failures before the provisioning command’s tee runs. For example, write the streaming/provisioning command output and failure diagnostics into the log file around both context upload and provisioning execution, and save even if only diagnostic text is available; then add regression assertions that backups/.failed contains a non-empty log for failed new and load both with and without keep-on-failure.

    T-Rex Ran code and verified through T-Rex

Reviews (12): Last reviewed commit: "fix(review): cmd_load reports which stag..." | Re-trigger Greptile

Live-tested ./box new end-to-end (twice, in isolated copies) plus five
parallel deep-read audits over provisioning, the Hermes supervisor, the
migration tool, ops/docs, and the test suite. Root-caused why box creation
fails today (stale HERMES_INSTALLER_SHA256 pin, then a silent set -e death
in verify_required because the Hermes installer's launcher never lands on
the agent user's PATH) and documented a prioritized, step-by-step fix plan
with reproduction instructions for every finding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

Adds an audit plan document, updates the Hermes installer pin and check, tightens provisioning and gateway startup behavior, enforces actual-byte ZIP extraction limits, adds a regression test, and ignores Python bytecode caches.

Changes

Hermes audit and hardening

Layer / File(s) Summary
Audit document P0-P1
docs/audit-2026-06-30-fixes.md
Adds the audit document framing plus the P0 and P1 sections covering blocking failures, race conditions, supervision gaps, and ZIP size enforcement gaps.
Audit document P2-P3
docs/audit-2026-06-30-fixes.md
Adds the P2 and P3 sections covering repair coverage gaps, CI and regression script notes, drift prevention items, and the suggested sequencing order.
Hermes pin check
Makefile, box.env
Updates the Hermes installer checksum pin and adds a Make target that compares the stored SHA256 against the live upstream installer.
Provisioning launcher and verification
provision/provision.sh
Adjusts provisioning PATH setup, rewires Hermes launcher discovery and symlinking, and expands required-tool verification to check Hermes, Executor, and the installed Hermes agent revision.
Gateway startup lock
guest/hb
Adds a filesystem lock for Hermes gateway startup and wraps gateway launch and cleanup in acquire/release helpers.
Box failure transcripts
box
Captures provisioning transcripts for box new/box load, adds keep-on-failure handling, and updates failure cleanup and usage text.
ZIP extraction limit
guest/hermes-state, tests/hermes-state.sh
Switches Hermes state extraction to count actual decompressed bytes and adds a regression test for the safety-limit failure path.
Cache ignore rule
.gitignore
Adds __pycache__/ to version-control ignore rules.

Possibly related PRs

  • davis7dotsh/tx9#2: Touches the same Hermes installer pin and related provisioning checks.
🚥 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
Title check ✅ Passed The title matches the main theme: an audit fix plan for box new clean-checkout failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description matches the changeset by summarizing the new audit plan and the related Hermes, verification, safety, and follow-up items.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/audit-2026-06-30-fixes.md`:
- Around line 99-104: Tighten the `verify_required` explanation to reflect the
actual control flow: when `INSTALL_HERMES` or `INSTALL_EXECUTOR` is set, a
failing `command -v` already stops execution under `set -e`, so do not describe
this path as a silent success. Update the diagnosis in `verify_required` to
focus on the real issue in that function: missing explicit failure handling and
the script’s silent exit behavior, and make sure the wording around the `git
rev-parse HEAD` fallback and the `command -v hermes` / `command -v executor`
checks matches the function’s actual return semantics.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b14d1f5-dc14-44ba-b531-daecdf5257a6

📥 Commits

Reviewing files that changed from the base of the PR and between ded8e51 and c56fad1.

📒 Files selected for processing (1)
  • docs/audit-2026-06-30-fixes.md

Comment on lines +99 to +104
3. **Separately** (a second, independent bug in the same function): even if (1)/(2)
didn't kill the script, `verify_required`'s actual return value is whatever
its *last* statement returns — the `git rev-parse HEAD` check — so the
`command -v hermes`/`command -v executor` checks on lines 213-214 were never
going to gate success/failure even if they survived `set -e`. Two bugs, same
six lines, masking each other.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tighten the verify_required diagnosis.

The current text says the function can return success while hermes/executor are missing, but when those INSTALL_* flags are set the failing command -v already trips set -e before that fallback can happen. The real bug is the silent exit and missing explicit failure handling, not a successful pass on absent tools.

Also applies to: 165-168

🤖 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 `@docs/audit-2026-06-30-fixes.md` around lines 99 - 104, Tighten the
`verify_required` explanation to reflect the actual control flow: when
`INSTALL_HERMES` or `INSTALL_EXECUTOR` is set, a failing `command -v` already
stops execution under `set -e`, so do not describe this path as a silent
success. Update the diagnosis in `verify_required` to focus on the real issue in
that function: missing explicit failure handling and the script’s silent exit
behavior, and make sure the wording around the `git rev-parse HEAD` fallback and
the `command -v hermes` / `command -v executor` checks matches the function’s
actual return semantics.

Upstream rotated hermes-agent.nousresearch.com/install.sh since the last
pin, so box new failed checksum verification on a clean checkout. Skimmed
the new installer (no suspicious network/eval patterns; same install flow
plus newer FHS-layout-detection logic for root installs) before re-pinning.

Also add `make check-hermes-pin`, a network-requiring target kept out of
`make check` (which stays hermetic), so drift can be caught proactively
instead of surfacing as a box new failure for a real user.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
Makefile (1)

29-29: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider adding a curl timeout.

curl -fsSL has no --max-time/--connect-timeout, so a hung connection would block the target indefinitely. Low impact since this is a manual, network-gated target excluded from make check.

🤖 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 `@Makefile` at line 29, The Makefile target that downloads the install script
uses curl without any timeout, so a stalled network request can hang the target
indefinitely. Update the curl invocation in the install flow to include a
reasonable connection and/or overall timeout while keeping the existing checksum
pipeline intact, so the manual target remains network-gated but won’t block
forever.
🤖 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 `@Makefile`:
- Around line 27-30: The check-hermes-pin recipe masks curl failures because the
curl output is piped into sha256sum, so a fetch error can look like a valid hash
mismatch. Update the Makefile target to detect the download failure directly in
the same shell block used by the pinned/live comparison, and only compute the
SHA256 after confirming curl succeeded. Keep the existing check-hermes-pin flow,
but make the live hash assignment fail fast with the intended fetch-failure
message instead of relying on the empty-output guard.

---

Nitpick comments:
In `@Makefile`:
- Line 29: The Makefile target that downloads the install script uses curl
without any timeout, so a stalled network request can hang the target
indefinitely. Update the curl invocation in the install flow to include a
reasonable connection and/or overall timeout while keeping the existing checksum
pipeline intact, so the manual target remains network-gated but won’t block
forever.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 705c5be1-b884-49f0-91ea-1361e0d56440

📥 Commits

Reviewing files that changed from the base of the PR and between c56fad1 and bf6c0fb.

📒 Files selected for processing (3)
  • Makefile
  • box.env
  • docs/audit-2026-06-30-fixes.md
✅ Files skipped from review due to trivial changes (1)
  • docs/audit-2026-06-30-fixes.md

Comment thread Makefile Outdated
Comment thread docs/audit-2026-06-30-fixes.md
Ben Davis and others added 2 commits June 30, 2026 14:04
The 100 GiB safety gate in _zip_layout only summed the ZIP central
directory's declared file_size, which is attacker-controlled metadata
that zipfile never cross-validates against the real inflate stream. A
crafted archive could declare a small size while extracting far more.

_extract now streams each member in 1 MiB chunks and tracks a running
total of actual bytes written, aborting the moment it exceeds the cap
regardless of what the archive claimed. Added a regression test that
lowers the cap and confirms extraction aborts on real bytes, not the
declared total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two stacked bugs in provision.sh's verify_required(): (1) `command -v
hermes` lived in an if-THEN body with no `|| { ...; return 1; }`, so the
moment it failed, set -e killed the whole provision.sh process with zero
output beyond "verifying required tools" — confirmed live, twice, on a
clean checkout. (2) even surviving that, the function's return value was
whatever its last statement returned, so the hermes/executor checks were
never actually gating success.

Root cause for why `command -v hermes` failed at all: provision.sh passes
an explicit --dir to the Hermes installer, which makes the installer skip
its own root/FHS auto-detection and link the `hermes` command into
$HOME/.local/bin (root's home, since this runs as root) instead of
/usr/local/bin — contradicting the comment that assumed FHS layout.
Verified by tracing the live (newly re-pinned, see P0-1) installer's
resolve_install_layout().

Fixes:
- install_hermes() now symlinks the real launcher location into
  $OPT/bin/hermes, which is on both the agent user's PATH (guest/profile.sh)
  and, with this change, provision.sh's own PATH too (added $OPT/bin to the
  top-of-script PATH export — needed for verify_required's `command -v
  hermes` to see it in the same process).
- Every check in verify_required now has explicit `|| { log ...; return 1; }`
  handling, matching the style already used for node/uv/nvim/claude/codex.
- Corrected the stale install-location comment.

Verified live end-to-end: `./box new` now reaches "provision complete" and
"Ready. Enter with: ./box enter <name>"; `./box doctor` passes all 17
checks; `./box rm --force` cleans up with no orphaned state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread Makefile Outdated
_start_gateway checked _gateway_running and, if not running, spawned
`hermes gateway run --replace` with no lock between the check and the
spawn. hb-workload's 20s reconcile loop and a host-initiated `hb up` (via
cmd_open/_prepare_runtime/cmd_repair) could both observe "not running" and
both exec the gateway, directly contradicting the single-writer guarantee
the gateway-enable confirmation ceremony exists to protect.

Added an mkdir-based lock (same pattern as box's _try_acquire_lock, with
the same stale-pid recovery) around the check-then-spawn in _start_gateway.
Every caller of _start_gateway (hb up, hb reconcile, gateway_enable) goes
through the same function, so one lock covers all paths without needing to
duplicate it in hb-workload.

Verified in isolation: 8 concurrent invocations of the locked
check-then-spawn section raced against each other, and exactly 1 actually
ran the "spawn" — confirms the lock serializes the window that was
previously racy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

T-Rex pricing update — T-Rex was free through June 2026. Effective July 1, 2026, T-Rex adds 2 credits on top of the standard 1-credit review (3 total). T-Rex settings

Comment thread provision/provision.sh
Comment on lines +157 to +158
if [[ -x "$HOME/.local/bin/hermes" ]]; then
ln -sf "$HOME/.local/bin/hermes" "$OPT/bin/hermes"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Keep launcher executable
This symlink points into root's home while the agent user is expected to run hermes through $OPT/bin. On a normal root-owned home directory, agent cannot traverse /root, so verify_required can pass as root while the box still cannot launch Hermes as agent.

Artifacts

Repro: shell harness that creates the root-home launcher symlink and tests root versus non-root execution

  • Contains supporting evidence from the run (text/x-shellscript; charset=utf-8).

Repro: command output showing root-side success and non-root Permission denied through the OPT symlink

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: provision/provision.sh
Line: 157-158

Comment:
**Keep launcher executable**
This symlink points into root's home while the `agent` user is expected to run `hermes` through `$OPT/bin`. On a normal root-owned home directory, `agent` cannot traverse `/root`, so `verify_required` can pass as root while the box still cannot launch Hermes as `agent`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/hermes-state.sh`:
- Around line 357-366: The test setup in the hermes-state shell script is
leaking Python temp directories because the `mkdtemp()` calls for `d`, `stage`,
and `external_stage` are not tied to the shell `$tmp` cleanup. Update the
temp-dir handling in the `_zip_layout` test block to use `TemporaryDirectory()`
contexts or create the directories under the existing bash-managed temp root so
they are removed automatically. Keep the changes localized around the temp setup
used with `zipfile.ZipFile`, `module._zip_layout`, and the
`stage`/`external_stage` paths.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bc4264dd-0c07-41ea-9be6-e4d66082f9eb

📥 Commits

Reviewing files that changed from the base of the PR and between bf6c0fb and 85c0413.

📒 Files selected for processing (6)
  • .gitignore
  • docs/audit-2026-06-30-fixes.md
  • guest/hb
  • guest/hermes-state
  • provision/provision.sh
  • tests/hermes-state.sh
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • docs/audit-2026-06-30-fixes.md

Comment thread tests/hermes-state.sh Outdated
Comment thread docs/audit-2026-06-30-fixes.md Outdated
the latter is more in keeping with the project's existing host-side systemd
pattern (`ops/systemd/`), but the cheap fix is the restart loop.

### P1-3: `gateway_is_disabled()` checks a marker file, not process state

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium docs/audit-2026-06-30-fixes.md:286

The P1-3 entry describes a failure scenario that cannot occur in the current guest/hb code. gateway_enable() calls rm -f "$GATEWAY_DISABLED" before _start_gateway, and on failure re-touches $GATEWAY_DISABLED — but the described 'spawned, then later failed and re-touched as disabled' path does not exist, since there is no code after _start_gateway succeeds that can fail before the function returns. The real stale-marker case is gateway_disable(), which touches $GATEWAY_DISABLED before calling _stop_gateway; if _stop_gateway fails, box doctor reports 'intentionally disabled' while a gateway process is still alive. As written, this entry would send the fix to gateway_enable and miss the actual bug in gateway_disable. Consider correcting P1-3 to target gateway_disable()'s ordering and fix suggestion accordingly.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @docs/audit-2026-06-30-fixes.md around line 286:

The P1-3 entry describes a failure scenario that cannot occur in the current `guest/hb` code. `gateway_enable()` calls `rm -f "$GATEWAY_DISABLED"` before `_start_gateway`, and on failure re-touches `$GATEWAY_DISABLED` — but the described 'spawned, then later failed and re-touched as disabled' path does not exist, since there is no code after `_start_gateway` succeeds that can fail before the function returns. The real stale-marker case is `gateway_disable()`, which touches `$GATEWAY_DISABLED` *before* calling `_stop_gateway`; if `_stop_gateway` fails, `box doctor` reports 'intentionally disabled' while a gateway process is still alive. As written, this entry would send the fix to `gateway_enable` and miss the actual bug in `gateway_disable`. Consider correcting P1-3 to target `gateway_disable()`'s ordering and fix suggestion accordingly.

Comment thread provision/provision.sh
Comment thread docs/audit-2026-06-30-fixes.md Outdated
before it calls `hb reconcile`, or simply rely on (1) since `hb reconcile`
already calls into `_start_gateway`.

### P1-2: `hb-workload` itself is unsupervised

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium docs/audit-2026-06-30-fixes.md:255

The P1-2 entry is factually wrong about the failure mode. box:_create_base starts hb-workload via exec runuser ... hb-workload, so hb-workload runs as PID 1 inside the guest — if it dies, PID 1 exits and the VM stops entirely. On normal deployments ops/systemd/tx9-box@.service invokes ops/tx9-host supervise %i, which restarts any VM whose smolvm machine status is no longer running. The current text claims the workload "persists unmanaged until the VM reboots" and recommends an internal while true; do ... hb-workload ...; done restart loop. That loop would suppress the VM-exit signal that the host-side supervisor depends on, hiding failures from the existing restart path. Correct the description to reflect that the VM exits (and is restarted by the host supervisor) rather than persisting unmanaged, and remove or revise the internal restart-loop recommendation so it does not mask failures from tx9-host supervise.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @docs/audit-2026-06-30-fixes.md around line 255:

The `P1-2` entry is factually wrong about the failure mode. `box:_create_base` starts `hb-workload` via `exec runuser ... hb-workload`, so `hb-workload` runs as PID 1 inside the guest — if it dies, PID 1 exits and the VM stops entirely. On normal deployments `ops/systemd/tx9-box@.service` invokes `ops/tx9-host supervise %i`, which restarts any VM whose `smolvm machine status` is no longer `running`. The current text claims the workload "persists unmanaged until the VM reboots" and recommends an internal `while true; do ... hb-workload ...; done` restart loop. That loop would suppress the VM-exit signal that the host-side supervisor depends on, hiding failures from the existing restart path. Correct the description to reflect that the VM exits (and is restarted by the host supervisor) rather than persisting unmanaged, and remove or revise the internal restart-loop recommendation so it does not mask failures from `tx9-host supervise`.

Comment thread guest/hb Outdated
Ben Davis and others added 2 commits June 30, 2026 14:13
A failed `box new`/`box load` produced exactly one line of explanation
("box creation failed") before _rollback_created deleted the VM — the only
place the real error lived. Both live P0-1/P0-2 failures during this audit
were only diagnosable by bypassing the trap-driven rollback manually; a
normal user had no equivalent escape hatch.

- _provision_into now optionally tees the provisioning transcript (the part
  that already shells guest output through the host process) to a caller-
  supplied log file.
- cmd_new and cmd_load capture that transcript to a temp file, and on
  failure persist it to backups/.failed/<name>-<timestamp>.log before
  calling die, so the evidence survives rollback.
- Added BOX_KEEP_ON_FAILURE=1 (or `box new <name> --keep-on-failure`) to
  skip rollback entirely and leave the VM up for direct inspection, with
  hints printed for how to look at it and clean it up afterward.
- cmd_new's die message now names which stage failed (creating the base VM
  / provisioning / preparing the runtime) instead of collapsing all three
  into one undifferentiated message.

Verified live: deliberately broke HERMES_GIT_SHA to force a real
provisioning failure under BOX_KEEP_ON_FAILURE=1. Confirmed the transcript
landed in backups/.failed/ with the actual git error, the die message said
"while provisioning", and the VM was left running (smolvm machine status
showed it alive) instead of being silently deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…test

Every box new/repair (full mode) installed whatever was currently `latest`
on the npm registry for claude-code, codex, and executor — two boxes
created a week apart, or the same box before/after a repair, could
silently end up on different versions with no way to pin or reproduce.

Added CLAUDE_CODE_VERSION/CODEX_VERSION/EXECUTOR_VERSION to box.env,
empty by default (preserves current latest-on-install behavior). When
set, install_claude/install_codex/install_executor in provision.sh now
install the pinned version via npm's @Version syntax.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
box (1)

737-764: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report the failing load stage.

cmd_new now reports whether base creation, provisioning, or runtime prep failed, but this cmd_load branch still collapses base creation and provisioning into destination provisioning failed.

Proposed fix
-  local keep_on_failure=0 log_file failed_log=""
+  local keep_on_failure=0 log_file failed_log="" stage=""
...
-  if ! _create_base "$n" || ! _provision_into "$n" full "$log_file"; then
+  if ! _create_base "$n"; then
+    stage="creating the base VM"
+  elif ! _provision_into "$n" full "$log_file"; then
+    stage="provisioning"
+  fi
+  if [[ -n "$stage" ]]; then
...
-    die "destination provisioning failed"
+    die "box load failed while $stage"
   fi
🤖 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 `@box` around lines 737 - 764, The cmd_load failure path still uses a generic
message for two different steps, so update the branch around _create_base and
_provision_into to report which stage failed. Split the combined check into
separate handling for base creation versus provisioning, and make the final die
message reflect the specific failing stage instead of always saying “destination
provisioning failed.”
🧹 Nitpick comments (1)
box (1)

596-596: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Preserve the provisioning exit status without relying on global pipefail.

The tee pipeline can report tee’s status instead of smolvm’s if pipefail is not active here. Process substitution keeps transcript capture while returning the guest command status.

Proposed fix
-    smolvm machine exec --name "$n" -- bash -lc "$cmd" 2>&1 | tee "$log_file"
+    smolvm machine exec --name "$n" -- bash -lc "$cmd" > >(tee "$log_file") 2>&1
🤖 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 `@box` at line 596, The provisioning command pipeline is losing the guest
command’s exit status because `smolvm machine exec` is piped through `tee`,
which can mask failures unless global `pipefail` is set. Update the provisioning
flow around the `smolvm machine exec` invocation to keep transcript capture
while preserving the command’s real status, using process substitution or an
equivalent approach instead of a plain pipe; locate the change in the shell
logic that builds and runs `cmd` for the machine execution.
🤖 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 `@box`:
- Around line 658-662: Make the transcript save path best-effort in the failure
handling block around the existing transcript copy logic in the
rollback/keep-on-failure flow (the `failed_log` save path used in the
provisioning handler). Guard the `mkdir -p backups/.failed` and `cp "$log_file"
"$failed_log"` steps so they cannot abort under `errexit`, and only emit the
“transcript saved” warning when the copy actually succeeds. Apply the same fix
to the duplicated save block referenced by the other occurrence so rollback
behavior still follows the original failure.
- Around line 812-817: The restore failure path in the box restore logic is
missing the `_provision_into "$n" assets` output in `log_file`, so later
provisioning errors are lost before rollback; update the restore/provision flow
to tee that call’s transcript to the log and ensure the failure handler
preserves the transcript before cleanup. Also adjust the keep-on-failure
messaging in the same restore handler to reflect that the incomplete destination
is being left in place for inspection, not that cleanup is still being
attempted.

---

Outside diff comments:
In `@box`:
- Around line 737-764: The cmd_load failure path still uses a generic message
for two different steps, so update the branch around _create_base and
_provision_into to report which stage failed. Split the combined check into
separate handling for base creation versus provisioning, and make the final die
message reflect the specific failing stage instead of always saying “destination
provisioning failed.”

---

Nitpick comments:
In `@box`:
- Line 596: The provisioning command pipeline is losing the guest command’s exit
status because `smolvm machine exec` is piped through `tee`, which can mask
failures unless global `pipefail` is set. Update the provisioning flow around
the `smolvm machine exec` invocation to keep transcript capture while preserving
the command’s real status, using process substitution or an equivalent approach
instead of a plain pipe; locate the change in the shell logic that builds and
runs `cmd` for the machine execution.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a2b6842-2586-4eb6-9d69-472f126668de

📥 Commits

Reviewing files that changed from the base of the PR and between 85c0413 and ecb1a53.

📒 Files selected for processing (2)
  • box
  • docs/audit-2026-06-30-fixes.md
✅ Files skipped from review due to trivial changes (1)
  • docs/audit-2026-06-30-fixes.md

Comment thread box Outdated
Comment thread box
Ben Davis and others added 2 commits June 30, 2026 14:18
hb-workload was exec'd as the VM's boot command, so if the loop process
itself died (OOM-killed, an unguarded edge case under its `set -u`), nothing
restarted it — health reconciliation and bridge socat processes stayed
unmanaged until the next VM reboot.

Dropped the `exec` and wrapped hb-workload in an outer restart loop as the
VM's boot command instead, so the supervisor (PID 1 in the guest) restarts
it on any exit.

Verified live: created a box, confirmed hb-workload was running under the
new supervisor loop, force-killed it (pkill -9), and confirmed a fresh
hb-workload process came back up within the 2s restart delay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gateway_is_disabled() only checked for the GATEWAY_DISABLED marker file.
If gateway_enable() spawned the gateway via _start_gateway() and something
later in that call path failed before returning, the marker was re-touched
as disabled but nothing verified the already-spawned process was actually
killed first — box doctor would then report "intentionally disabled" while
a real gateway process was alive underneath that claim.

gateway_is_disabled() now requires both the marker AND _gateway_running
being false. gateway_enable()'s failure path explicitly calls
_stop_gateway before re-touching the marker, instead of assuming the
marker alone is sufficient.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread box
Comment thread box Outdated
Comment thread guest/hb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
box.env (1)

22-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the executor package reference.

EXECUTOR_VERSION documents itself via npm view executor versions, but "executor" is a generic name on the npm registry. Worth a one-line comment clarifying the exact scoped/registry package this pin targets (matching whatever provisioning resolves), to avoid future confusion when someone runs that exact npm view command and gets an unrelated package.

🤖 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 `@box.env` at line 22, The EXECUTOR_VERSION comment is ambiguous because `npm
view executor versions` points to a generic package name, so update the inline
note on `EXECUTOR_VERSION` to explicitly name the exact package/repository scope
that provisioning resolves to. Keep the reference near `EXECUTOR_VERSION` in
box.env and make it clear which executor package the version pin applies to, so
the lookup command is not misleading.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@box.env`:
- Line 22: The EXECUTOR_VERSION comment is ambiguous because `npm view executor
versions` points to a generic package name, so update the inline note on
`EXECUTOR_VERSION` to explicitly name the exact package/repository scope that
provisioning resolves to. Keep the reference near `EXECUTOR_VERSION` in box.env
and make it clear which executor package the version pin applies to, so the
lookup command is not misleading.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dd652058-9807-4c48-a92b-8f48751227d9

📥 Commits

Reviewing files that changed from the base of the PR and between ecb1a53 and 14b89ff.

📒 Files selected for processing (5)
  • box
  • box.env
  • docs/audit-2026-06-30-fixes.md
  • guest/hb
  • provision/provision.sh
✅ Files skipped from review due to trivial changes (1)
  • docs/audit-2026-06-30-fixes.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • provision/provision.sh

assets_only() only ran make_agent, install_config, place_assets, seed_data,
and write-manifest — never install_hermes or install_executor. A box whose
Hermes or Executor install failed or went missing could only be fixed by
the full, multi-minute reinstall path (cmd_repair always calls
_provision_into full); there was no cheap "retry just the broken piece"
repair, and assets mode (only used by cmd_load's post-restore step) left
those tools broken if they were already broken.

install_hermes/install_executor are now idempotent (skip reinstall if
already on PATH and, for hermes, already at the pinned git SHA — the same
bar verify_required already holds them to) and assets_only calls both
right after make_agent, plus verify_required at the end to confirm the
repair actually worked.

Verified live: created a box, deleted the hermes and executor binaries to
simulate a failed install, ran assets-mode provisioning directly, and
confirmed both were reinstalled and resolvable again. Re-ran assets mode a
second time and confirmed the idempotent guards skipped reinstalling
already-healthy tools ("hermes already installed... skipping reinstall").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread guest/hb Outdated
Comment thread provision/provision.sh
Ben Davis and others added 2 commits June 30, 2026 14:33
Five of thirteen box subcommands had zero test coverage (ls, rm, enter,
stop, start), alongside untested code paths in already-tested commands:
the _assign_port exhaustion loop and control-character archive rejection.

New tests/cli-surface.sh covers:
- box ls: empty registry, populated registry (running + missing-from-smolvm
  rows), and --all (which needed the smolvm fixture's bare `machine ls`
  filled in — it previously fell through to exit 2).
- box rm: --force happy path, the non-interactive refusal path (run under
  setsid to genuinely detach the controlling terminal — `[[ -r /dev/tty ]]`
  on Linux is a permission-bits check, not proof a terminal exists, so a
  plain stdin redirect doesn't reach that code path the way it would on a
  real interactive shell), and the "type the name to confirm" mismatch vs.
  match cases via a real pty (python's pty.fork(), since that prompt reads
  from /dev/tty directly and can't be faked with ordinary redirection).
- box stop/start: delegate to the right smolvm command, and a held per-box
  lock blocks a concurrent stop before smolvm is ever invoked.
- box enter: execs the expected `smolvm machine exec -it ... -- login -f
  agent` command line.
- _assign_port: pre-registers a 1-port range as occupied and confirms the
  documented "no free host port in lo-hi" exhaustion message.

tests/lifecycle-smoke.sh's existing archive-safety fixture generator gained
two more cases (control character in a member name, and in a symlink's
link target) alongside the eleven it already covered, confirming
_validate_archive's safe_text() check end to end through both the direct
validator and the box load path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
make check is fast and fully hermetic (the tests/fixtures/smolvm stub means
no real VM/KVM/network access is needed), so every regression the suite
catches previously depended on a human remembering to run it locally
before merging.

Added .github/workflows/check.yml, installs shellcheck + jq (python3 is
already on the runner image) and runs `make check`. Updated the
README and the pinned assertion in tests/static.sh that previously
asserted CI didn't exist yet — left it as a real assertion (checks the
workflow file exists and actually runs `make check`) rather than quietly
deleting the check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread box Outdated
Comment on lines +740 to +743
cmd_load() {
local file="${1:?usage: box load <archive.tar.gz.gpg> [name]}" n="${2:-restored}" raw guest_tmp
local keep_on_failure=0 log_file failed_log=""
[[ "${BOX_KEEP_ON_FAILURE:-0}" != 1 ]] || keep_on_failure=1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Parse load keep flag
cmd_load only reads BOX_KEEP_ON_FAILURE; the new --keep-on-failure CLI flag is not accepted for load. Running ./box load backup.tar.gz.gpg --keep-on-failure treats the flag as the restored box name and fails _validate_name, so the inspection path is unavailable for load failures unless callers set the env var.

Artifacts

Repro: shell harness that runs the failing load command with a dummy archive

  • Contains supporting evidence from the run (text/x-shellscript; charset=utf-8).

Repro: command output showing --keep-on-failure rejected as an invalid box name

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: box
Line: 740-743

Comment:
**Parse load keep flag**
`cmd_load` only reads `BOX_KEEP_ON_FAILURE`; the new `--keep-on-failure` CLI flag is not accepted for `load`. Running `./box load backup.tar.gz.gpg --keep-on-failure` treats the flag as the restored box name and fails `_validate_name`, so the inspection path is unavailable for load failures unless callers set the env var.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread Makefile
tests/regressions.sh had grown to 1131 lines covering ~10 unrelated concern
areas in one flat file, so a single failing assertion meant scanning up to
1131 lines for context. The tempdir+trap+cleanup boilerplate was also
independently reimplemented (with slightly different rigor) in three other
test files, and make_repo() was duplicated inline in lifecycle-smoke.sh.

Extracted tests/lib.sh: the canonical tmp-dir+trap setup (with the same
signal-to-exit-code mapping box itself uses — 129/130/143 — including the
self-check assertions that previously only lived in regressions.sh),
wait_for_file/wait_for_pattern, and make_repo(). All four pre-existing test
files (lifecycle-smoke.sh, hermes-state.sh, plus the new cli-surface.sh)
now source it instead of duplicating their own version.

Split regressions.sh into five files by concern, grouped by what they
actually source/invoke (verified with a line-by-line diff against the
original to confirm zero content was lost or duplicated across the split):
- regressions-tx9-host.sh: host-preflight checks, ops/tx9-host
  health/supervise/backup-credential behavior
- regressions-box-core.sh: provisioning ownership (own_hermes_home),
  restore permission modeling, resource defaults, Git bundle verification
- regressions-hb-workload.sh: guest/hb + guest/hb-workload behavior
  (reconcile, bridges, gateway enable/disable, doctor, MCP wiring)
- regressions-doctor-mcp.sh: box's cmd_doctor + the MCP protocol-health
  suite, plus cmd_open/cmd_repair lifecycle guards
- regressions-lock-rollback.sh: registry/lock/rollback races, save/restore
  failure injection

Makefile's `test` target and SHELL_FILES now glob tests/regressions-*.sh
(and tests/static.sh mirrors the same glob for its own executable-bit and
syntax checks), so adding a new regression-category file doesn't require
touching either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The first CI run failed lint: apt's packaged shellcheck flagged
`[[ cond ]] && cmd || true` as SC2015 across several pre-existing lines
(box, guest/hb, provision/provision.sh) and the (now-split) test files.
Current shellcheck deliberately doesn't apply SC2015 to `[[ ]]` tests
(it's a clean boolean test, not a command whose own failure needs separate
handling) — confirmed locally with shellcheck 0.11.0, which reports
nothing for that pattern. Ubuntu's apt repos lag upstream releases, so
`apt-get install shellcheck` pulled an older version with the stricter
behavior.

Download the pinned v0.11.0 release binary directly instead, matching
what local development already runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread Makefile Outdated
Ben Davis and others added 2 commits June 30, 2026 14:52
This invocation of box ls was missing PATH="tests/fixtures:$PATH", so
_host_preflight basic's `command -v smolvm` silently resolved to a real
smolvm binary on dev machines that have one installed, masking the gap
locally. CI has no real smolvm, so the first real run failed with
"smolvm not found on PATH" — exactly the kind of gap make check running
in CI (P2-3) exists to catch.

Verified by re-running the full suite with the real smolvm hidden from
PATH (filtering it out of every PATH directory before invoking make
check), matching what CI sees; everything passes now, including this
case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
HERMES_IMPORT_PATH_MAP defaulted to a Davis-specific macOS path
(/Users/davis=...), shipped as the committed default in box.env. Anyone
else cloning the repo got a path mapping wrong for them, silently applied
to every import-hermes invocation.

Defaults to empty now. The --map OLD=NEW per-invocation flag already
covers this (docs/nexus-operations.md's migration runbook already
demonstrates it), so a non-empty default isn't structurally required;
operators who want a standing default can still set one in their own
deployment's box.env.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread guest/hb Outdated
[[ ! -e "$QUIESCE_FILE" && ! -e "$GATEWAY_DISABLED" ]] || return 0
command -v hermes >/dev/null 2>&1 || { echo "hermes not installed" >&2; return 1; }
_acquire_gateway_lock || { echo "could not acquire Hermes gateway lock" >&2; return 1; }
trap _release_gateway_lock RETURN

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Clear return trap
This RETURN trap is never unset, so after _start_gateway returns the shell keeps running _release_gateway_lock on later function returns. A later hb operation in the same process can remove gateway.lock while another process owns it, reopening the double-spawn race this lock is meant to close. Capture the previous trap or add trap - RETURN before returning from _start_gateway.

Artifacts

Repro: focused bash harness for lingering RETURN trap and lock deletion

  • Contains supporting evidence from the run (text/x-shellscript; charset=utf-8).

Repro: harness output showing lingering RETURN trap deletes recreated gateway.lock

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: guest/hb
Line: 105

Comment:
**Clear return trap**
This `RETURN` trap is never unset, so after `_start_gateway` returns the shell keeps running `_release_gateway_lock` on later function returns. A later `hb` operation in the same process can remove `gateway.lock` while another process owns it, reopening the double-spawn race this lock is meant to close. Capture the previous trap or add `trap - RETURN` before returning from `_start_gateway`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Ben Davis and others added 3 commits June 30, 2026 14:59
All three units already ran as the unprivileged tx9 user, but none set
any of the standard cheap hardening directives, despite needing nothing
outside /opt/tx9, /var/backups/tx9, /var/lib/tx9, and /etc/tx9.

Added to each [Service] block: NoNewPrivileges=true, ProtectHome=true,
ProtectSystem=strict, PrivateTmp=true, and ReadWritePaths scoped to those
four paths.

Verified rather than assumed:
- systemd-analyze verify accepts all three units with the new directives
  (no syntax/semantic errors).
- /dev/kvm read/write access is unaffected — confirmed live, since these
  directives only restrict filesystem mounts, not device nodes; KVM access
  stays governed by the existing SupplementaryGroups=kvm membership.
- ReadWritePaths mechanics actually grant writes inside listed paths and
  block them outside, confirmed with systemd-run against scratch
  directories using the identical directive set.
- Ran the real backup flow (mkdir destination, write .partial, sha256sum,
  atomic rename) end to end under the exact hardening directives via
  systemd-run with a stubbed box/tx9-host — succeeds.

Found and fixed a real gap while doing this: TX9_NAS_DIR is an
operator-configured path (e.g. a CIFS mount) that ProtectSystem=strict's
static ReadWritePaths can't anticipate. Documented in
docs/nexus-operations.md that enabling NAS replication now requires a
`systemctl edit` drop-in adding that path to ReadWritePaths, or
replication silently starts failing closed (the local backup still
succeeds either way).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both /var/backups/tx9 and any configured NAS target grew unbounded
forever — neither box save nor tx9-backup@.service ever pruned anything,
and the CIFS automount was documentation-only (no .mount/.automount unit
in the repo).

- ops/systemd/mnt-davis\x2dvault-tx9.{mount,automount}: a starting-point
  CIFS automount matching the /mnt/davis-vault/tx9 example already in
  docs/nexus-operations.md. Verified clean with systemd-analyze verify.
  Filename matters here — confirmed the correct systemd-escaped name via
  `systemd-escape --path` (only the hyphen inside "davis-vault" escapes
  to \x2d; the separator before "tx9" doesn't), since the audit notes for
  this item had it subtly wrong.
- ops/tx9-backup-prune <box>: keeps the newest TX9_BACKUP_RETAIN_COUNT
  backups (default 14) per box, with an optional TX9_BACKUP_RETAIN_DAYS
  cap applied on top (never keep something older than N days even if
  it's within the count). Prunes both TX9_BACKUP_DIR and TX9_NAS_DIR,
  skips a missing NAS directory rather than failing, and never touches
  another box's backups (matched by filename prefix).
- ops/systemd/tx9-backup-prune@.{service,timer}: same hardening
  directives as the other units (P3-2), runs daily after the backup
  timer.

Verified live: built synthetic backup sets spanning 20 days and confirmed
count-only pruning, the count+days intersection, idempotent re-runs,
graceful handling of a missing NAS directory, and input validation —
matching what the new automated regression test in
tests/regressions-tx9-host.sh now covers going forward. Also confirmed
ProtectSystem=strict's static ReadWritePaths can't anticipate an
operator-configured TX9_NAS_DIR, and documented the systemctl edit
drop-in needed to avoid silently-failing-closed replication.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tent

README.md and docs/usage-guide.html independently maintained the same
command table, config table, and backup/restore guarantees prose, with no
generation step tying them together — a guaranteed drift source as either
file gets updated. usage-guide.html also isn't actually rendered anywhere
(no Pages config, no build step; GitHub shows raw .html as source), so
keeping it self-contained for "offline" use wasn't buying anything that
justified the duplication-maintenance cost.

Picked README as canonical (it's what renders on GitHub and what
tests/static.sh already greps against, per the audit's own reasoning).
usage-guide.html keeps its distinct narrative/tutorial framing and
walkthrough commands, but the three literally-duplicated, easy-to-drift
pieces now link to README instead of restating it: the command table, the
configuration table (pointed at box.env directly, since that's the actual
source of truth for what each setting does — there wasn't a matching table
in README to begin with), and the backup/restore guarantees.

Verified the HTML still parses with balanced tags after trimming.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ProtectHome=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=/opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium systemd/tx9-backup-prune@.service:14

ProtectSystem=strict makes the entire filesystem read-only except for the paths listed in ReadWritePaths. The prune script targets TX9_NAS_DIR (e.g. /mnt/davis-vault/tx9) in addition to /var/backups/tx9, but TX9_NAS_DIR is not in ReadWritePaths. Every rm -f the script issues against the NAS mount fails with EROFS, so NAS retention never runs even though the service installs without error. Add the NAS mount path to ReadWritePaths, or have the unit read TX9_NAS_DIR and include it dynamically.

-ReadWritePaths=/opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9
+ReadWritePaths=/opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9 /mnt/davis-vault/tx9
Also found in 2 other location(s)

ops/tx9-backup-prune:70

TX9_NAS_DIR pruning is wired into the new script at line 70, but the shipped tx9-backup-prune@.service runs with ProtectSystem=strict and ReadWritePaths=/opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9. Any NAS path such as the documented /mnt/davis-vault/tx9 is therefore read-only inside the service. When prune_dir reaches an old NAS backup and executes rm -f, the unit fails with Read-only file system, so scheduled pruning never works for NAS backups.

docs/nexus-operations.md:93

The new sandbox guidance is too narrow: tx9-backup@.service only allows writes under /opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9, but this section tells readers to add a drop-in only when TX9_NAS_DIR is outside those paths. A box that sets TX9_BACKUP_DIR to any other location will fail the same way—the backup job cannot write its primary archive there under ProtectSystem=strict—yet the runbook never warns about that case or tells operators to add the custom backup directory to ReadWritePaths=.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @ops/systemd/tx9-backup-prune@.service around line 14:

`ProtectSystem=strict` makes the entire filesystem read-only except for the paths listed in `ReadWritePaths`. The prune script targets `TX9_NAS_DIR` (e.g. `/mnt/davis-vault/tx9`) in addition to `/var/backups/tx9`, but `TX9_NAS_DIR` is not in `ReadWritePaths`. Every `rm -f` the script issues against the NAS mount fails with `EROFS`, so NAS retention never runs even though the service installs without error. Add the NAS mount path to `ReadWritePaths`, or have the unit read `TX9_NAS_DIR` and include it dynamically.

Also found in 2 other location(s):
- ops/tx9-backup-prune:70 -- `TX9_NAS_DIR` pruning is wired into the new script at line `70`, but the shipped `tx9-backup-prune@.service` runs with `ProtectSystem=strict` and `ReadWritePaths=/opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9`. Any NAS path such as the documented `/mnt/davis-vault/tx9` is therefore read-only inside the service. When `prune_dir` reaches an old NAS backup and executes `rm -f`, the unit fails with `Read-only file system`, so scheduled pruning never works for NAS backups.
- docs/nexus-operations.md:93 -- The new sandbox guidance is too narrow: `tx9-backup@.service` only allows writes under `/opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9`, but this section tells readers to add a drop-in only when `TX9_NAS_DIR` is outside those paths. A box that sets `TX9_BACKUP_DIR` to any other location will fail the same way—the backup job cannot write its primary archive there under `ProtectSystem=strict`—yet the runbook never warns about that case or tells operators to add the custom backup directory to `ReadWritePaths=`.

Comment on lines +14 to +15
ReadWritePaths=/opt/tx9 /var/backups/tx9 /var/lib/tx9 /etc/tx9
ExecStart=/opt/tx9/ops/tx9-backup-prune %i

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Allow NAS pruning
ops/tx9-backup-prune also deletes old files from TX9_NAS_DIR, but this unit runs with ProtectSystem=strict and only allows writes to the listed local paths. With the documented NAS example under /mnt/davis-vault/tx9, the timer can read the mount but rm fails, so NAS retention remains unbounded unless operators add a second drop-in that the docs only mention for tx9-backup@.

Artifacts

Repro: sandbox harness running the real prune script with NAS excluded from writable paths

  • Contains supporting evidence from the run (text/x-shellscript; charset=utf-8).

Repro: command output showing local pruning succeeds and NAS pruning fails with read-only filesystem errors

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: ops/systemd/tx9-backup-prune@.service
Line: 14-15

Comment:
**Allow NAS pruning**
`ops/tx9-backup-prune` also deletes old files from `TX9_NAS_DIR`, but this unit runs with `ProtectSystem=strict` and only allows writes to the listed local paths. With the documented NAS example under `/mnt/davis-vault/tx9`, the timer can read the mount but `rm` fails, so NAS retention remains unbounded unless operators add a second drop-in that the docs only mention for `tx9-backup@`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Ben Davis and others added 3 commits June 30, 2026 15:40
Code review (local multi-angle review plus CodeRabbit/Macroscope/Greptile
on the PR) surfaced real bugs in the P1-1 gateway lock and exposed that
_executor_up had the identical unlocked check-then-spawn race the lock was
built to fix, just for the other daemon:

- guest/hb's mkdir+pid-file lock (and an mv-based "TOCTOU fix" attempt to
  it) both had a confirmed race in stale-lock reclaim: two contenders that
  both observe a dead owner can each believe they're entitled to reclaim,
  and one's cleanup can destroy a lock the other has already freshly
  re-acquired. Verified directly — 5-8 of 10 racing processes would
  "win" the critical section in repeated trials. Replaced with flock,
  which is kernel-enforced and needs no manual dead-owner detection at
  all (auto-released the instant every fd referencing it closes).
- Generalized the lock into _acquire_daemon_lock/_release_daemon_lock and
  put _executor_up's check-then-spawn behind it too, closing the same
  double-spawn class of bug for Executor that P1-1 only closed for the
  gateway.
- Found and fixed a second bug introduced while building the flock
  version: a long-lived daemon spawned while the lock fd is still open
  inherits it, and (since flock locks attach to the open file
  description, not a single fd) that inherited copy keeps the lock held
  for the daemon's entire lifetime if the spawning shell ever dies before
  explicitly releasing — wedging every future acquire attempt. Added
  _spawn_without_lock_fd, which closes the lock fd inside the spawned
  subshell before exec'ing the daemon. Verified: a stand-in long-lived
  process spawned without this wrapper kept the lock held after the
  spawner explicitly released it; with the wrapper, a fresh acquire
  succeeded in milliseconds.
- Removed the RETURN trap _start_gateway used to release its lock —
  confirmed empirically that a RETURN trap set inside a function fires
  again on every subsequent return up its call stack (not just its own),
  so e.g. calling _start_gateway from gateway_enable's failure branch
  could release the lock a second time after _stop_gateway's up-to-30s
  retry loop, a window wide enough for another process to have
  legitimately re-acquired it. Replaced with explicit release calls at
  every return point.
- gateway_enable's failure handler called _stop_gateway unconditionally,
  including when _start_gateway failed only because it lost the lock race
  to a concurrent legitimate operation — which would kill that other
  operation's gateway. _start_gateway now returns a distinct code (2) for
  "couldn't acquire the lock" vs. 1 for a real failure, and
  gateway_enable only stops the gateway for the latter.

Verified live end-to-end in a real guest VM, not just locally: killed
Executor, fired 10 concurrent `hb reconcile` calls, and confirmed via `ps
-ef` (not just log lines) that exactly one process actually spawned it
while the other nine correctly observed "already running." Added a
regression test exercising the lock's mutual exclusion and the
spawn-without-fd-leak property so this doesn't silently regress.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two real bugs caught by review bots:

- `for f in tests/regressions-*.sh; do ./"$f"; done` doesn't fail fast: a
  failing script in the middle of the glob doesn't stop the loop, and
  make only sees the *last* iteration's exit status — an earlier
  regression failure could be silently swallowed by a later file
  succeeding, defeating the whole point of running this in CI. Added
  `|| exit 1`.
- check-hermes-pin piped curl directly into sha256sum; a failed curl
  produces zero bytes, and sha256sum of zero bytes is still a
  valid-looking (just wrong) hash, so the existing `-z "$live"` empty
  check never caught it — a network failure would misleadingly report
  "does NOT match upstream" instead of "fetch failed". Now downloads to
  a temp file first and checks curl's own exit status before hashing.
  Also switched to detecting shasum as a fallback hasher, since plain
  sha256sum isn't on stock macOS (the project's other supported host).

Verified: forced a curl failure against an unreachable host and confirmed
the target now reports "failed to fetch" with exit 1, instead of
quietly hashing zero bytes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…traversal

Two bots (Macroscope, Greptile) flagged that install_hermes's symlink
($OPT/bin/hermes -> $HOME/.local/bin/hermes, where $HOME is root's home
since the script runs as root) requires agent to traverse root's home
directory, and that a default 700 root home would block that — citing it
as a likely regression.

Verified live both ways: on the project's actual ubuntu:24.04 base image,
/root is 755 by default, and agent could already execute hermes through
the symlink chain (not just resolve it via command -v — actually ran
`hermes --version` as agent). Then verified the converse: manually set
/root to 700 in the same running box, confirmed agent immediately lost
the ability to run hermes (exit 127), then applied the fix and confirmed
it restored execution.

So the bots' specific claim didn't hold against the actual deployed
image, but the underlying concern — relying on an upstream base image's
default permissions rather than asserting what's actually required — is
real and worth closing regardless of what's true today. install_hermes
now explicitly chmod o+x's the exact path components agent needs to
traverse, right after creating the symlink.

Also added a clarifying note to the P1-5 audit entry: a bot flagged that
its illustrative code snippet resets the byte counter inside the loop,
which would let a bomb split across many small entries evade the cap.
Confirmed the actual implementation in guest/hermes-state was never
written that way (the counter is declared once, outside the per-entry
loop) — the snippet was just illustrative shorthand that didn't show the
necessary placement. Added a note so a future reader doesn't copy it
literally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread provision/provision.sh
Comment on lines +95 to +97
if command -v hermes >/dev/null 2>&1 && [[ "$(git -C "$install_dir" rev-parse HEAD 2>/dev/null)" == "$sha" ]]; then
log "hermes already installed at pinned $sha — skipping reinstall"
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium provision/provision.sh:95

The early-return guard at line 95 skips own_hermes_home, which is the only step that fixes /data/home/agent/.hermes ownership back to agent:agent after root-run installs. In assets repair mode, when Hermes is already pinned but its state dir is root-owned, provisioning succeeds yet the agent user still cannot write Hermes state. Consider calling own_hermes_home before returning, or moving it above the guard.

  if command -v hermes >/dev/null 2>&1 && [[ "$(git -C "$install_dir" rev-parse HEAD 2>/dev/null)" == "$sha" ]]; then
+    own_hermes_home
     log "hermes already installed at pinned $sha — skipping reinstall"
     return 0
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @provision/provision.sh around lines 95-97:

The early-return guard at line 95 skips `own_hermes_home`, which is the only step that fixes `/data/home/agent/.hermes` ownership back to `agent:agent` after root-run installs. In `assets` repair mode, when Hermes is already pinned but its state dir is root-owned, provisioning succeeds yet the `agent` user still cannot write Hermes state. Consider calling `own_hermes_home` before returning, or moving it above the guard.

Ben Davis and others added 4 commits June 30, 2026 15:45
- tests/cli-surface.sh's no-controlling-terminal box rm test needs
  setsid/timeout (util-linux), not on stock macOS — the project's other
  supported host. Skip with a clear message instead of failing the whole
  suite when they're unavailable, matching the existing IPv6-unavailable
  skip pattern elsewhere in the suite. Verified the skip path actually
  triggers by building a PATH that excludes only those two binaries.
- tests/hermes-state.sh's P1-5 regression test used three bare
  tempfile.mkdtemp() calls with no cleanup, leaking directories under
  /tmp on every test run. Switched to TemporaryDirectory() context
  managers.
- check-hermes-pin's curl call had no timeout, so a hung connection would
  block the target indefinitely (low-impact since it's excluded from
  `make check`, but cheap to fix).
- Clarified the EXECUTOR_VERSION comment in box.env: "executor" is a
  generic npm package name, so note which actual project it resolves to
  rather than just repeating the ambiguous `npm view` command.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t saves

Three real gaps in the P0-3 failure-handling work, caught by CodeRabbit
and Greptile:

- The transcript-save block (mkdir backups/.failed + cp) ran under set -e
  with no failure guard. A full disk or read-only checkout there would
  abort the handler *before* keep-on-failure's _clear_created ran — so
  the one thing an operator needs most (the box preserved for inspection)
  would silently not happen, in exactly the failure conditions where it
  matters most. Extracted _save_failed_transcript(), which never lets a
  logging failure propagate past a warning.
- cmd_load's second failure path (the long _pause_box/restore/gateway-
  disable/verify-state/resume/prepare-runtime chain) never teed
  _provision_into "$n" assets's output and never saved any transcript
  before rollback, unlike cmd_new and cmd_load's first failure path. Now
  passes log_file through and calls _save_failed_transcript there too.
- cmd_load only read BOX_KEEP_ON_FAILURE, not the --keep-on-failure flag
  cmd_new accepts — load <file> [name] had a free third positional slot
  for it, so there was no structural reason for the asymmetry. Added it,
  and fixed the keep-on-failure message in the second failure path
  (it previously said "attempting cleanup" even when keep-on-failure had
  just left the box in place specifically to skip cleanup).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flagged independently by Macroscope and a local review pass: unlike
install_hermes's idempotency guard (which checks both presence and the
pinned git SHA), install_executor only checked `command -v executor` —
once EXECUTOR_VERSION is pinned and bumped, `box repair` (assets mode)
would log "executor already installed — skipping reinstall" and silently
leave the box on the old version forever, exactly the kind of drift P1-4
was meant to close.

Now compares the installed executor's actual version (parsed from
`executor --version`) against EXECUTOR_VERSION when one is pinned, and
only skips reinstall on a real match. Unpinned (empty EXECUTOR_VERSION)
keeps the original presence-only/latest-on-install behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-reference from the historical audit entry to the later flock-based
replacement, so a reader following P1-1's fix steps doesn't implement the
version with the confirmed TOCTOU bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread box

cmd_load() {
local file="${1:?usage: box load <archive.tar.gz.gpg> [name]}" n="${2:-restored}" raw guest_tmp
local file="${1:?usage: box load <archive.tar.gz.gpg> [name] [--keep-on-failure]}" n="${2:-restored}" raw guest_tmp

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium box:755

cmd_load assigns n="${2:-restored}" before checking for --keep-on-failure, so the invocation ./box load <file> --keep-on-failure parses the flag as the box name. _validate_name then rejects --keep-on-failure and the restore never starts, even though the usage string advertises the flag as usable without an explicit name. Consider detecting --keep-on-failure in $2 before assigning it to n, or parsing positional and flag arguments separately.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @box around line 755:

`cmd_load` assigns `n="${2:-restored}"` before checking for `--keep-on-failure`, so the invocation `./box load <file> --keep-on-failure` parses the flag as the box name. `_validate_name` then rejects `--keep-on-failure` and the restore never starts, even though the usage string advertises the flag as usable without an explicit name. Consider detecting `--keep-on-failure` in `$2` before assigning it to `n`, or parsing positional and flag arguments separately.

Ben Davis added 2 commits June 30, 2026 22:12
…d Go rewrite

The initialize/validate/delete-session JSON-RPC sequence was byte-for-byte
duplicated between box's _mcp_initialize and guest/hb's _mcp_auth_works
(~55 lines). Extract it into guest/lib-mcp.sh, sourced by both, so the
security-sensitive handshake logic can't drift between host and guest checks.

Also record why a Go rewrite was evaluated and deferred (docs/future-go-port.md),
so the reasoning doesn't need to be re-derived if it comes up again.
CodeRabbit flagged that cmd_load collapsed base-VM creation and provisioning
into one generic "destination provisioning failed" message, unlike cmd_new
(which already reports the specific failing stage). Apply the same stage
pattern here.
@bmdavis419

Copy link
Copy Markdown
Contributor Author

Triaged the CodeRabbit/Macroscope/Greptile findings from earlier pushes against the current code (HEAD f564a22).

Auto-resolved by the platform (marked "No longer relevant as of " or "✅ Addressed in commits...") once later fix(review) commits landed — verified a sample by hand, all genuinely fixed:

  • Makefile curl-failure masking, sha256sum/macOS fallback, fail-fast regression loop, timeout/setsid availability
  • provision.sh root-home symlink permissions, executor version-drift skip guard
  • guest/hb stale-lock TOCTOU, missing/empty pid-file crash window
  • box transcript persistence best-effort, load provisioning-failure transcript capture, --keep-on-failure under set -e
  • tests/hermes-state.sh mkdtemp leak

Manually verified as false positives / already covered (auto-resolution markers were coincidental line-drift from the unrelated guest/lib-mcp.sh extraction, not real fixes, so checked by hand):

  • box:_provision_into's tee pipeline "masks a provisioning failure" — false positive, box runs under set -euo pipefail globally (line 4), so the pipeline's exit status already reflects smolvm machine exec, not tee.
  • guest/hb:gateway_enable "unconditionally stops a healthy gateway on failure" — the lock-busy case (status 2) is already excluded from _stop_gateway; the only remaining case is "multiple gateway processes detected," which the code already treats as a fault to clean up, consistent with the project's single-writer invariant. Not a bug.
  • box.env:22 EXECUTOR_VERSION comment ambiguity — already carries the exact package-scope clarification requested.

Fixed (f564a22): cmd_load collapsed base-VM creation and provisioning into one generic "destination provisioning failed" message; now reports the specific failing stage, matching cmd_new's existing pattern.

Skipped, documented (audit-doc narrative precision, not functional code issues):

  • P1-2/P1-3 doc entries in docs/audit-2026-06-30-fixes.md — Macroscope argues the original problem narratives could better attribute root cause (e.g. P1-3's story arguably fits gateway_disable better than gateway_enable). The fixes described and shipped for both are correct and already verified against current code (gateway_is_disabled() checks live process state, not just the marker; gateway_enable's failure path calls _stop_gateway first). Left the historical problem narrative as-is since it accurately reflects the bug as understood pre-fix, similar to not rewriting an issue's original repro after landing the fix.
  • verify_required doc wording (docs md ~99-104) — already tightened to match the requested framing in an earlier commit; no change needed.

@bmdavis419
bmdavis419 merged commit b4d2c18 into main Jul 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant