Skip to content

feat(hub): cloud bakes belong to the control box - #275

Merged
madarco merged 8 commits into
nightlyfrom
feat/hub-authoritative-bakes
Jul 28, 2026
Merged

feat(hub): cloud bakes belong to the control box#275
madarco merged 8 commits into
nightlyfrom
feat/hub-authoritative-bakes

Conversation

@madarco

@madarco madarco commented Jul 28, 2026

Copy link
Copy Markdown
Owner

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

shareBakesWithControlBox pushed on hub setup/deploy/update; the only pull was buried inside agentbox 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 syncBakesWithControlBox now: it pulls before it pushes, and self-update's refresh adopts whatever the box has. Two correctness fixes came with it:

  • The pull compared with a strict !== while the hub's own adoption uses matchClaudeInstallFingerprint — so it refused records the hub would accept. Both sides apply one rule now.
  • Adopting now mirrors the 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 prepare baked cloud bases on the laptop

Minutes 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.

--local bakes here; --via-hub fails instead of falling back. Docker and remote-docker always bake locally (local images, not portable snapshots). --name/--location/--size keep the bake local, since the hub's parseProviderPrepare doesn't carry them. A failed hub bake exits non-zero rather than silently re-baking here — it would fail here too.

3. agentbox.localhost reported the wrong machine

The local UI could insist "hetzner — needs bake" while agentbox hetzner created boxes fine. Settings and System now mirror the control box for cloud providers, tagged control 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):

  • Mirror inverted the verdicts, which is the point: local digitalocean-prepared.json exists yet the row read not-baked; no local vercel record yet it read baked. Local fingerprint metadata correctly suppressed on hub rows.
  • Full bake round-trip: routing → POST → SSE log streaming → job poll to done → custody GET → fingerprint compare.
  • Unreachable path: every cloud row went unknown, docker unaffected.
  • cloud.viaHub=false: everything reverts to local state, origin absent.

Two bugs the live run surfaced, fixed in eadb0285: cloud.viaHub=false wasn't turning the mirror off, and an unreachable box claimed "needs credentials".

One caveat. The live adoption ended baked-not-adopted because the record I served was genuinely from an older build context (confirmed separately: e2b reads stale locally). 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, adopted classification.

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 prepare for cloud providers now defaults to baking on the control box (job API + SSE logs), then adopting the custody record locally. --local / --via-hub override routing; docker and remote-docker always bake here. prepare status also lists the control box’s cloud provider inventory when reachable.

Bake sync is bidirectional: shareBakesWithControlBox becomes syncBakesWithControlBox — it pulls matching records from the box before pushing, and self-update runs the same adoption sweep. Adoption uses fold-tolerant fingerprint matching (like the hub), pins box.image<Provider> on adopt, and adds an adopted status 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 show origin: hub rows with links out; cloud creates are disabled locally; unreachable boxes show unknown, 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.

madarco added 5 commits July 28, 2026 21:28
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
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentbox-web Ready Ready Preview, Comment Jul 28, 2026 9:43pm

Request Review

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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;
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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
@madarco
madarco merged commit 252d89a into nightly Jul 28, 2026
4 checks passed
@madarco
madarco deleted the feat/hub-authoritative-bakes branch July 28, 2026 21:53
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant