Conversation
Records the verified driver decision: drizzle-orm 0.45.2 has no node:sqlite driver, so SQLite goes through drizzle's sqlite-proxy over node:sqlite (no native dep), and Postgres moves onto drizzle-orm/node-postgres. Claude-Session: https://claude.ai/code/session_01Xqzoz8sFavVVTEJ4z9WYJS
One drizzle schema (boxes/events/box_status/prompts/create_jobs) in both dialects, and two stores over it: - SqliteStore — node:sqlite (DatabaseSync) behind drizzle's sqlite-proxy driver, DB at ~/.agentbox/hub/store.db, WAL + busy_timeout, schema applied lazily on first open. drizzle ships no node:sqlite driver and its better-sqlite3 one would add a native dep, so the ~20-line proxy executor is the adapter. Guarded with a clear error on Node < 22.5. - PostgresStore — ported onto the same schema via drizzle-orm/node-postgres; every hand-written SQL query is gone (the idempotent CREATE TABLE DDL stays, since drizzle-kit migration folders don't survive tsup bundling). Both drivers stay lazy + external: relay's bin.cjs carries neither store, and the CLI bundle keeps pg and node:sqlite out of its load path. makeStore() gains a sqlite branch (sqlite: URL or a bare path). SqliteStore runs the existing store conformance suite (pure, no docker/network) plus an on-disk restart case; the Postgres fixture now truncates create_jobs too, so its queue case no longer passes only against a virgin database. Claude-Session: https://claude.ai/code/session_01Xqzoz8sFavVVTEJ4z9WYJS
The hub server now resolves the relay's store from the profile: an explicit RELAY_STORE_URL/POSTGRES_URL wins, the hetzner (control box) profile otherwise defaults to SQLite at ~/.agentbox/hub/store.db, and localhost keeps the in-memory store. A control box on a small VPS therefore needs no database container, and its registry/approvals/queue survive a restart. Claude-Session: https://claude.ai/code/session_01Xqzoz8sFavVVTEJ4z9WYJS
Records what shipped (createRequire for node:sqlite, verified bundle posture, optional Store.migrate, the pre-existing pg fixture bug), the verification matrix, and opens a Backlog section — chiefly that the daemon's loops and the hub backend still read in-memory state, which phase 3 must route through the Store. Also refreshes the Store seam paragraph in host-relay.md. Claude-Session: https://claude.ai/code/session_01Xqzoz8sFavVVTEJ4z9WYJS
A wholesale vi.mock factory undefines every other export of the module for the whole graph, so it breaks as soon as any transitive import needs one — which is what happened when the relay's new SqliteStore started reading STATE_DIR (restore-agent-sessions died at import with "No STATE_DIR export is defined on the @agentbox/config mock"). Spread importOriginal() and override only the function under test; portless-prompt had the same latent footgun. Claude-Session: https://claude.ai/code/session_01Xqzoz8sFavVVTEJ4z9WYJS
…box SSH keys) Phase 2 of the control-box plan. A path-and-bytes CustodyStore seam with a filesystem backend (FsCustodyStore at ~/.agentbox/hub/custody/, 0700 dirs / 0600 files, content-hash skip) exposed over a shared /admin/custody/* dispatcher mounted in both the hosted-plane handler and the relay daemon. The routes are admin-bearer-gated (not loopback-gated — a control box behind Caddy makes every request look loopback) and fail closed; values are never logged. The agent-credential set reuses AGENT_SYNC_SPECS (the same registry a cloud create seeds from) rather than a second file list. CLI on `agentbox control-plane`: credentials push, secrets push, custody pull/list; an opportunistic hash-skipped push rides `credentials propagate`. Tests: fs store + both handlers over in-process HTTP (relay), pure planner + fake-fetch client (cli). Full pnpm test / typecheck / lint green. Claude-Session: https://claude.ai/code/session_01TEBCAjM2JmmhztkhN1Svcc
Full-hub Hetzner deploy + resident create worker. Resolves the two phase-3 blockers (write-through durable store into the in-memory caches + boot hydration so loops/hub-backend see state; retention sweep for answered prompts + finished jobs) and details the six deploy/worker/firewall/enqueue items, files, tests, and the in-box docker smoke. Claude-Session: https://claude.ai/code/session_017WVUdi7NrV6dxFvezZPLV7
…lockers) Blocker A: WriteThroughStore mirrors a durable store's writes into the in-memory registry/events/statusStore the daemon loops + hub backend read synchronously, and hydrates them on boot. So a hetzner+SQLite hub shows agent status and its cloud-keepalive loop sees boxes. localhost/MemoryStore path is unchanged (the wrapper is only built when a durable store is injected). Blocker B: optional Store.prunePrompts/pruneCreateJobs (SQLite+Postgres, using expires_at/finishedAt) + a periodic startRetentionLoop in the daemon so an always-on control box doesn't accumulate answered prompts / finished jobs. Claude-Session: https://claude.ai/code/session_017WVUdi7NrV6dxFvezZPLV7
…ore/handler Extract the create-queue enqueue/status endpoints into a framework-agnostic handleRemoteBoxesRequest (mirrors phase 2's custody shared-dispatcher), mounted in BOTH the relay daemon (server.ts, what the control box runs) and the Vercel plane handler. Admin-bearer gated, fail-closed. The control box's full hub now serves POST/GET /remote/boxes, so the PC can enqueue against it. Claude-Session: https://claude.ai/code/session_017WVUdi7NrV6dxFvezZPLV7
…e creates - Promote makeControlPlaneCreateBox + CreateBoxDeps into @agentbox/relay so both the CLI worker command and the hub can build a CreateBoxFn. - apps/hub/lib/hub-worker.ts: resident in-process worker (gated on AGENTBOX_HUB_WORKER=on) draining /remote/boxes. Its CreateBoxFn seeds agent creds from the custody agents/ scope into host backups before provider.create, registers the box on this hub (control-plane topology), and mirrors minted hetzner/DO box SSH keys into custody boxes/<id>/ssh/. A mockCreate seam (AGENTBOX_HUB_WORKER_MOCK=1) drives the queue offline for the in-box smoke. - Dual-IP firewall: thread extraInboundCidrs through CloudProvisionRequest; the hetzner backend appends the admin PC egress CIDR to the box firewall sources for control-plane-topology creates (unless inbound is open). Fail-loud egress detection unchanged. Claude-Session: https://claude.ai/code/session_017WVUdi7NrV6dxFvezZPLV7
New --via-hub / --url flags: instead of building locally, resolve the control plane target, POST the create job to /remote/boxes (repo origin + provider + branch/name), then stream GET /remote/boxes/:id until done/failed. Cloud providers only; needs a configured control plane + admin token. The resident hub worker provisions the box VPS-side. Claude-Session: https://claude.ai/code/session_017WVUdi7NrV6dxFvezZPLV7
…command - Dockerfile: build + run the standalone server.ts (relay + Next UI + worker), add git/openssh-client for the worker's clone + hetzner ssh. CMD runs the standalone bundle; server.ts self-migrates auth/store on boot. - docker-compose.yml: app-only, hetzner profile, SQLite, worker on, persistent /root/.agentbox host-bind volume (store.db/auth.db/custody/secrets.env). Drop the Postgres container. - control-plane-deploy.ts: scp the provider-secret subset (HCLOUD/E2B/DAYTONA) from the host secrets.env into the data volume; write AGENTBOX_HUB_DATA_DIR / PUBLIC_URL / ADMIN_CIDR (== deploying PC egress) into the VPS .env. - New `agentbox control-plane deploy hetzner [--ref] [--repo]`: re-deploys the full hub reusing the existing App creds/env (no manifest flow), pinning the VPS clone to --ref for feature-branch live verify. Claude-Session: https://claude.ai/code/session_017WVUdi7NrV6dxFvezZPLV7
…l box - Record phase 3 as code-done in the plan (checkboxes, "what actually shipped", smoke results), mark blockers A/B addressed in the Backlog, add phase-3 follow-ups, and mark the live-hetzner verify PENDING-HOST. - Update apps/web control-plane.mdx: the Hetzner deploy is now the full "control box" (standalone hub + resident worker + SQLite, no Postgres); document `control-plane deploy hetzner [--ref]` and `create --via-hub`. Claude-Session: https://claude.ai/code/session_017WVUdi7NrV6dxFvezZPLV7
The standalone esbuild bundle inlines the @agentbox/sandbox-* packages but
keeps their cloud SDKs (e2b, @daytona/sdk, @vercel/sandbox) external; pnpm's
strict layout has no /repo/node_modules/<sdk>, so the resident worker's
provider import died on the control box ('Cannot find package e2b'). Declare
the SDKs as apps/hub dependencies so they resolve via apps/hub/node_modules —
the same path pg/better-auth/next already use. Found in the phase 3 live
hetzner verify (the in-box mock smoke cannot reach a real provider import).
Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
…lers The control box's resident worker registers a freshly created box via the public plane URL, and the PC will do the same in phase 4 — but server.ts kept /admin/* strictly loopback-only, so registration 403'd and the box was left with an unknown token (push/status dead). Allow a timing-safe admin-bearer match as the non-loopback alternative, same posture as core/handler.ts and the custody//remote dispatchers. Fail-closed: a laptop relay configures no admin token, so its gate stays loopback-only. Found live on the deployed control box (phase 3 verify: 'unknown box token' on agentbox-ctl git push; hub logs: 'plane register-box → 403'). Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
…llow) A control-plane box's git.lease-token is auto-allowed only for the box's sanctioned agentbox/* branch, which the gate reads from the REGISTRATION's worktrees — but registerBoxWithPlane never sent any, so every hub-created box's push blocked on a human approval (in-box push hung until timeout). Register the /workspace worktree (branch = sanctionedBranch = agentbox/<name>) at both the create and resume call sites. hostMainRepo is the create-time seed checkout; on the hub worker that temp clone is deleted after create, so host-side git RPCs against it stay dead — recorded in the plan backlog (the lease path, which control-plane boxes actually use, never touches it). Found live on the deployed control box (phase 3 verify). Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
leaseAndPush spawned bare 'git push <remote> <branch>', which on a baked box resolves to the agentbox git shim — and the shim refuses positional remote/branch for push (by design), so every control-plane box's leased push died with 'positional origin not allowed'. Use runRealGit (the direct-mode helper that pins /usr/bin/git / AGENTBOX_REAL_GIT_PATH) for the set-url/push/ restore sequence, mirroring direct mode. The June live validation ran on a bare cloud-init VPS with no shim on PATH, which is why this never surfaced. Found live on the deployed control box (phase 3 verify, e2b box). Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
… git-rpc guard Promote listStatuses() to the Store interface (MemoryStore via a new BoxStatusStore.list(), RemoteStore via RPC, WriteThroughStore forwards; covered across every backend by the store-conformance suite). Mount /admin/store as a shared dispatcher (store-rpc-routes.ts) in both server.ts and core/handler.ts so a PC's RemoteStore works against the real control box, not only the Vercel plane. Carry BoxRegistration.sandboxId. Add DELETE /remote/boxes/:id — reap a box's control-box state (forget + deleteStatus + custody boxes/<sandboxId>/ cleanup). Reject host-side git.push/git.fetch against a worker-created box whose hostMainRepo is gone (hostRepoUnavailableReason) instead of a cryptic git -C failure. Expose custody on the relay handle for the hub reap. Claude-Session: https://claude.ai/code/session_01C5GMhU6hbgqYRqoZZCCoau
…sandboxId Centralize the per-box ssh-dir namespace rule in boxSshDirForProvider (sandbox-core), shared by the hub-worker mirror, the PC push-up, and hub pull — killing the phase-3 bug where hetzner's un-namespaced dir was read with a namespace and mirrored nothing. Carry sandboxId through plane-register (create + resume). A PC-created control-plane cloud box pushes its minted SSH key up to custody at create (pushBoxSshToCustody; no-op for e2b/vercel). Claude-Session: https://claude.ai/code/session_01C5GMhU6hbgqYRqoZZCCoau
mirrorBoxSshToCustody now uses boxSshDirForProvider and keys custody by sandboxId. hub-backend.destroy falls back to reaping a Store-only box (registration + status + custody) when it isn't in host state. HubState gains controlPlane (from relay.controlPlaneUrl; null on the control box itself) and the topbar links to it. Claude-Session: https://claude.ai/code/session_01C5GMhU6hbgqYRqoZZCCoau
New control-plane subcommands over the admin API: boxes list|rm, prompts list|answer, credentials pull, custody rm — plus `agentbox hub pull <box>` to download a hub-created box's SSH keys into ~/.agentbox/boxes/<sandboxId>/ssh so attach/cp/port-forward work. `agentbox relay status` gains a control-box section (URL + /healthz reachability). A ControlPlaneAdminClient wraps RemoteStore + prompt/reap fetches. Tests (temp HOME): hub-pull planner, admin client. Claude-Session: https://claude.ai/code/session_01C5GMhU6hbgqYRqoZZCCoau
Plan: phase-4 implementation plan + what-shipped + smoke commands, ticked items, extended Backlog. Public docs: control-plane.mdx "Operate through the control box" section (boxes/prompts/hub pull/credentials), hub.mdx hub pull, host-relay.md store/custody/phase-4 notes. Claude-Session: https://claude.ai/code/session_01C5GMhU6hbgqYRqoZZCCoau
…nt projects volume The hub queue (web-UI creates, queued agent runs, host-app launchers) spawns 'node $AGENTBOX_CLI_ENTRY …', but the control-box image never built apps/cli and set no entry — a web-UI create failed instantly with 'cannot spawn queue worker'. Build the CLI in the image and set AGENTBOX_CLI_ENTRY. Also mount a persistent /root/projects volume: the full-host hub keeps real project clones (the worker's per-job temp checkouts are deleted after create and make broken 'projects' in the UI — backlog). Found live on the deployed control box (final goal-scenario verify). Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
…tion) The compose ran the hub on :3000 in-container, but every CLI relay client (ensureRelay + the queue's spawned workers) is hardwired to 127.0.0.1:8787 — so a web-UI create spawned a SECOND relay daemon inside the container that took ownership of the new box, invisible to the hub's store/registry. Run the hub on 8787 in-container (compose env + port map, Caddy upstream, EXPOSE) so in-container CLI spawns discover and REUSE the hub's own relay. Found live on the deployed control box (final goal-scenario verify). Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
All four phases live-verified against the deployed Hetzner control box: web-UI box create + host-off push (PC relay hard-killed, branch advanced on GitHub via control-box lease). Records fixes #225/#226 and four new backlog findings (notably the web-UI/queue create path not wiring git leasing). Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
Task-oriented page for the deployed hub / control box: deploy to Hetzner, create boxes from the web UI or 'create --via-hub', run them with the laptop off (leased push), and manage them from the PC (boxes list, hub pull, prompts, custody). Linked from run-an-agent's next steps and added to the Guides nav. Marked experimental (the control-plane commands are still WIP/hidden). Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
…p spinner on deploy failure A transient Hetzner 502/504 during firewall creation aborted the whole deploy (and box create) on its first step, because createFirewall used retryOnAmbiguous: false. Unlike a server/image create (billable — a hidden success on retry would duplicate a paid resource), a firewall is free and a retry-duplicate is a harmless, per-op-uniquely-named orphan, so retrying a transient gateway error is safe. Flip both createFirewall call sites (control-plane deploy + per-box) to retryOnAmbiguous: true; setFirewallRules / deleteFirewall already were. Regression test: createPerBoxFirewall recovers from a 502. Also stop the 'control-plane setup' deploy spinner (code 1) on failure — it was declared inside the try, so the catch couldn't stop it and it kept animating its last 'creating the firewall…' frame under the error message. Reported live: 'agentbox control-plane setup' → 'deploy to hetzner failed: hetzner 502 http_502: Bad Gateway' with the spinner still spinning. Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
…r secrets The deploy's PROVIDER_SECRET_KEYS (what it scp's from the host secrets.env to the VPS) drifted from apps/hub's PROVIDER_CRED_KEYS (what marks a provider 'configured'): it only carried hcloud/e2b/daytona, so Vercel and DigitalOcean showed 'not configured' on a freshly deployed control box even with the creds set locally. Add DIGITALOCEAN_TOKEN/_API_URL and the Vercel access-token keys (VERCEL_TOKEN/OIDC/TEAM_ID/PROJECT_ID). VERCEL_AUTH_SOURCE is intentionally excluded — a CLI-login token lives in the Vercel CLI store, not secrets.env, so copying the marker without a token would falsely show 'configured' and fail at create time; use a VERCEL_TOKEN or the hub Settings form for vercel-from-hub. Reported live: vercel + digitalocean missing on a newly deployed control box. Claude-Session: https://claude.ai/code/session_01QnaMFuHVZUdTq5VzxiSg2L
A daytona create failed with "failed to start the claude session (exit 255)" and no cause: the box came up healthy with no agent in it. Two bugs behind that. `runDetached` resolved on the child's `exit` event, which fires before the piped stderr has drained — an ssh that gives up in under a second reported an empty stderr, so the thrown error carried nothing but the exit code. Resolve on `close` instead. And the pre-start fired exactly one ssh. Daytona's SSH gateway takes the ephemeral attach token as the username and hangs up on one it doesn't recognise yet, which is what a token minted a second after sandbox create looks like. Retry a 255 (ssh's own pre-command failure) up to 3 times with backoff — each attempt re-runs buildAttach and so mints a fresh token. A non-transport exit still fails on the first try, and a session the dropped connection left behind is accepted rather than retried into tmux's duplicate-session error. Claude-Session: https://claude.ai/code/session_01WM35PAMmswyzcxmAbMXaPp
|
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 4 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 d1a422f. Configure here.
| const cfg = await loadEffectiveConfig(process.cwd()); | ||
| const url = (cfg.effective.relay.controlPlaneUrl ?? '').replace(/\/$/, ''); | ||
| const adminToken = process.env.AGENTBOX_RELAY_ADMIN_TOKEN ?? ''; | ||
| if (!url || !adminToken) return; |
There was a problem hiding this comment.
Credential push skips env file
Medium Severity
After credentials propagate, the new custody mirror reads AGENTBOX_RELAY_ADMIN_TOKEN only from the environment and never loads ~/.agentbox/control-plane/control-plane.env. On a typical machine where setup wrote the token to that file, the push is a silent no-op even though hub custody is configured.
Reviewed by Cursor Bugbot for commit d1a422f. Configure here.
| log.warn( | ||
| `control box configured but ${routing.fellBackReason}; building ${providerName} box locally.`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Via-hub flag falls back silently
Medium Severity
With --via-hub, routing returns hub immediately, but if createCloudBoxViaHubAndAdopt returns null (e.g. missing git origin), foreground cloud runs continue into a local cloudAgentCreate without failing or warning—unlike agentbox create --via-hub, which stops with an error.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d1a422f. Configure here.
| // The box may well exist on the control box — don't let the error imply it | ||
| // definitely doesn't. | ||
| log.warn(`could not reach the control box — did not check whether '${ref}' is one of its boxes.`); | ||
| } |
There was a problem hiding this comment.
Numeric ref shows index error
Low Severity
After hub auto-adopt misses, purely numeric refs still exit with “no box with index … in this project” even when the user meant a numeric cloud sandbox id. Hub adoption was intentionally tried first, but the failure path still assumes a per-project index.
Reviewed by Cursor Bugbot for commit d1a422f. Configure here.
| if (box.needsAdopt !== true) return box; | ||
| const adopted = await tryAutoAdopt(box.name, process.cwd()); | ||
| if (adopted === null || adopted === 'unreachable') return null; | ||
| return (await listBoxes()).find((b) => b.id === adopted.id) ?? null; |
There was a problem hiding this comment.
Dashboard adopt uses name only
Low Severity
When a hub-only row needs adoption, the dashboard calls tryAutoAdopt(box.name) instead of the stable box.id (or sandbox id). Registrations allow optional names, so adoption can fail while the same box would resolve by id via hub adopt.
Reviewed by Cursor Bugbot for commit d1a422f. Configure here.
It sat under Getting started, where a pre-release channel is the wrong thing to hand a new user. Both inbound links come from Reference pages (cli, configuration), so that is where readers arrive from. Claude-Session: https://claude.ai/code/session_01WM35PAMmswyzcxmAbMXaPp
`hub setup` / `hub deploy hetzner` sent every progress line to a @Clack spinner, which overwrites itself, and only wrote `deploy.json` on success — so a deploy that died after the VPS booted (e.g. a 502 at `/healthz`) left no log and no record of the still-running machine. - Tee both commands to `~/.agentbox/logs/hub-{setup,deploy}.log`. - Add `onProvisioned` to `deployControlPlaneToHetzner`, fired as soon as the VPS has an IP, so the record is persisted before the ssh wait, the compose build, and the healthz poll — and print ssh + `docker compose logs app` recovery steps on failure. - Emit a `Host agentbox-hub` block in the managed `~/.agentbox/ssh/config` from that record, so the control box is reachable with `ssh agentbox-hub` instead of a per-deploy key path under `control-plane/ssh/<stamp>/`. Claude-Session: https://claude.ai/code/session_01WM35PAMmswyzcxmAbMXaPp
…m port `DEFAULT_DEPLOY_REF` was hardcoded to `main`, but the deploy is a two-sided contract: the VPS builds `apps/hub` from the cloned ref while the HOST writes the Caddyfile and `.env` that wrap it. A nightly CLI wrote `reverse_proxy app:8787` and full-hub env keys, while the VPS built v0.27.1 — whose compose publishes `8787:3000` behind Postgres and whose container listens on :3000. Caddy dialled a dead port for the whole healthz window while `curl localhost:8787/healthz` on the VPS answered fine, surfacing only as "did not become healthy (HTTP 502)". - `deployRefForVersion` derives the ref from the running CLI: `nightly` for a nightly build, `v<version>` for a release. - Parse the Caddy upstream port from the deployed compose (`hubContainerPort`) instead of hardcoding this CLI's port. - Reject a pre-full-hub ref (`isFullHubCompose`) right after the clone rather than after a 20-minute build. - On a failed healthz poll, ssh in and report whether the hub is up and unreachable or actually down, with `docker compose ps` + app logs. - The cloud-init clone no longer falls back to a bare `git clone`, which silently left the default branch checked out — a SHA ref now checks out, and a bad ref fails loudly instead of building the wrong version. Claude-Session: https://claude.ai/code/session_01WM35PAMmswyzcxmAbMXaPp
… test `user-config.schema.json` is `additionalProperties: false`, so a key the parser accepts but the schema doesn't know is a red squiggle in the user's editor. 15 registered keys had drifted out of it, including four whole branches (`git`, `ssh`, `cloud`, `integrations`) — so documented settings like `git.pushMode`, `relay.controlPlaneUrl`, `cloud.viaHub` and `update.channel` all flagged as invalid. `schema-drift.test.ts` missed it because it only validates a curated fixture list. Add an exhaustive KEY_REGISTRY -> schema check plus a per-enum value comparison; the enum check immediately caught `box.provider` missing `remote-docker`. Claude-Session: https://claude.ai/code/session_01WM35PAMmswyzcxmAbMXaPp
Installing a GitHub App is an owner/admin decision, so a control box was unusable on any repo the user doesn't own — hit live: the App was created under one account, the repos live under another, and every cloud create failed with "GitHub App is not installed on <repo>" no matter what was clicked. The fix is not a new credential model. The relay's bundle path — box asks the host to push, box never sees a credential — is already wired and reachable on a control box (server.ts routes git.push/git.fetch from any cloud registration into executeCloudAction). It just had no host checkout, no credential and no gh. - `hub.gitAuth: gh | app` (default `gh`). `hub setup` now reuses the token from your own `gh auth login` (or git's credential helper), shows whose it is and what it can do, and skips the App flow entirely. `--git-auth app` keeps it. - runGitRpc falls back to a scratch repo when there is no host checkout: a bundle is self-contained, so unbundle + push needs no persistent clone. The push target comes from the REGISTERED origin, never the box's own — a box could rewrite `origin` and redirect the host's credentials. - `auto` push mode leases only when the hub runs an App; a gh-mode hub holds one broad token that must never enter a box, so boxes route through the relay. - The hub loads the token at boot and runs `gh auth setup-git`, so clone, push and the gh.pr.* host actions all authenticate from one place. `gh` added to the image (those actions exited 127 before). - The worker no longer throws at construction without an App — that killed the hub at boot, which in gh mode is the normal day-one state. Resolved per job. - The token ships in the VPS data volume, not compose env (docker inspect). - The "run `agentbox hub add`" nag is silent in gh mode: there is no App. Claude-Session: https://claude.ai/code/session_01WM35PAMmswyzcxmAbMXaPp
The hub's git token must not survive into the compose env — that is readable via `docker inspect`. Pin the split, the export/whitespace forms, the App-mode no-op, and that an empty assignment is not shipped as a credential. Claude-Session: https://claude.ai/code/session_01WM35PAMmswyzcxmAbMXaPp
* test: add the always-on hub test VM (clean PC for remote-hub testing) scripts/hub-test-vm.sh provisions a persistent clean Ubuntu VPS that plays a user's PC: published CLI from npm, gh logged in as the test account, and a credential vault outside ~/.agentbox so every run can start from virgin state. docs/hub-test-vm.md is the runbook + test matrix. * test(hub-test-vm): fix reset self-kill + firewall reap, record the first live pass - pkill -f agentbox-relay matched the ssh command line running it, so reset killed its own session and stopped before the wipe; bracket the pattern. - Retry the control box's firewall delete (it stays attached to the dying server) and treat 404 as gone. - run/log subcommands: a long command needs tmux, an ssh nohup dies mid-pull.
…or source `agentbox hub deploy hetzner` cloned the monorepo onto the VPS and built 14 workspace packages plus the CLI there — ~2m40s of a ~4m45s deploy — to produce an artifact the published package already carries: `runtime/hub/apps/hub/server.js`, the same standalone bundle `agentbox hub` spawns locally. It now installs `@madarco/agentbox` pinned to the deploying CLI's exact version. The host ships the compose stack (the base docker-compose.yml verbatim, plus an override that swaps only the build block), so there is no repo on the VPS at all and no version-skew surface — that coupling is what made a nightly CLI's deploy 502 against a `main` hub listening on a different port. It also fixes base-image fingerprint parity by construction: the control box's runtime/ tree is now the same npm tarball the PC installed, rather than a ref that happens to match. `--ref`/`--repo` keep the clone-and-build path for deploying unreleased code, and a dev build (whose version was never published) falls back to it automatically. `--package <spec>` pins a different npm spec. deploy.json now records which build a control box is running, and `hub status` shows it.
… port A hub answering on the VPS but not over HTTPS was reported as one guess — "Caddy cannot reach it, so its upstream port does not match" — which is wrong whenever the certificate is the problem. Hit live: sslip.io derives the hostname from the IP, so a recycled Hetzner address arrived already at Let's Encrypt's 5-certs-per-identifier-per-week cap, and the deploy blamed a port that was perfectly correct. diagnoseUnhealthy now greps Caddy's own log when the hub is healthy locally, and names the actual cause — including the fix for the rate-limited case (redeploy for a different IP, or pass --domain).
…ntrol The sslip.io default derives the hostname from the IP, so it isn't really yours and Hetzner reuses released addresses — landing on one that already had five certificates issued this week leaves a healthy hub with no usable HTTPS until the 7-day window rolls. --domain is the way out (and what the new rate-limit diagnostic points at); the underlying deploy already accepted a domain, only the CLI never exposed it.
MDX parses the attribute value as JSX, so \" is an invalid attribute name character, not an escaped quote — `next build` failed to evaluate the page.
… text Bugbot caught that the certificate check matched Caddy's SUCCESS lines: `tls.obtain` appears in "obtaining certificate", "lock acquired", and "certificate obtained successfully", so a healthy cert plus a genuine upstream mismatch would have been reported as a certificate problem. The verdict now comes from an HTTPS request made ON the VPS (--resolve pinned to loopback), which exercises the real certificate and the real reverse_proxy upstream: 200 means the break is outside the box (DNS / :443 rule), 000 means the TLS handshake never completed, any other status means Caddy answered and the upstream is wrong. The log is read only for the rate-limit detail, which has an unambiguous error signature. Split out as the pure `describeCaddyHop` so the three-way verdict is unit-tested, including the exact log that fooled the old check.
… 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.
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.
…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.


Note
High Risk
Large behavioral shift (default cloud creates on remote hub, auto-adopt, merged destroy/prune/approvals) plus release/CI dist-tag guardrails—mistakes could affect stable users or leave ghost hub state.
Overview
This PR is a pre-release cut (
0.28.0-nightly.…) with a full [Unreleased] changelog covering the control box, nightly channel, and many CLI/hub fixes.Control box as default path. With a remote hub configured, cloud
createand foregroundclaude/codex/opencoderoute to the control box (cloud.viaHub,--via-hub/--local). New helpers enqueue jobs, poll, adopt locally, and run background-ientirely on the VPS.agentbox control-plane *is removed in favor ofagentbox hub *(setup, deploy, custody, credentials, boxes, prompts, jobs, adopt/pull/target).Local adoption. Box resolution auto-adopts hub boxes by name;
ls/ dashboard merge remote listings; approvals and relay URLs follow each box’s plane; destroy/prune reaps hub registration; prepare can share/adopt baked bases via custody.Nightly channel. Docs and
/release-notes nightlyflow; CI andbox-imagerun onnightlybut do not movelateston images or npm; tray install respects stable vs nightly release tags; README documents@nightlyandself-update --channel.Other notable changes:
wsadded as a direct dependency; Portless brought back before hub/box.localhostURLs; cloud attach helpers moved to@agentbox/sandbox-cloud; docs map updated (control-box plan, nightly plan);install tray→install appin README.Reviewed by Cursor Bugbot for commit d1a422f. Configure here.