Add native installers and Vite+-managed runtime - #16
Conversation
Provisioning now uses each tool's official installer instead of npm globals: - claude: native installer (claude.ai/install.sh) — self-contained binary, no Node dependency; CLAUDE_CODE_CHANNEL picks stable/latest or a pinned version - codex: native installer (chatgpt.com/codex/install.sh) — standalone binary releases under /opt/hermes-box/tooling/codex - hermes: the normal curl | bash setup, always latest; the pinned-SHA / git-bundle / installer-checksum machinery (HERMES_GIT_SHA, HERMES_INSTALLER_SHA256, HERMES_GIT_BUNDLE, make check-hermes-pin) is removed - node/npm: managed by Vite+ (vite.plus) at /opt/hermes-box/tooling/vite-plus, defaulted to LTS via `vp env default lts`; NodeSource apt install removed - executor: installed with `vp install -g executor` so both containers can update it in place with `vp install -g executor@latest` The agent user owns the vite-plus/claude/codex tooling trees so self-updates (claude update, installer re-runs, vp installs) work without sudo, and /data/home/agent/.local/bin is first on PATH so updated launchers on the durable volume win over image-baked copies. tx9 list/create/open URLs now render the machine's Tailscale IP when one is present (100.64.0.0/10 interface scan), else the primary LAN IP, else hostname — bare hostnames often don't resolve from the machines these URLs are opened on. TX9_URL_HOST still overrides. Verified e2e: image builds, tx9 create passes all doctor checks, agent and executor containers resolve node v24 LTS + all tools through the vp shims, claude update to a newer version wins on PATH, and vp install -g executor@latest updates in place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified each update command in a live box and fixed the two that failed: - hermes update: ran `uv pip install --upgrade` into a root-owned venv and died with EACCES. provision.sh now chowns the hermes checkout + venv to agent (both fresh installs and the assets repair path). - codex update: the standalone updater detects its install by looking under $CODEX_HOME/packages, but runtime CODEX_HOME points at /data (config/auth) while the binaries live in the image tooling dir. profile.sh adds a codex() wrapper that points only `codex update` at the install home. Working already, no change needed: vp up -g, vp upgrade (0.2.2→0.2.3 in place), claude update (2.1.195→2.1.202 onto /data, wins on PATH). backup: exclude ./home/agent/.local/bin — claude/codex self-updates drop absolute-target launcher symlinks there, which the archive validator (correctly) refuses; the launchers are recreated by the next self-update in a restored box. tx9 enter <box> --executor drops into the executor container as agent with the box profile sourced, for maintenance like `vp install -g executor@latest` (executor has no self-update subcommand; vp is its update path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR removes Hermes pinning, switches provisioning and runtime tooling to Vite+, updates URL host resolution to use Tailscale or outbound IP detection, and adds an executor-container entry mode with matching backup exclusions. ChangesRemove Hermes installer pinning
Adopt Vite+ managed Node/tooling
Tailscale/outbound IP based URL host resolution
CLI executor enter mode
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
| if curl -fsSL https://hermes-agent.nousresearch.com/install.sh \ | ||
| | bash -s -- "${args[@]}" --hermes-home "$HERMES_HOME" --skip-setup --non-interactive; then |
There was a problem hiding this comment.
🔍 Hermes installer SHA256 verification and Git SHA pinning are both removed
The old provisioning flow downloaded the Hermes installer to a temp file, verified its SHA256 against HERMES_INSTALLER_SHA256 from box.env, then ran it with a pinned --commit SHA. The new flow at provision/provision.sh:127-128 pipes curl | bash directly with no checksum verification and no commit pin — the comment says "always installing the latest Hermes release." The corresponding make check-hermes-pin target and the static test grep -q 'HERMES_INSTALLER_SHA256' are also removed. This is a deliberate simplification but represents a significant reduction in reproducibility — two builds at different times may get different Hermes versions.
Was this helpful? React with 👍 or 👎 to provide feedback.
| local install_dir=/usr/local/lib/hermes-agent | ||
| # Idempotent: skip the multi-minute reinstall if hermes is already on PATH. | ||
| # Lets `assets` repair mode call this safely on every run. | ||
| if command -v hermes >/dev/null 2>&1 && [[ -x "$install_dir/venv/bin/python" ]]; then |
There was a problem hiding this comment.
🟡 Medium provision/provision.sh:103
The idempotency guard in install_hermes skips reinstallation whenever a venv already exists, so re-running provisioning after a newer Hermes release keeps the stale version installed. The check at line 103 (command -v hermes && [[ -x "$install_dir/venv/bin/python" ]]) treats any prior install as current and returns early, logging hermes already installed — skipping reinstall. Since the PR switched from a pinned commit to the upstream "latest" installer, a box provisioned with an older Hermes will never receive updates on subsequent provisioning runs. Consider comparing the installed version against the target release before skipping, or removing the skip when CODEX_RELEASE/the requested version differs from what is already installed.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @provision/provision.sh around line 103:
The idempotency guard in `install_hermes` skips reinstallation whenever a venv already exists, so re-running provisioning after a newer Hermes release keeps the stale version installed. The check at line 103 (`command -v hermes && [[ -x "$install_dir/venv/bin/python" ]]`) treats *any* prior install as current and returns early, logging `hermes already installed — skipping reinstall`. Since the PR switched from a pinned commit to the upstream "latest" installer, a box provisioned with an older Hermes will never receive updates on subsequent provisioning runs. Consider comparing the installed version against the target release before skipping, or removing the skip when `CODEX_RELEASE`/the requested version differs from what is already installed.
| // interface carries one. Detected by address range rather than by shelling | ||
| // out to the tailscale CLI, so it works regardless of how tailscaled was | ||
| // installed. | ||
| func tailscaleIP() string { |
There was a problem hiding this comment.
🟡 Medium box/box.go:248
tailscaleIP() returns the first local IPv4 address in 100.64.0.0/10, treating any CGNAT address as Tailscale. That range is the general RFC 6598 carrier-grade NAT shared space, not unique to Tailscale, so a host on an ISP/mobile/other-VPN CGNAT link is misidentified as having Tailscale. URLHost() then publishes an arbitrary non-Tailscale CGNAT address in dashboard/open URLs, producing links unreachable from other machines. Consider checking that the address belongs to a Tailscale interface (e.g. by interface name or via the Tailscale API) rather than relying on the CGNAT range alone.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @internal/box/box.go around line 248:
`tailscaleIP()` returns the first local IPv4 address in `100.64.0.0/10`, treating any CGNAT address as Tailscale. That range is the general RFC 6598 carrier-grade NAT shared space, not unique to Tailscale, so a host on an ISP/mobile/other-VPN CGNAT link is misidentified as having Tailscale. `URLHost()` then publishes an arbitrary non-Tailscale CGNAT address in dashboard/open URLs, producing links unreachable from other machines. Consider checking that the address belongs to a Tailscale interface (e.g. by interface name or via the Tailscale API) rather than relying on the CGNAT range alone.
There was a problem hiding this comment.
🟡 Medium
Line 43 in d142c89
tx9 enter --executor <box> fails when the agent container is stopped or missing, even though the executor container exists and is running. The readiness check b.AgentState != "running" || b.ExecutorState != "running" unconditionally requires both containers and then calls box.Start, which rejects a box missing its agent with missing container(s). This blocks the executor-only maintenance path that --executor is meant to provide. Consider gating the agent checks on !*intoExecutor so executor entry only requires the executor container to be present and running.
- if b.AgentState != "running" || b.ExecutorState != "running" {
+ if (!*intoExecutor && b.AgentState != "running") || b.ExecutorState != "running" {🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @internal/cli/cmd_enter.go around line 43:
`tx9 enter --executor <box>` fails when the agent container is stopped or missing, even though the executor container exists and is running. The readiness check `b.AgentState != "running" || b.ExecutorState != "running"` unconditionally requires both containers and then calls `box.Start`, which rejects a box missing its agent with `missing container(s)`. This blocks the executor-only maintenance path that `--executor` is meant to provide. Consider gating the agent checks on `!*intoExecutor` so executor entry only requires the executor container to be present and running.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
provision/provision.sh (2)
96-152: 🔒 Security & Privacy | 🟠 Major | ⚖️ Poor tradeoffHermes install lost its integrity check with no replacement.
Per
docs/nexus-operations.md's new historical note, the priorHERMES_GIT_SHA/HERMES_GIT_BUNDLE/HERMES_INSTALLER_SHA256pinning verified the fetched installer against a known-good hash before execution. That verification is now gone entirely —install_hermesrunscurl ... | bashunconditionally with no hash/signature check. This is an intentional simplification per the PR's stated goals, but it's worth confirming the team has accepted the reduced supply-chain guarantee for this specific installer (vs. e.g. pinning the release tag/checksum while still auto-updating).🤖 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 `@provision/provision.sh` around lines 96 - 152, The install_hermes flow now executes the downloaded installer without any integrity verification, so restore a safety check or explicitly gate the change. Update install_hermes to validate the fetched script before piping it into bash, using the existing HERMES_INSTALLER_SHA256 or equivalent pinning approach referenced by the historical note, or document and isolate the intentional trust tradeoff if the team wants to keep curl | bash. Keep the fix centered on install_hermes and its curl/bash invocation so the installer still supports auto-update while retaining a known-good checksum or signature check.
50-152: 🔒 Security & Privacy | 🟠 Major | ⚖️ Poor tradeoffMultiple new curl-pipe-to-shell installs without integrity verification.
Static analysis flags CWE-494 across the vite.plus, uv, claude, codex, and hermes installers — all now piped directly into
bash/shwith no checksum or signature check before execution. This is a broader supply-chain exposure than before: any of these five upstream endpoints being compromised or serving a tampered script results in code execution as root during provisioning. This tradeoff appears deliberate (see the "always install latest" design goal replacing pinned installs), so raising for awareness/sign-off rather than as a blocker.🤖 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 `@provision/provision.sh` around lines 50 - 152, The installer functions curl-pipe remote scripts directly into bash/sh without any integrity check, so update the provisioning flow to verify each download before execution. For the install paths in install_vite_plus, install_uv, install_claude, install_codex, and install_hermes, add a checksum/signature validation step (or pin to a trusted artifact) before invoking the shell installer, and fail closed if verification does not pass. Use the existing function names and logging points to keep the changes localized and preserve the current install behavior only after validation succeeds.Source: Linters/SAST tools
🧹 Nitpick comments (1)
guest/profile.sh (1)
31-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winHardcoded path duplicates
$HBinstead of reusing it.
$HBis already in scope (used two lines above forVP_HOME), but the update path here is hardcoded as/opt/hermes-box/tooling/codex. If$HBis ever repointed, this silently falls out of sync with the rest of the file.♻️ Proposed fix
codex() { if [ "${1:-}" = "update" ]; then - CODEX_HOME=/opt/hermes-box/tooling/codex command codex "$@" + CODEX_HOME="$HB/tooling/codex" command codex "$@" else command codex "$@" 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 `@guest/profile.sh` around lines 31 - 42, The update branch in codex() hardcodes the install path instead of reusing the existing HB variable. Update the CODEX_HOME assignment inside the update case to derive the codex tooling path from HB so it stays consistent with the VP_HOME setup and other references in this script. Keep the non-update command path unchanged.
🤖 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 `@internal/box/box.go`:
- Line 272: The UDP probe in box.go uses net.Dial, which triggers the noctx lint
rule and can fail CI. Update the dial in the relevant box helper to use a
net.Dialer with DialContext instead, supplying a bounded context (with context
and time imports as needed) so the behavior stays equivalent while satisfying
golangci-lint.
---
Outside diff comments:
In `@provision/provision.sh`:
- Around line 96-152: The install_hermes flow now executes the downloaded
installer without any integrity verification, so restore a safety check or
explicitly gate the change. Update install_hermes to validate the fetched script
before piping it into bash, using the existing HERMES_INSTALLER_SHA256 or
equivalent pinning approach referenced by the historical note, or document and
isolate the intentional trust tradeoff if the team wants to keep curl | bash.
Keep the fix centered on install_hermes and its curl/bash invocation so the
installer still supports auto-update while retaining a known-good checksum or
signature check.
- Around line 50-152: The installer functions curl-pipe remote scripts directly
into bash/sh without any integrity check, so update the provisioning flow to
verify each download before execution. For the install paths in
install_vite_plus, install_uv, install_claude, install_codex, and
install_hermes, add a checksum/signature validation step (or pin to a trusted
artifact) before invoking the shell installer, and fail closed if verification
does not pass. Use the existing function names and logging points to keep the
changes localized and preserve the current install behavior only after
validation succeeds.
---
Nitpick comments:
In `@guest/profile.sh`:
- Around line 31-42: The update branch in codex() hardcodes the install path
instead of reusing the existing HB variable. Update the CODEX_HOME assignment
inside the update case to derive the codex tooling path from HB so it stays
consistent with the VP_HOME setup and other references in this script. Keep the
non-update command path unchanged.
🪄 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: daa996bc-7168-4d0a-a86c-b17f93d63fe7
📒 Files selected for processing (13)
MakefileREADME.mdbox.envdocs/nexus-operations.mdguest/hbguest/profile.shinternal/box/box.gointernal/box/box_test.gointernal/cli/cmd_backup.gointernal/cli/cmd_enter.gointernal/cli/dispatch.goprovision/provision.shtests/static.sh
💤 Files with no reviewable changes (1)
- README.md
| // (and skips loopback and docker bridge addresses that a plain interface | ||
| // scan couldn't tell apart from the real LAN address). | ||
| func outboundIP() string { | ||
| conn, err := net.Dial("udp4", "192.0.2.1:9") // TEST-NET-1, never routed |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
net.Dial is flagged by the noctx linter and may fail CI.
golangci-lint reports this line as an error (net.Dial must not be called. use (*net.Dialer).DialContext). The UDP dial only performs route selection and won't block, but to satisfy the configured linter, switch to a Dialer with a bounded context.
♻️ Proposed fix using DialContext
-func outboundIP() string {
- conn, err := net.Dial("udp4", "192.0.2.1:9") // TEST-NET-1, never routed
- if err != nil {
- return ""
- }
- defer conn.Close()
+func outboundIP() string {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ var d net.Dialer
+ conn, err := d.DialContext(ctx, "udp4", "192.0.2.1:9") // TEST-NET-1, never routed
+ if err != nil {
+ return ""
+ }
+ defer conn.Close()Requires adding context and time imports.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| conn, err := net.Dial("udp4", "192.0.2.1:9") // TEST-NET-1, never routed | |
| func outboundIP() string { | |
| ctx, cancel := context.WithTimeout(context.Background(), time.Second) | |
| defer cancel() | |
| var d net.Dialer | |
| conn, err := d.DialContext(ctx, "udp4", "192.0.2.1:9") // TEST-NET-1, never routed | |
| if err != nil { | |
| return "" | |
| } | |
| defer conn.Close() |
🧰 Tools
🪛 golangci-lint (2.12.2)
[error] 272-272: net.Dial must not be called. use (*net.Dialer).DialContext
(noctx)
🤖 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 `@internal/box/box.go` at line 272, The UDP probe in box.go uses net.Dial,
which triggers the noctx lint rule and can fail CI. Update the dial in the
relevant box helper to use a net.Dialer with DialContext instead, supplying a
bounded context (with context and time imports as needed) so the behavior stays
equivalent while satisfying golangci-lint.
Source: Linters/SAST tools
| codex() { | ||
| if [ "${1:-}" = "update" ]; then | ||
| CODEX_HOME="$HB/tooling/codex" command codex "$@" | ||
| else | ||
| command codex "$@" | ||
| fi | ||
| } |
There was a problem hiding this comment.
🟡 Health check for codex installation always passes, even when the binary is missing
The codex wrapper shell function (codex() at guest/profile.sh:36-42) is sourced into the health-check script (guest/hb:5), so the "codex installed" check (guest/hb:483) always succeeds because command -v finds the function, not the real binary.
Impact: A box with a missing or broken codex binary will pass hb doctor with a false "ok" for the codex-installed check, hiding the problem from operators.
The shell function shadows the binary lookup in command -v
guest/hb sources guest/profile.sh at line 5. The new codex() function defined at guest/profile.sh:36-42 is now in scope for every hb invocation. The _command_exists helper at guest/hb:468 uses command -v "$1", which returns success for shell functions — not just external binaries. So _check "codex installed" _command_exists codex at guest/hb:483 will always pass, regardless of whether the actual codex binary exists on disk.
The same issue affects _unwire_legacy at guest/hb:351 and wire_mcp at guest/hb:381, where command -v codex guards calls to codex mcp .... Those paths have their own error handling so the impact is limited there, but doctor is the primary diagnostic tool and should not silently pass.
A fix would be to check for the binary explicitly, e.g. command -v codex >/dev/null 2>&1 && [[ "$(type -t codex)" != "function" ]] || command -v "$(which codex 2>/dev/null)" ..., or more simply, have _command_exists use type -P (which only finds files on PATH, ignoring functions) instead of command -v.
Prompt for agents
The codex() shell function wrapper defined in guest/profile.sh lines 36-42 is sourced by guest/hb at line 5. This causes command -v codex (used in _command_exists at guest/hb:468) to always return true, since it finds the shell function rather than the actual binary. This makes the doctor check at guest/hb:483 ineffective for detecting a missing codex binary.
Two possible approaches:
1. In guest/hb, change _command_exists to use type -P instead of command -v. type -P only searches PATH for files and ignores shell functions and builtins. This would fix the check for codex and any future wrapper functions.
2. Alternatively, export the codex function only for interactive shells (e.g. guard it with a check like [[ $- == *i* ]]) so it doesn't affect hb's non-interactive sourcing of profile.sh.
Approach 1 is simpler and more robust. The change would be in guest/hb around line 468, changing _command_exists from command -v to type -P.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if curl -fsSL https://hermes-agent.nousresearch.com/install.sh \ | ||
| | bash -s -- "${args[@]}" --hermes-home "$HERMES_HOME" --skip-setup --non-interactive; then |
There was a problem hiding this comment.
🟨 Removal of installer integrity verification allows undetected supply-chain tampering
The previous code verified the Hermes installer's SHA-256 checksum against a pinned value (HERMES_INSTALLER_SHA256 in box.env) before executing it as root. The new code at provision/provision.sh:127-128 pipes curl output directly into bash with no integrity check:
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- ...
This removes a defense-in-depth measure against supply-chain attacks. If the upstream installer is compromised or modified (MITM, CDN compromise, account takeover), the tampered script executes as root during image build with no detection.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
tx9 enter --executorfor executor-container maintenance.Testing
Note
Add native installers for Claude/Codex and replace NodeSource/npm globals with Vite+-managed runtime
vp) instead of NodeSource andnpm install -g;VP_HOMEis set and PATH updated in both provision.sh and profile.sh$OPT/binand owned byagenttx9 entergains an--executorflag to open an interactive shell inside the executor container with box environment sourcedURLHostin box.go now prefers the machine's Tailscale IP, then primary LAN IP, before falling back to hostname/localhost, affecting dashboard URL reachability~/.local/bin/claudeand~/.local/bin/codexsymlinks to avoid archive validation failuresMacroscope summarized 68787f7.
Greptile Summary
This PR moves box provisioning to a Vite+-managed runtime and native tool installers. The main changes are:
tx9 enter --executorfor executor-container maintenance.Confidence Score: 5/5
Safe to merge with normal installer-drift risk.
No confirmed correctness issue was found in the changed paths. The provisioning, PATH setup, backup exclusions, and runtime checks are updated consistently around the new installer model.
provision/provision.shandguest/profile.shdeserve normal attention because they define the new runtime and install layout.What T-Rex did
Important Files Changed
tx9 enter --executorwith executor-container shell environment setup.Reviews (2): Last reviewed commit: "fix: address native installer review fin..." | Re-trigger Greptile