Skip to content

fix: make the in-app updater edition-aware - #370

Merged
KrasimirKralev merged 3 commits into
betafrom
fix/updater-edition-aware
Aug 11, 2026
Merged

fix: make the in-app updater edition-aware#370
KrasimirKralev merged 3 commits into
betafrom
fix/updater-edition-aware

Conversation

@KrasimirKralev

@KrasimirKralev KrasimirKralev commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The problem

An in-app update on a Hermes device ends red on gateway_verify. That step waits for the OpenClaw gateway on port 18789, which this edition deliberately masks and closes, so it can only ever throw β€” and because it is failFast, the run stops there.

The clearest way to see that this step was never meant to run on this SKU: the run contradicts itself. post_update calls step_update_smoke, which on hermes fails the install if anything is listening on 18789. The very next step demands that something is.

The user-visible damage is worse than a red row. runUpdate only persists update_completed when nothing failed, so a Hermes box never records a finished update. That flag gates SetupWizard.tsx's resume step and the /update/versions route β€” the device keeps presenting a completed update as unfinished.

What was already fine

Worth stating, because it narrowed the change considerably:

  • step_openclaw_install, step_openclaw_patch and step_gateway_setup already early-return on hermes inside install.sh. They were never failures β€” they cost time and printed skip lines, but reported completed.
  • Hermes provisioning was already re-applied on every update, from inside step_post_update. What was missing was not the work but its visibility: it was wrapped in || echo "Warning: … (non-fatal)", so a device that failed to provision its entire edition still finished the update green.

The change

Steps carry an applicability predicate, and inapplicable ones are dropped from the list. A dropped step never renders, so it reads as absent rather than as a failure β€” no UI, StepStatus or translation change was needed, since UpdateStep.tsx renders state.steps verbatim and Meter derives its bounds from the array.

step openclaw dual hermes
openclaw_install / openclaw_patch run run dropped (openclawIsAbsent())
gateway_setup / gateway_verify run run dropped (gatewayIsAbsent())
hermes_edition (new) dropped run run (hasHermesHarness())

Each step names the helper matching its own reason for being skipped rather than one blanket edition check. The two predicates are identical today; keeping them distinct means they can diverge without the filter quietly going wrong. dual ships both harnesses and is unchanged.

Provisioning becomes its own visible step. hermes_edition runs after post_update (so step_systemd_services has already refreshed the unit files it reinstalls) and is no longer called from inside step_post_update, so it runs exactly once and can fail honestly. Fresh installs are unaffected β€” they call step_hermes_edition directly.

The scoped OpenClaw-only update refuses on an edition without OpenClaw instead of no-opping two steps and then failing on the same absent gateway. /setup-api/update/openclaw was reachable and unguarded.

setup-hermes-edition.sh passes HOME explicitly to every runuser call. All three scripts it invokes resolve state from ${HOME:-/home/clawbox} and it runs as root. runuser does reset HOME today β€” verified on hardware, util-linux 2.37.2 β€” but relying on that would let the dashboard-auth script read the wrong config, find no credentials to verify, and mint a password the proxy does not hold.

Idempotency

hermes_edition now runs on every update, so re-running has to be free. Verified on a real Hermes box, in a throwaway root with overridden paths:

second-run said: [hermes-dash-auth] already configured (stored hash verifies the stored password) β€” skipping
password UNCHANGED: PASS
hash UNCHANGED: PASS
dashboard blocks in config: 1

creds_are_consistent() re-derives the scrypt hash from the stored plaintext and exits before any minting, so a working box is never rotated. This is now pinned by a behavioural test β€” run the script twice, assert password and hash unchanged β€” rather than a source-text scan.

A guard that looks dead and is not

Once applies() drops gateway_setup on hermes, the is_hermes_edition guard inside step_gateway_setup reads as dead code. It is not. step_post_update has no predicate, runs on every SKU, and calls step_gateway_setup itself β€” deleting that guard would reinstall and enable an OpenClaw gateway on a Hermes box halfway through its own update, reintroducing this bug one layer down. install.sh --step gateway_setup by hand is the same hole. There is now a comment at the guard and tests pinning both ends of the argument.

Testing

  • Unit: edition-aware step lists for all three SKUs, the OpenClaw-only refusal, a full hermes update reaching completed with update_completed persisted, the no-re-mint behaviour, and a cross-layer pin that hasHermesHarness() and has_hermes_harness() agree.
  • e2e-install's 90-upgrade-main-to-beta.spec.ts is unaffected: it asserts every step in state.steps completed without naming them, and the container resolves as openclaw, where the list is unchanged.

A Hermes variant of the e2e-install upgrade spec is not feasible today, and this is why rather than an excuse. CLAWBOX_EDITION is not plumbed through docker-compose.test.yml or entrypoint.sh. step_hermes_install has no test-mode guard and shells out to an unpinned third-party installer, degrading to a warning on failure β€” leaving no hermes binary, a crash-looping dashboard unit, and step_validate_services returning 1. That aborts install.sh under set -e, so .needs-install is never removed, the healthcheck never passes, and global-setup.ts burns its 40-minute timeout and fails every spec in the suite. The harness is also workers: 1 against one hardcoded container, and the edition is baked at install time with the gateway deleted and masked, so the two editions cannot coexist. Doing it properly needs a second compose identity and a decision about what "Hermes installed" means in CI.

Pre-flight before the manual upgrade test

The whole fix keys off readEdition(). Run this on the older Hermes box first:

cat /etc/clawbox/edition.env                     # expect CLAWBOX_EDITION=hermes
systemctl show clawbox-setup -p Environment      # expect CLAWBOX_EDITION=hermes
systemctl is-enabled clawbox-gateway.service     # expect: masked

If none of those carry the SKU, readEdition() falls back to openclaw, the box runs the OpenClaw steps, and it fails at gateway_verify exactly as before β€” making a correct fix look broken. Confirm the edition resolves before drawing any conclusion from the run.

Also set the update target, since a box with no .update-branch resolves via its current branch's upstream and can silently aim at main:

echo beta | sudo tee /home/clawbox/clawbox/.update-branch

Expected on hermes: bootstrap_updater β†’ apt_update β†’ nvidia_jetpack β†’ performance_mode β†’ chromium_install β†’ vnc_install β†’ restart β†’ post_update β†’ hermes_edition, ending completed, with no OpenClaw or gateway rows shown at all.

Summary by CodeRabbit

  • New Features

    • Updates now adapt automatically to the device edition, running only applicable setup steps.
    • Hermes provisioning is handled as a distinct update step with independent status reporting.
    • Full installations support Hermes edition setup.
  • Bug Fixes

    • Prevented gateway and OpenClaw setup from running on unsupported editions.
    • Preserved existing dashboard authentication credentials when setup is rerun.
    • Fixed credential lookup consistency during Hermes setup.

An update on a Hermes device ended red on `gateway_verify`. That step waits
for the OpenClaw gateway on port 18789, which this edition deliberately masks
and closes, so it could only ever throw β€” and being `failFast`, it stopped the
run there.

The run contradicted itself: `post_update`'s smoke test FAILS the install if
anything is listening on 18789, and the very next step demanded that something
was.

The visible damage was worse than a red row. `runUpdate` only persists
`update_completed` when nothing failed, so a Hermes box never recorded a
finished update β€” which gates the setup wizard's resume step and the versions
route, leaving the device presenting a completed update as unfinished.

Steps now carry an `applies()` predicate and inapplicable ones are dropped from
the list entirely, so they render as absent rather than as failures:

  - openclaw_install / openclaw_patch  β€” no `openclaw` binary on hermes
  - gateway_setup / gateway_verify     β€” no gateway unit, port closed

Each names the helper matching its own reason (binary vs gateway) rather than
one blanket edition check, so the two can diverge later without the filter
quietly going wrong. `dual` ships both harnesses and is unchanged.

Hermes provisioning becomes its own visible step. It already ran on every
update β€” inside `step_post_update`, wrapped in `|| echo "(non-fatal)"` β€” so a
device that failed to provision its entire edition still finished green with
the failure buried in the journal. Same work, moved to where it can be seen and
can fail honestly; `step_post_update` no longer calls it, so it runs once.
Fresh installs are unaffected β€” they call `step_hermes_edition` directly.

The scoped OpenClaw-only update now refuses on an edition without OpenClaw
instead of no-opping two steps and then failing on the same absent gateway.

`setup-hermes-edition.sh` passes HOME explicitly to every `runuser` call. All
three scripts resolve state from `${HOME:-/home/clawbox}`; this runs as root,
and relying on runuser to reset HOME would have let the dashboard-auth script
read the wrong config, find no credentials to verify, and mint a password the
proxy does not hold. Verified on hardware that the existing early-exit does
hold β€” the stored hash verifies the stored password, so re-running mints
nothing β€” and this makes that independent of runuser's semantics.

The updater tests now pin the SKU. They are routinely run on a device, where
/etc/clawbox/edition.env exists and is authoritative, so without pinning the
suite's result would depend on which box it ran on.
Review pass over the previous commit. Behaviour is unchanged.

The one real defect: the updater tests set CLAWBOX_EDITION_FILE themselves and
deleted it in afterEach. vitest.config.ts already points that variable at a
path that cannot exist, precisely so the suite stays hermetic on a device β€” so
the delete was removing a suite-wide guarantee for every later file in the
worker. Only the edition VALUE is pinned now.

Test consolidation:
  - `loadUpdater(edition?)` replaces four hand-rolled resetModules/env/import
    preambles, matching loadHarness() in harness-edition.test.ts.
  - Two assertions that the exact-list test already subsumed are folded into it.
  - Dropped a DISPATCH_STEPS check that duplicates install-edition-lock.test.ts.
  - Replaced a source-text scan of setup-hermes-dashboard-auth.sh with a
    behavioural test in the file that owns that script: run it twice, assert the
    password and hash are untouched. Verified against the real script on device.

Added the cross-layer pin that edition-source.ts's docblock claims but nothing
enforced: `hasHermesHarness()` and install.sh's `has_hermes_harness()` must
agree on which SKUs run the Hermes harness, or a device either skips its own
provisioning or dispatches a step that immediately returns.

`applicableSteps()` drops a parameter that only ever took one value, and
`createInitialState()` loses its default so the list is passed explicitly at
every call site β€” the invariant that the same array feeds both `state.steps`
and the runner is easier to see when nothing resolves it implicitly.

Trimmed comments that restated the code or re-told the commit message.
Now that the updater filters `gateway_setup` out on hermes, the
`is_hermes_edition` guard inside step_gateway_setup reads as dead code and
invites deletion. It is not dead: step_post_update has no `applies()` predicate,
runs on every SKU, and calls step_gateway_setup itself β€” so removing that guard
would reinstall and enable an OpenClaw gateway on a Hermes box halfway through
its own update, reintroducing this PR's bug one layer down. `install.sh --step
gateway_setup` run by hand is the same hole.

Comment at the guard, plus tests pinning both ends of the argument: post_update
still calls gateway_setup, and gateway_setup (and the two openclaw steps) still
refuse on hermes.
@KrasimirKralev
KrasimirKralev requested a review from a team as a code owner August 11, 2026 19:03
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d373ef65-4105-4385-a88e-9860445be534

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 06fbce3 and fe3d9c8.

πŸ“’ Files selected for processing (8)
  • install.sh
  • scripts/setup-hermes-edition.sh
  • src/lib/edition-source.ts
  • src/lib/openclaw-config.ts
  • src/lib/updater.ts
  • src/tests/unit/hermes-dashboard-auth-yaml.test.ts
  • src/tests/unit/install-hermes-edition-step.test.ts
  • src/tests/unit/updater.test.ts

πŸ“ Walkthrough

Walkthrough

Changes

Edition-aware provisioning

Layer / File(s) Summary
Edition contracts and Hermes setup
src/lib/edition-source.ts, src/lib/openclaw-config.ts, install.sh, scripts/setup-hermes-edition.sh, src/tests/unit/install-hermes-edition-step.test.ts, src/tests/unit/hermes-dashboard-auth-yaml.test.ts
Hermes detection and gateway availability are exposed. Hermes setup subprocesses now use HOME="$CLAWBOX_HOME". Tests cover guards, dispatch, and repeated authentication setup.
Edition-filtered updater execution
src/lib/updater.ts, src/tests/unit/updater.test.ts
The updater filters unavailable steps, dispatches Hermes provisioning separately, reuses the filtered list for state and continuation, and rejects unsupported scoped OpenClaw updates. Edition-specific tests cover Hermes, OpenClaw, dual, and default installations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: area: gateway, area: install

Suggested reviewers: yalexx, georgik77

πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 clearly and concisely describes the primary change: making the in-app updater edition-aware.
Description check βœ… Passed The description explains the problem, implementation, testing, idempotency, guards, and known E2E limitation in sufficient detail.
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.
✨ Finishing Touches πŸ’‘ 1
πŸ“ Generate docstrings πŸ’‘
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/updater-edition-aware

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

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

@github-actions

Copy link
Copy Markdown

πŸ¦€ ClawReview

Scuttled over to say hello and get you oriented πŸ¦€

This PR fixes a bug where in-app updates on Hermes devices always ended in failure: the gateway_verify step waited for the OpenClaw gateway on port 18789, which Hermes deliberately masks β€” so it could only ever throw, and because it is failFast, the run stopped before update_completed was ever persisted. The fix adds an applies() predicate to each update step definition so edition-inapplicable steps are dropped from the list entirely rather than shown as completed no-ops. As part of the same change, Hermes reprovisioning (hermes_edition) becomes its own visible, failable step instead of being silently swallowed inside step_post_update, and HOME is now passed explicitly to every runuser call in setup-hermes-edition.sh to prevent credential re-minting against the wrong config path on devices where the script runs as root.

At a glance

  • πŸ”§ Fix Β· touches the in-app updater step list + Hermes edition provisioning + install.sh shell scripts
  • Base branch: beta Β· +129 source / +299 tests across 8 files
  • βœ… base beta matches the beta-first convention
  • βœ… conventional PR title
  • βœ… source changes come with test changes
  • ℹ️ touches security-sensitive paths (install.sh) β€” review with extra care

Good to know

  • 🟑 Directly modifies the auto-update flow that executes on every customer device β€” the step ordering, filtering logic, and post-restart resume path all changed.
  • ℹ️ hermes_edition now runs on every update (not just fresh installs), so idempotency is load-bearing; the new behavioral test in hermes-dashboard-auth-yaml.test.ts pins the no-re-mint guarantee.
  • ℹ️ install.sh gains a multi-line comment block explicitly warning future editors not to remove the gateway_setup edition guard β€” the test suite enforces the same contract from the TypeScript side.
  • ℹ️ gatewayIsAbsent() is newly exported from openclaw-config.ts and hasHermesHarness() is a new export from edition-source.ts β€” two new public symbols in shared lib files.

β€” ClawReview πŸ¦€. I set the scene; CodeRabbit reviews the code; you decide. Conventions: docs.

@github-actions github-actions Bot added area: install Auto-triage area area: gateway Auto-triage area labels Aug 11, 2026
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

CI Summary

βœ… Tests

  • Result: passed
  • View run
  • Coverage: statements 64.75%, branches 53.76%, functions 62.61%, lines 66.88%

βœ… E2E

βœ… E2E Install

@KrasimirKralev
KrasimirKralev merged commit 6d4862c into beta Aug 11, 2026
10 checks passed
@KrasimirKralev
KrasimirKralev deleted the fix/updater-edition-aware branch August 11, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: gateway Auto-triage area area: install Auto-triage area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant