feat(hub): cloud bakes belong to the control box - #275
Conversation
Bake records only ever flowed PC -> control box, so a CLI update (which moves the build context and stales every cloud base at once) ended in a nag to spend minutes re-baking - even when the box already held a base for the exact context we would bake. `syncBakesWithControlBox` now pulls before it pushes, `self-update`'s refresh adopts whatever the box has, and the pull matches across the claudeInstall fold like the hub's own adoption does. Adopting also mirrors the `box.image<Provider>` pin a real bake writes, so an adopted daytona base is actually used. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
With cloud.viaHub on, a cloud box is built on the control box from ITS prepared state - so a bake run on the PC spends minutes producing a snapshot nothing will boot, and the two sides then disagree about what the base is. `agentbox prepare --provider <cloud>` now drives the hub's own bake API (POST /api/v1/providers/:id/prepare), streams its job log, and adopts the resulting record from custody, so one bake leaves both machines current. `--local` keeps it here; `--via-hub` fails rather than falling back. The status inventory also reports the control box's providers, since those are the ones a cloud create actually boots. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
A cloud create routes to the control box and is built from ITS baked bases, so this host's cloud bakes are never used - yet the local hub reported its own, and could insist "hetzner - needs bake" while `agentbox hetzner` created boxes fine. Settings and System now show the control box's state for cloud providers (docker and remote-docker stay local, their bases being local images), tagged "control box" with a link out to manage them there. Cloud creates from the local UI are disabled with a pointer, rather than failing on a provider this host would never use. An unreachable control box reads as unknown - never this host's state under a control-box label. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
Two things the live run surfaced. With cloud.viaHub=false a cloud box is built here again, so mirroring the control box would describe a machine that does no work for this user - the mirror now opts out with it. And an unreachable control box no longer claims "needs credentials", which is a specific assertion about a box we did not reach; the badge reads unknown. The adoption sweep also logged a "different build context" line per provider - several lines to say nothing happened. It is silent per provider now and the caller reports the outcome once. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
Covers the routing (prepare on the control box, --local/--via-hub), the two-way bake sync, and what agentbox.localhost shows when a control box is configured. Adds the stub-control-box recipe to hub-testing, since it exercises every PC-side path without a VPS. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 980e344. Configure here.
| * applies — the outcome is `share-failed`, never a false `match`. | ||
| */ | ||
| pushSucceeded: boolean; | ||
| /** |
There was a problem hiding this comment.
Npm-fold local bake not recognized
Medium Severity
Adoption now compares custody records with fold-tolerant matchClaudeInstallFingerprint while probing a native context hash, but localBakeBlocksAdoption still treats “already current” as strict string equality. A local base baked with box.claudeInstall=npm stores the folded fingerprint, so the check fails and tryAdoptPreparedBase can skip blocking and fall through to a full local re-bake even though the base already matches this build context.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 980e344. Configure here.
| const { providerName, effective, forceHub, forceLocal, hubApiAvailable } = input; | ||
| if (forceLocal) return { where: 'local' }; | ||
| if (forceHub) return { where: 'hub' }; | ||
| if (!isHubRoutableProvider(providerName)) return { where: 'local' }; |
There was a problem hiding this comment.
Docker prepare routes via hub
Medium Severity
resolvePrepareRouting returns hub for --via-hub before checking isHubRoutableProvider, so agentbox prepare --provider docker --via-hub can POST a docker bake to the control box. Docs and the same module’s comments say docker and remote-docker always bake locally because their bases are machine-local images, not portable cloud snapshots.
Reviewed by Cursor Bugbot for commit 980e344. Configure here.
An npm-mode machine stores the FOLDED fingerprint, so once the probe switched to the raw native hash, `localBakeBlocksAdoption`'s strict compare called a perfectly current local base a miss - a prepare that found nothing in custody then re-baked a base it already had, every time. It matches across the fold now, like the pull and the hub's hydration. And `--via-hub` short-circuited ahead of the provider check, so `prepare --provider docker --via-hub` could POST a docker bake to the control box - baking THAT machine's image and leaving this one untouched. Create gets away with the same ordering because runCreateViaHub rejects docker downstream; there is no second gate here, so the check moved up. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
remote-docker was lumped in with docker as "always bakes locally", but the two aren't alike. A docker base is an image on THIS machine; a remote-docker base is an image on a THIRD machine both sides reach, and "is this host baked?" is answered by asking that engine rather than by trusting a local file - so a bake driven from the control box is visible here immediately, with no record to sync back. Routing now uses `isHubBakeableProvider` (everything but plain docker) rather than the create-side `isHubRoutableProvider`, and drives the hub's existing POST /api/v1/hosts/:alias/bake. Gated on the control box having that alias in its own registry - it SSHes there as itself, so our alias list says nothing about whether it can - falling back here with a notice otherwise. Creates are unchanged: they still run from this machine, over your own ~/.ssh/config. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
`agentbox docker add` writes the alias registry locally and nothing syncs it, so a hub-routed bake falls back to the PC until the same alias is registered on the control box by hand. Documented rather than automated: copying the alias across would flip the routing gate to true without the control box's key being authorized on that host, replacing a working local bake with a failing remote one. The hub's POST /api/v1/hosts already probes before saving, so that - not a file copy - is the shape to build if this is ever automated; the SSH-key half can't be. Also notes that the local hub's Remote Docker list stays this machine's, since that is what its creates use. Claude-Session: https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99


With a control box configured, cloud boxes are created there and built from its baked bases. Three places still assumed otherwise.
1. Bake records only ever flowed PC → control box
shareBakesWithControlBoxpushed onhub setup/deploy/update; the only pull was buried insideagentbox prepare. So a CLI update — which moves the build context and stales every cloud base at once — ended in a nag to spend minutes re-baking, even when the box already held a base for the exact context we'd bake.It is
syncBakesWithControlBoxnow: it pulls before it pushes, andself-update's refresh adopts whatever the box has. Two correctness fixes came with it:!==while the hub's own adoption usesmatchClaudeInstallFingerprint— so it refused records the hub would accept. Both sides apply one rule now.box.image<Provider>pin a real bake writes. Without it an adopted daytona base is inert (daytona resolves its base from that key, not from prepared-state).2.
agentbox preparebaked cloud bases on the laptopMinutes spent producing a snapshot nothing boots, after which the two sides disagree about what the base is. A cloud bake now runs on the control box (
POST /api/v1/providers/:id/prepare), streams its job log into the same spinner, and the record is adopted back — one bake, both machines current.--localbakes here;--via-hubfails instead of falling back. Docker and remote-docker always bake locally (local images, not portable snapshots).--name/--location/--sizekeep the bake local, since the hub'sparseProviderPreparedoesn't carry them. A failed hub bake exits non-zero rather than silently re-baking here — it would fail here too.3.
agentbox.localhostreported the wrong machineThe local UI could insist "hetzner — needs bake" while
agentbox hetznercreated boxes fine. Settings and System now mirror the control box for cloud providers, taggedcontrol box, with links out to manage them there; docker and remote-docker keep showing this host. Cloud creates are disabled in the local dialog with a pointer, rather than failing on a provider this host would never use.An unreachable control box reads
unknown— never this host's state under a control-box label. That would not be a degraded answer, it would be a wrong one.Verification
Live, against a stub control box on :9911 (recipe added to
docs/hub-testing.md— it needs ~60 lines and no VPS):digitalocean-prepared.jsonexists yet the row read not-baked; no local vercel record yet it read baked. Local fingerprint metadata correctly suppressed on hub rows.done→ custody GET → fingerprint compare.unknown, docker unaffected.cloud.viaHub=false: everything reverts to local state,originabsent.Two bugs the live run surfaced, fixed in
eadb0285:cloud.viaHub=falsewasn't turning the mirror off, and an unreachable box claimed "needs credentials".One caveat. The live adoption ended
baked-not-adoptedbecause the record I served was genuinely from an older build context (confirmed separately:e2breadsstalelocally). That was the guard working, not a failure — but it means the write-on-match half is covered by the round-trip unit test, not by a live run. Computing a matching full fingerprint needs a real bake.pnpm typecheck,pnpm lint, and the suites pass (1679 CLI, 70 hub, 168 sandbox-cloud). New tests:route-prepare(9),provider-origin(5), fold-tolerant pull,adoptedclassification.Docs
deployed-hub.mdx(routing + mirror),cli.mdx+configuration.mdx(flags,cloud.viaHub),docs/cloud-providers.md§1.2.2,docs/hub-testing.md, CHANGELOG.https://claude.ai/code/session_012XDVE3ZwSmw35hnKjgay99
Note
Medium Risk
Touches control-plane custody sync, remote hub API calls, and provider listing across CLI and hub; failures are mostly best-effort, but wrong routing or mirror logic could mislead users about bake readiness or where to create boxes.
Overview
With a control box configured, cloud boxes are built from its baked bases — this PR aligns prepare, bake sync, and the local hub UI with that model.
agentbox preparefor cloud providers now defaults to baking on the control box (job API + SSE logs), then adopting the custody record locally.--local/--via-huboverride routing; docker and remote-docker always bake here.preparestatus also lists the control box’s cloud provider inventory when reachable.Bake sync is bidirectional:
shareBakesWithControlBoxbecomessyncBakesWithControlBox— it pulls matching records from the box before pushing, andself-updateruns the same adoption sweep. Adoption uses fold-tolerant fingerprint matching (like the hub), pinsbox.image<Provider>on adopt, and adds anadoptedstatus so stale-local warnings are suppressed when the pull already fixed the provider.The laptop hub mirrors the control box for cloud providers (
remote-hub+mergeRemoteProviders): Settings, System, and create showorigin: hubrows with links out; cloud creates are disabled locally; unreachable boxes showunknown, not this machine’s bake state. Hub create errors for hub-routable providers point at the control box.Reviewed by Cursor Bugbot for commit 980e344. Configure here.