Companion to cloud-providers.md (the abstraction +
caveats) and create-and-checkpoints.md (the
docker equivalent). This doc walks the cloud create end-to-end: every step
that runs on the host, every step that runs inside the Daytona sandbox, how
.git and workspace files land inside the box, and what changes between the
first box you ever create and the next one. Source of truth:
packages/sandbox-cloud/src/cloud-provider.ts,
packages/sandbox-cloud/src/workspace-seed.ts,
packages/sandbox-cloud/src/agent-credentials.ts,
packages/sandbox-daytona/src/backend.ts.
Hetzner / Vercel / E2B follow the same flow. The steps below (mint-identity → reserve volumes → provision → workspace seed → credential seed → env-upload → ctl/dockerd/VNC launch → preview URLs → relay register → BoxRecord persist) are provider-agnostic; only the
provisionstep differs (Daytona snapshot vs. Hetzner snapshot vs. Vercel snapshot vs. E2B custom template baked from a Dockerfile viaTemplate.build()).agentbox prepare --provider e2bis the one E2B-specific prerequisite, andbox.imageE2bpins the resulting template id — seee2b_backlog.md. ThelaunchDockerdstep is skipped for Vercel and E2B (Firecracker microVMs can't run nested containers).
Routed through daytonaProvider → createCloudProvider(daytonaBackend)
(packages/sandbox-cloud/src/cloud-provider.ts:142):
- Mint identity —
id,name, branchagentbox/<name>, and two per-box bearer tokens (relayTokenfor the in-box agent,bridgeTokenfor the host poller). Bring up the host relay (ensureRelay). - Reserve agent credential volumes —
ensureAgentVolumesForCloudcallsbackend.ensureVolume()foragentbox-claude-config,agentbox-codex-config,agentbox-opencode-config. These are org-scoped in Daytona, so they exist once per org and get reused by every future box. - Provision the sandbox —
backend.provision({ image | snapshot, volumes, env, resources }).imageis either:- a published snapshot name (
agentbox prepare --provider daytonabakes the Dockerfile.box once, ~7 min, and lets every future create skip the build → seconds), or agentbox/box:dev(FALLBACK_IMAGE) → translated byresolveImage()toImage.fromDockerfile(...), which uploads the build context and triggers Daytona's cold build (the slow path).
- a published snapshot name (
- Seed
/workspace—seedCloudWorkspace(packages/sandbox-cloud/src/workspace-seed.ts). Skipped entirely if you booted from a checkpoint snapshot — the snapshot already carries/workspace. - Seed credentials volume if fresh —
seedAgentVolumesIfFreshchecks the sharedagentbox-credentialsvolume's per-agent subpath for.agentbox-seeded-at; if missing, tar + upload only the auth-token files (.credentials.jsonfor claude,auth.jsonfor codex/opencode) and drop the marker. Tiny payload (~KBs), seconds to extract. The bulk static config (plugins / skills / marketplaces / settings) is not shipped here — it's baked into the snapshot atprepare --provider daytonatime (see Snapshot bake). - Upload env/config files (
uploadEnvFiles) — the.env/secrets.toml/ etc. the setup wizard collected. - Run the in-box bootstrap (
kickCloudBootstrap→ onebackend.execofagentbox-ctl bootstrap) — a single idempotent self-configure that launches dockerd (best-effort), the ctl daemon, and the VNC stack (best-effort), each only if not already live, and (on the plane / cloud-IDE path) clones/workspacefrom a leased token first. This replaced the three separate host-driven launches and is re-run verbatim on resume — seein-box-supervisor.md. - Mint preview URLs — webproxy (8080), per-
expose.portservice URLs fromagentbox.yaml, and the bridge URL on 8788 so the host'sCloudBoxPollercan reach the in-box relay. - Register with host relay (
registerBoxWithRelay) → spawns theCloudBoxPollerfor this box. - Persist
BoxRecord(recordBox). On any failure:backend.destroy(handle)to avoid a paid orphan.
All implemented in packages/sandbox-cloud/src/workspace-seed.ts. For a git
workspace, per repo (root + any 1st-level nested repos):
- On host:
git stash create→ SHA of a one-off commit holding staged + tracked-modified changes (without disturbing your worktree). - Park that SHA under
refs/agentbox-carryover/stash(a temp host-side ref) so the clone can fetch it. git clone --no-checkout [--depth=N] file://<hostRepo> <stage>/clone— a shallow clone (default cap 200; adaptive: redo at 100 if the resulting tar exceeds 20 MB).--no-checkoutskips materializing the host-side working tree (we'd just discard it); the in-boxgit checkoutdoes that later. Cap controlled bybox.bundleDepth(--bundle-depth <n>flag, oragentbox config set box.bundleDepth N): unset → adaptive;N > 0→ fixed shallow depth;0→ no--depth(full history). Cloud-only — docker bind-mounts.git/. Note:git bundle createhas no--depthflag in any git version (--depthonly exists on clone/fetch). The shallow-clone-then-tar dance is the portable way to cap commit count.git -C <stage>/clone fetch [--depth=N] file://<hostRepo> +refs/agentbox-carryover/stash:refs/remotes/origin/agentbox-carryover/stash— pulls the carryover ref into the shallow clone.tar -C <stage>/clone -czf <stage>/workspace.tar.gz .— tars.git/(the only thing in the clone dir after--no-checkout).git ls-files --others --exclude-standard -z+tar -czf untracked.tar.gz→ captures untracked-not-ignored files (stash create doesn't).- Delete the temp host ref.
backend.uploadFileshipsworkspace.tar.gz(anduntracked.tar.gzif non-empty) to/tmp/in the sandbox.- Inside the sandbox (run as one
bash -cscript):cd /tmp(avoid stale-cwd FD when we wipe/workspace)sudo rm -rf /workspace && mkdir -p /workspace && chown ...tar -C /workspace -xzf /tmp/agentbox-workspace.tar.gz— this is how.gitlands in the box (extracted from the shallow clone's.git/)git remote set-url origin <host's origin>— repoint from thefile://placeholder to the real upstream sogit pushlater works (it gets tunneled back through the host relay)git checkout -B agentbox/<box-name>— materializes the working tree from HEAD (the clone was--no-checkout)git stash apply refs/remotes/origin/agentbox-carryover/stash(best-effort; soft-fails on shallow-clone merge conflicts)tar -xzf /tmp/agentbox-carryover-untracked.tar.gzinto/workspace- clean up tars
If the host workspace isn't a git repo, seedFromTar just tar -czf .
the whole dir, uploads, extracts. No clone, no branch.
A plain git clone populates only commits + pointer files, never
.git/lfs/objects. Cloud boxes also have no host credentials and (hetzner)
locked egress, so an in-box smudge of a missing object would hit the upstream
LFS endpoint unauthenticated and fail — leaving broken pointers (or aborting
the seed under set -e). Docker sidesteps this via its bind-mounted, shared
.git/lfs; cloud has no bind mount, so the objects are seeded explicitly:
- Between the shallow clone (step 3) and the tar (step 5),
seedCloneLfsObjects(workspace-seed.ts) probesgit lfs ls-files, best-effortgit lfs fetch origin <ref>(host holds the creds) to warm the host cache, then copies only the checkout ref's object blobs — content-addressed.git/lfs/objects/aa/bb/<oid>— into the clone. They ride the existingworkspace.tar.gz, so the in-boxgit checkoutsmudges real content with zero box network/creds. Bounded to the working set (not the whole.git/lfscache, which can be GBs) and fully best-effort: a missing oid is left as a pointer, never failing the seed. - The checkpoint-restore delta path (
tryReseedRepoDelta) ships only the oids the delta introduces (target \ checkpointTip) as a smallagentbox-delta-lfs.tar.gz, extracted into the box's.gitbefore the reset. - Each cloud base image installs
git-lfs+git lfs install --system(the provider install scripts; daytona inherits it fromDockerfile.box) so the filter is registered — without it the checkout silently skips smudge, since cloud boxes have no bind-mounted~/.gitconfig.
Out of scope (today): push-back of box-created LFS objects and lazy on-demand
git lfs pull of objects beyond the seeded working set — both need a relay
LFS transport. For docker, push-back already works as an emergent property of
the shared .git/lfs + host-side git push.
| First time | Subsequent boxes | |
|---|---|---|
| Box image build | ~7 min Dockerfile.box build on Daytona | Reuses the snapshot (built once or via agentbox prepare --provider daytona) — seconds |
| Agent static config (plugins/skills/marketplaces/settings) | Already baked into the snapshot — no per-create work | Same — baked once at prepare --provider daytona |
Agent credentials volume (.credentials.json / auth.json) |
Tar + upload + extract (~KBs, seconds) | Marker present → skipped; the volume mount carries the tokens forward |
| Workspace bundle | Always built + uploaded + cloned (per box — each gets a fresh /workspace on a fresh branch) |
Same — every box reseeds |
| Per-agent SSH keys / Daytona auth | agentbox daytona login prompts and writes ~/.agentbox/secrets.env |
Read from secrets.env silently |
| Host relay | ensureRelay boots the relay daemon |
Already running — no-op |
The "cold once, warm forever" optimizations are the published snapshot
(which now bundles the Dockerfile build and all agent static config) and the
credentials volume (which keeps the token files alive across boxes so
re-auth doesn't need a snapshot re-publish). Workspace bundle is a per-box
cost by design — each box needs its own isolated /workspace on its own
branch.
agentbox prepare --provider daytona (packages/sandbox-daytona/src/prepare.ts)
calls the documented Daytona snapshot API — no sandbox is provisioned. The
whole build + register happens server-side in one operation:
-
Host-side: stage filtered tarballs of
~/.claude(minus.credentials.json),~/.codex(minusauth.json), and opencodedata/+config/(minusauth.json). Seestage{Claude,Codex,Opencode}StaticForUploadinpackages/sandbox-docker/src/host-stage.ts. -
Build an
Imagefluently:const image = Image.fromDockerfile(Dockerfile.box) .addLocalFile(claudeTar, '/tmp/agentbox-seed-claude.tar.gz') .addLocalFile(codexTar, '/tmp/agentbox-seed-codex.tar.gz') .addLocalFile(opencodeTar, '/tmp/agentbox-seed-opencode.tar.gz') .runCommands( 'mkdir -p /home/vscode/.claude /home/vscode/.codex /home/vscode/.local/share/opencode', 'tar -xzf /tmp/agentbox-seed-claude.tar.gz -C /home/vscode/.claude', 'tar -xzf /tmp/agentbox-seed-codex.tar.gz -C /home/vscode/.codex', 'tar -xzf /tmp/agentbox-seed-opencode.tar.gz -C /home/vscode/.local/share/opencode', 'chown -R vscode:vscode /home/vscode/.claude /home/vscode/.codex /home/vscode/.local', 'rm -f /tmp/agentbox-seed-*.tar.gz', );
-
daytona.snapshot.create({ name, image })— Daytona uploads the layered build context to object storage, builds the image, and registers the result as an org-scoped named snapshot. Returns when the snapshot isactive. -
The
prepareCLI command pinsbox.image: <name>into the project config so subsequentagentbox create --provider daytonaboots from it.
Replaces the old agentbox daytona publish-snapshot, which used the
broken sandbox._experimental_createSnapshot API
(POST /api/sandbox/<id>/snapshot now 404s on Daytona's side). See
https://www.daytona.io/docs/en/snapshots/ for the documented API.
The resulting snapshot carries plugins/skills/marketplaces/settings pre-
populated. Every subsequent agentbox create --provider daytona boots from
this snapshot and skips the static seed entirely.
Run agentbox prepare (no args) at any point to print the current inventory:
docker's agentbox/box:dev image, the three shared docker volumes, and on
the Daytona side all agentbox* snapshots (state / size / age / (pinned)
marker) and agentbox* volumes — handy for spotting orphaned legacy volumes
and confirming the snapshot pinned in the project config still exists.
The Dockerfile bakes three symlinks at the agent-expected credential paths:
~/.claude/.credentials.json -> /home/vscode/.agentbox-creds/claude/.credentials.json
~/.codex/auth.json -> /home/vscode/.agentbox-creds/codex/auth.json
~/.local/share/opencode/auth.json -> /home/vscode/.agentbox-creds/opencode/auth.json
At runtime, agentbox-credentials (a single per-org Daytona volume) is
mounted three times via subpath (claude/, codex/, opencode/) under
/home/vscode/.agentbox-creds/. The dangling symlinks resolve through to
the mounted credential files. agentbox daytona resync re-uploads into the
same volume after a host re-auth — no snapshot republish needed.
If you want to skip even the workspace seed, use
agentbox create --provider daytona --checkpoint <name>: the snapshot already
carries /workspace, and step 4 is skipped
(cloud-provider.ts:218).
agentbox prepare --provider X stamps the build-context fingerprint into
~/.agentbox/<provider>-prepared.json's base.contextSha256 — a SHA over
every file that gets baked into the base image / snapshot (the staged
agentbox-ctl bundle, the agent helpers, the dockerfile context, etc.).
On every agentbox create / agentbox claude, the CLI recomputes that
SHA from the same inputs (evaluateBaseFreshness in
apps/cli/src/checkpoint-lookup.ts) and compares. A mismatch means the
local install has drifted from the baked base — typically a CLI upgrade
that changed one of the baked files.
Checksum-only. The CLI version strings stored next to the fingerprint
are informational; they never influence the decision. A CLI patch that
doesn't change any baked file produces an identical checksum, so the
base stays fresh and no prompt fires.
What happens when a stale base is detected:
- TTY — the wizard merges a confirm into the existing "checkpoint is
stale, recreate?" path: "The <provider> base image is out of date — its
baked runtime no longer matches your current install. Recreate the base
and run Setup Wizard? (rebuilds the base — ~N min — then starts
fresh)". Yes runs
runPrepare(force: true)before the create, then proceeds with a fresh base + discarded checkpoint. No boots on the existing base (the checkpoint is kept as-is). -y/ non-TTY — logs a loud▲warning naming the provider and pointing atagentbox prepare --provider X --force, then proceeds on the existing base. Never auto-bakes an expensive snapshot in a scripted run.--snapshot <ref>(interactive) — no rebuild hijack; the explicit snapshot is a deliberate restore. The non-interactive warn still fires.- Docker — silent:
ensureImagealready self-heals on a mismatch by rebuildingagentbox/box:devinline before container start.
Cloud checkpoints work in three layers: a Daytona-native snapshot primitive, the cloud-provider capability that wraps it, and the CLI command that drives the workflow.
daytonaBackend.createSnapshot(handle, snapshotName) (packages/sandbox-daytona/src/backend.ts:470)
calls sb._experimental_createSnapshot(snapshotName). Daytona puts the
sandbox into the snapshotting state, freezes its filesystem (including any
warmed agent volumes + the seeded /workspace), and registers an
org-scoped named snapshot. 15-min timeout, no retry on ambiguous
failures — a 504 mid-snapshot could leave a half-built name a retry would
collide on.
The sandbox must be running to snapshot, so the CLI command resumes/starts
paused or stopped boxes first (apps/cli/src/commands/checkpoint.ts:320).
The peer deleteSnapshot(name) is idempotent (already-gone counts as
success).
Each user-facing checkpoint maps to two records:
A. Org-wide Daytona snapshot, named deterministically by cloudSnapshotName
(packages/sandbox-cloud/src/checkpoint.ts:61):
agentbox-ckpt-<hash(projectRoot)>_<mnemonic(basename)>-<userName>
The project-hash prefix prevents collisions across projects and across users
in the same Daytona org. The agentbox-ckpt- prefix makes orphans
recognisable in the dashboard.
B. Local host manifest at:
~/.agentbox/cloud-checkpoints/<backend>/<projectHash-mnemonic>/<name>/manifest.json
A thin JSON pointer from the user-facing project-scoped name (e.g. setup)
to the unique Daytona snapshot name + source box metadata. This is how
agentbox checkpoint ls works without round-tripping the cloud.
makeCloudCheckpoint(backend) (cloud-provider.ts:707) wires it up:
create(box, name)→backend.createSnapshot(...)thenwriteCloudCheckpointManifest(...).list(projectRoot)→ reads the manifest dir.remove(projectRoot, ref)→backend.deleteSnapshot(...)(best-effort) then unconditionally removes the local manifest, so a remote-only failure doesn't strand a dead pointer.
agentbox checkpoint create [--name X] [--set-default] for cloud boxes
routes to runCloudCheckpointCreate (apps/cli/src/commands/checkpoint.ts:308):
- Resolve
projectRoot; default name is<box-name>-<last6 of ts>. - Probe state → resume/start if paused/stopped (snapshot needs the sandbox running).
- Post a relay notice (
CHECKPOINT_NOTICE) so attachedclaude/codexsessions see a banner that the box is freezing. provider.checkpoint.create(box, name)→ snapshot + manifest.- If
--set-default, writebox.defaultCheckpointDaytona(or whateverdefaultCheckpointConfigKey(provider)returns) to project config — separate per-provider keys so docker creates in the same project don't pick up a snapshot they can't resolve. - Clear the relay notice in
finally.
Differences from the docker path:
- No
--merged— Daytona snapshots are flattened by construction (warned + ignored). - No
--replace— Daytona snapshot deletes are async on their side, so re-creation can race the delete; the workflow is explicitagentbox checkpoint rm <name>then re-create.
agentbox create --provider daytona --checkpoint <name> (or --checkpoint
set as the default via box.defaultCheckpointDaytona):
In cloud-provider.ts:171, before provision:
const found = await resolveCloudCheckpoint(req.projectRoot, backend.name, req.checkpointRef);
if (found) snapshotName = found.manifest.snapshotName;Then backend.provision({ snapshot: snapshotName, … }) instead of
{ image }. The Daytona SDK takes the CreateSandboxFromSnapshotParams
overload (backend.ts:193) — no Dockerfile build, no
onSnapshotCreateLogs, just rehydrate the named snapshot in seconds. The
workspace-seed step is skipped because the snapshot already contains
/workspace; reseeding would clobber whatever setup state you captured.
If you pass a checkpoint name that doesn't have a manifest for the cloud backend, it's logged and dropped (you might have a docker checkpoint with the same name; that's not our store) and create falls back to the base image.
An agent inside the box can call agentbox checkpoint create over the
relay; the host-side handler (packages/relay/src/host-actions.ts:117)
doesn't re-implement the snapshot — it just shells out to the host CLI's
checkpoint create <boxId>, which routes back through
provider.checkpoint.create. Same decoupling as the docker handler.
Requires AGENTBOX_CLI_ENTRY to be set in the relay env (it is, when
ensureRelay starts it).
AgentBox actually maintains two distinct snapshot tiers in Daytona, used for different purposes and reached through different mechanisms. They are not unified under one "snapshot" concept.
| Base / "image" snapshot | Project / "setup" snapshot | |
|---|---|---|
| What it captures | Just the Dockerfile.box runtime (Node, Playwright, Chromium, agent CLIs, ctl, VNC stack). No /workspace. |
Everything in the box at capture time, including /workspace (installed deps, generated files, dev DB seed, etc.) |
| Scope | Org-wide; shared across all projects | Org-wide registry but prefixed by project hash (agentbox-ckpt-<hash>_<mn>-<name>) so two projects can't collide |
| Created by | agentbox daytona prepare --provider daytona [--name X] (one-off, manual; rebuilds ~7 min) |
agentbox checkpoint create [--name X] [--set-default] (per-box, anytime) |
| Stored as | Daytona snapshot agentbox-box-prebuilt-<ts> (or whatever --name you pass) |
Daytona snapshot agentbox-ckpt-<projectHash>_<mn>-<name> + host manifest |
| Consumed via config | box.image: <name> (project or user config) |
box.defaultCheckpointDaytona: <name> (or per-box --checkpoint <name>) |
| Daytona SDK call | client.create({ image: "<name>", … }) |
client.create({ snapshot: "<name>", … }) |
| What still runs on create | Workspace seed (git bundle + clone), agent-volume seed if first time | Workspace re-seeds in OVERLAY mode (seedCloudWorkspace({ overlay: true })): keeps the snapshot's gitignored warm artifacts but swaps .git, moves onto a fresh agentbox/<box> branch at the host base ref, and applies the host's uncommitted/untracked carry-over; agent volumes still mounted |
Dockerfile.box --(prepare --provider daytona)--> base snapshot (one-off, org-wide)
│
▼ box.image
agentbox create --(provision + seed /workspace)--> fresh box
│
run setup wizard / installs / migrations
│
▼
checkpoint create --set-default --> project setup snapshot (per-project)
│
▼ box.defaultCheckpointDaytona
agentbox create --(provision, /workspace warm; overlay-reseed .git)--> warm box
- Without the base snapshot: every first-box-per-project pays the ~7-min Dockerfile build (Daytona's internal layer cache helps for unchanged build contexts, but there's no AgentBox-side guarantee).
- Without the project setup snapshot: every box re-runs whatever the
setup wizard did (
pnpm install,prisma generate, populating a dev DB, etc.). - With both: cold create ≈ seconds, and
/workspace's warm artifacts are already at the state you snapshotted. - A box from a project snapshot is NOT frozen on the source box's branch.
The snapshot's
/workspace/.gitis the source box's per-box branch; booting it verbatim would put every checkpoint-derived box on that same stale branch with the wrong files. So cloud create re-seeds in overlay mode (seedCloudWorkspace({ overlay: true })): it keeps the gitignored warm tree (node_modules, build caches) and moves the box onto a freshagentbox/<new-box>branch at the host's current base ref (git checkout -f -Bgit reset --hard, dropping the checkpoint's own tracked commits — matching docker), then replays the host's stash + untracked carry-over.
- Transport is incremental: since the box can't bind-mount the host
.git(docker's trick), it ships only the commits the checkpoint is MISSING (checkpointTip..hostTarget) as a git bundle fetched into the existing.git. If the box diverged / the tip is unknown / the bundle can't build, it falls back to a full shallow-clone.gitswap. --no-resync(resyncOnStart: false) re-branches to the host tip but skips the stash + untracked overlay (matches docker's gatedresyncWorkspaceFromHost).- Conflicts are box-wins + reported: a host uncommitted/untracked change
that collides with the box's restored tree is skipped (the box version
kept) and recorded; the create returns a
ResyncResultonCreatedBox.resyncso the CLI injects the same "conflicting host changes SKIPPED …agentbox-ctl reload" prompt into claude/codex/opencode that docker does. The cloud equivalent of docker'sregenerateRestoredWorktrees+resyncWorkspaceFromHost.
- Base:
agentbox-box-prebuilt-*(no project hash — they're meant to be shared). - Project:
agentbox-ckpt-<hash>_<mnemonic>-<name>(project hash baked in, so two projects in the same Daytona org can both have asetupcheckpoint without colliding).
Both prefixes are deliberately recognisable in the Daytona dashboard so you can hand-clean orphans if needed.
This is the one place the docker and daytona providers behave differently by default for new users, and it's worth knowing.
ensureImage(ref) (packages/sandbox-docker/src/image.ts:90) is called at
the top of every box-creating command (create, claude, codex, opencode). It
checks imageExists(ref) and, if missing, runs buildImage(...) against
the bundled Dockerfile.box. That builds + tags agentbox/box:dev in the
local Docker daemon. Subsequent creates short-circuit on imageExists and
reuse the cached image. The user is never told to do anything — first
create just takes longer.
On a fresh --provider daytona install with no box.image set:
req.imageis undefined → falls back toFALLBACK_IMAGE = 'agentbox/box:dev'(cloud-provider.ts:145).daytonaBackend.provision({ image: 'agentbox/box:dev', … })→resolveImage('agentbox/box:dev')(backend.ts:144) returnsImage.fromDockerfile(ctx.dockerfile).- Daytona builds the Dockerfile.box from scratch → ~7 min cold build, every time a fresh sandbox is provisioned without a base snapshot.
Daytona's control plane does its own internal layer caching keyed by build context, so repeated cold builds for the exact same Dockerfile in the same org tend to be faster than 7 min in practice — but there's no AgentBox-side guarantee, and any tweak to the build context invalidates it.
To opt into the base snapshot, two manual steps performed once per org:
agentbox daytona prepare --provider daytona --name agentbox-box # ~7 min, one-off
agentbox config set --user box.image agentbox-box # or --projectAfter that, every agentbox create --provider daytona provisions from the
named snapshot in seconds.
A few constraints that make auto-publish a worse default than docker's:
- Publishing provisions a real sandbox for the duration of the build →
costs sandbox slot + compute on the user's Daytona org. Doing it
implicitly on every
agentbox daytona login(or first create) would surprise users who only wanted to provision one box. - Snapshots are org-scoped, not user-scoped. The first person to publish picks the canonical name everyone in the org shares; that's a coordination choice, not a per-user default.
- There's no AgentBox-hosted "public" snapshot under an Anthropic-owned
Daytona org we could ship as a default
box.image. That's the natural next step but isn't built — would need an Anthropic-managed publishing pipeline + cross-org snapshot access in Daytona.
A worthwhile UX improvement (not yet built): after the first Dockerfile
build, hint agentbox prepare --provider daytona in the success log so users
discover it before paying the cold cost twice.