feat(hub): hub update and hub destroy, plus the running build in hub status#261
Conversation
… build A control box was a one-way trip: the only way to change its version was to deploy a whole new VPS, and the only way to get rid of one was to delete the server by hand and then remember to clear ~/.agentbox/control-plane, the agentbox-hub SSH block, and relay.controlPlaneUrl. `hub update` moves an existing box to a new build in place. `--channel nightly|stable` installs the newest published build; the default matches this CLI's exact version, which is what keeps shared bake fingerprints matching. `--ref` switches a box to building from source. Everything after the ssh hop is the same code a deploy runs (the new applyControlPlaneConfig), so an update re-ships the compose stack, regenerates .env and the Caddyfile, and re-pushes provider secrets — a newer hub can want new env keys or a different container port, and re-running the whole configuration is the only way an update stays equivalent to a redeploy. The data volume is untouched, so the store, logins and custody survive. It re-syncs the firewall's SSH source over the Hetzner API before connecting: only :22 is IP-locked (:80/:443 stay open so boxes can reach the hub), so a laptop that changed networks otherwise cannot ssh in at all — and the API path still works when ssh doesn't. `hub destroy` tears down the server + firewall and purges the local state. It first asks the hub what boxes it still owns and refuses without --force, because cloud boxes outlive the hub and its store is often their only record. Teardown needs no ssh, and never throws for an already-gone resource — a half-deleted box must not leave config pointing at nothing with no way to clear it. `unset-url --purge` now shares that purge, so it stops leaving a stale `Host agentbox-hub` block behind. `hub status` gains version/channel/drift. The relay already reported AGENTBOX_CLI_VERSION on /healthz; both hub images now export it, read from the installed package's manifest rather than the requested spec (a spec can be a dist-tag). The live version beats the deploy record, which only says what was last deployed — wrong after a failed update, and absent for a hub someone else set up.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Found live. Downgrading a control box to a build that cannot run with its config reported SUCCESS: the hub starts listening BEFORE it starts its create worker, so a version that boots and then throws serves healthz for a moment and dies, and the poll landed in that window. The box then restarted every 60s while the CLI said "Control box is healthy". applyControlPlaneConfig now checks `docker compose ps` after the healthz poll — a crash-looping container reports `restarting`, never `running` — and fails with the app logs attached. Also from the same run: `hub update --ref` against a package-mode box died with a bare "not a git repository", because cloud-init never cloned one there. It now says so and points at `hub deploy hetzner --ref` instead. And the destroy safety gate is split out as the pure `destroyGate` so its rules are testable without a live hub: a non-empty box list is a hard stop, but an unreachable hub is NOT — a deploy that never came up is the most common thing to want deleted, and refusing there would leave no way to clean up at all.
Live verification (Hetzner, one VPS)
Post-destroy, all six claims verified independently: server gone, firewall gone, Two bugs the live run caught (both fixed in 0a87bd4)1. A crash-looping update reported success. The first downgrade printed "Control box is healthy" while the container restarted every 60 seconds. The hub starts listening before it starts its create worker, so a build that boots and then throws serves healthz for a moment and dies — and the poll landed in that window. Now 2. A note on why that downgrade crashesNot a defect in this PR: CoverageThe destroy safety gate is split out as the pure |
|
bugbot run |
| }): Promise<ControlPlaneDestroyResult> { | ||
| if (readHetznerCredStatus().source === 'none') { | ||
| throw new Error('no HCLOUD_TOKEN configured — run `agentbox hetzner login` first'); | ||
| } |
There was a problem hiding this comment.
Destroy skips purge without token
Medium Severity
hub destroy calls runHetznerDestroy, which throws when no HCLOUD_TOKEN is configured. The handler never reaches purgeLocalControlPlaneState or clearing relay.controlPlaneUrl, so local state can stay pinned to a hub the user already removed in the console.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 0a87bd4. Configure here.
…at dist-tags Three fixes, from CI and the bugbot pass: - destroy built the Hetzner client eagerly, so a record with neither id — which deletes nothing — still threw "HCLOUD_TOKEN is empty". Only my machine had a token, so it passed locally and broke CI. The client is now lazy. - `hub update` printed "Control box is healthy" from a probe it never checked. The update already waits for a healthy hub, so a failing probe here is usually transient, but claiming health from a failed probe is the same false success the crash-loop guard was just added to prevent. - `--package nightly` records the literal dist-tag, and reading it as a version printed `version: nightly` and classified it `stable` (channelOfVersion only looks for a `-nightly.` suffix). A spec that isn't a concrete version now reports no version and no channel until the hub says what it installed.
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4ba6053. Configure here.


Why
A control box was a one-way trip. Changing its version meant deploying a whole new VPS; getting rid of one meant deleting the server by hand in the Hetzner console and then remembering to clear
~/.agentbox/control-plane, theagentbox-hubSSH block, andrelay.controlPlaneUrl. Andhub statuscouldn't say what version was actually running — which matters now that host↔hub version parity is what makes shared bake fingerprints match.The enabling refactor
deployControlPlaneToHetznersplit into provision +applyControlPlaneConfig. Everything from "read the compose" to the healthz poll is exactly what an update has to redo — a newer hub can want new.envkeys, a different container port, or new compose assets — sohub updatere-runs the deploy's own configuration half rather than growing a second implementation of it. No behaviour change for deploy.agentbox hub updateMoves the existing VPS to a new build in place; the data volume (store, logins, custody, box SSH keys) is untouched.
Defaulting to this CLI's version is deliberate — that parity is what keeps bake records shared.
It re-syncs the firewall's SSH source over the Hetzner API before it connects. Only
:22is IP-locked (:80/:443stay open so boxes reach the hub from anywhere), so a laptop that changed networks can't ssh in at all — and the API path still works when ssh doesn't, so this repairs a box that has already locked you out. Regenerating the env also refreshesAGENTBOX_HUB_ADMIN_CIDR, so direct PC→box SSH keeps working after your IP moves.agentbox hub destroyPrints exactly what it will delete — VPS, firewall,
~/.agentbox/control-plane, theagentbox-hubalias,relay.controlPlaneUrl— and confirms first.--keep-credentialskeepscontrol-plane.envfor a later redeploy.Two deliberate behaviours:
--force. Cloud boxes outlive the hub and its store is often their only record. An unreachable hub says so rather than blocking, since a broken deploy is exactly what you'd want to delete.Teardown needs no ssh at all, so it works from a different network than the one that deployed the box.
unset-url --purgenow shares the same purge helper, which also fixes it leaving a staleHost agentbox-hubblock behind.hub statusThe relay already reported
AGENTBOX_CLI_VERSIONon/healthz; both hub images now export it, read from the installed package's manifest rather than the requested spec (a spec can be a dist-tag likenightly). The live version beats the deploy record — the record only says what was last deployed, which is wrong after a failed update and absent for a hub someone else set up.channelissource (<ref>)for a build-from-source box, since the ref is the distinction that matters there. A drift nudge appears only when there's a live reading to compare, so a stale record can't nag forever.Verification
Unit: 8 tests for the status precedence (live beats record, fallback, source-ref channel, drift only when live) and 4 for destroy tolerance (already-gone server still deletes the firewall, ordering, never throws, no-op). Dockerfile drift guards assert the version comes from
package.jsonand not fromAGENTBOX_SPEC. Fullpnpm build/typecheck/lint/ suites green, plus the web build.Live on Hetzner — deploy → status →
update --package <older>→updateback →update --ref→destroy(with and without a registered box). Results in a follow-up comment.Note
Medium Risk
Changes touch production control-plane provisioning, firewall rules, and destructive teardown; mistakes could orphan cloud boxes or leave broken local config, though destroy gating and non-throwing partial teardown reduce that risk.
Overview
Closes the Hetzner control box lifecycle:
agentbox hub updateupgrades the existing VPS in place (data volume kept) via the same configuration path as deploy, andagentbox hub destroyremoves server, firewall, local deploy state, SSH alias, andrelay.controlPlaneUrl, with guards against orphaning registered boxes unless--force.The enabling change in
@agentbox/sandbox-hetznerextractsapplyControlPlaneConfigfrom deploy so update re-ships compose, env, Caddy, secrets, anddocker compose up. Updates can re-sync firewall SSH to the current egress IP over the API before SSH, and post-health checks usedocker compose psso a one-off healthy/healthzduring a crash loop is not treated as success.hub statusfor remote hubs now shows version (live/healthzover deploy record), channel, build line, and a drift nudge towardhub updateonly when a live version differs from the CLI. Hub Dockerfiles setAGENTBOX_CLI_VERSIONfrom the installedpackage.json, not the npm spec/dist-tag.unset-url --purgeusespurgeLocalControlPlaneStateso staleagentbox-hubSSH config is cleared. Docs and unit tests cover destroy gating, status precedence, and destroy tolerance for already-deleted cloud resources.Reviewed by Cursor Bugbot for commit 4ba6053. Configure here.