Skip to content

feat(cli): put ls/list on the hub /api/v1 boxes endpoint (thin CLI Step 3) - #281

Merged
madarco merged 5 commits into
feat/cli_api_consolidationfrom
agentbox/step3-listing
Jul 29, 2026
Merged

feat(cli): put ls/list on the hub /api/v1 boxes endpoint (thin CLI Step 3)#281
madarco merged 5 commits into
feat/cli_api_consolidationfrom
agentbox/step3-listing

Conversation

@madarco

@madarco madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Step 3 — Listing (ls / list) onto /api/v1

Converts agentbox ls/list to a pure /api/v1 REST client, per docs/hub-api-single-path-plan.md.

What changed

  • agentbox list reads GET /api/v1/boxes only (via fetchBoxListing) and renders straight off the enriched HubApiBox view model — no more client-side /admin/store merge. Same wire the tray and web UI read, in both local-hub and remote-control-box modes.
  • Enriched Box payload (apps/hub/lib/boxes/types.ts + hub-api-client.ts) with the adoption/reconstruction fields cloud boxes need — sandboxId, originUrl, publicHost, image, webPort, previewUrls, lastAgent, topology, plus branch/shellCount. These are on the critical path for Step 4 (registrationToBoxRecord rebuild) and Step 7 (url reads endpoints off the payload); the exact field map + what stays a re-minted secret is documented in the plan doc's "Notes for later steps".
  • --live moves server-side as GET /api/v1/boxes?live=1, mirroring the established GET /api/v1/providers?freshness=1 opt-in-expensive pattern: on the in-process host topology the hub runs an authoritative provider.probeState per cloud box (docker skipped, 4s/box timeout, best-effort). The hub is now the only place holding provider credentials, so a persisted lastState that lies (platform-side stop) can still be corrected — dropping the flag would have been a real regression.
  • Offline fallback kept, re-keyed onto the API { boxes } payload (~/.agentbox/hub-boxes-cache.json). ls still prints the last listing (marked stale) when the hub is unreachable.

Deferred (noted in the plan doc)

hub-merge.ts / list-merged.ts / hub-list.ts's fetchHubListing (the /admin/store admin path) were not deleted — commands/dashboard.ts (an IO-plane TUI, outside Step 3's file set) still consumes them at runtime. Deleting them + converting the dashboard is Step 7/11 cleanup. fetchHubListing's cache was repointed to hub-registrations-cache.json to avoid a schema clash with the new API cache.

Verification

  • Full-repo pnpm typecheck green (31/31); hub suite (79) + CLI suite (1099) + new box-listing.test.ts (4) + rewritten list-cmux.test.ts green.
  • End-to-end on a real local hub with a mix of docker (x2) + e2b cloud (x2) + an in-flight job: (creating) box: ls and ls -g render correctly, project-scoping groups right, cloud rows carry the enriched fields (sandboxId/image/webPort/previewUrls/topology/branch) verified straight off the /api/v1/boxes response body.
  • Remote-shaped path (CLI pointed at the hub as a remote control box via relay.controlPlaneUrl + AGENTBOX_HUB_API_KEY): identical output — same code, both modes.
  • Offline cache: hub stopped → remote-shaped ls -g prints all boxes from cache with hub unreachable — showing boxes as of just now.

https://claude.ai/code/session_01QcvgzAktjnWXx1hmieqeTe


Note

Medium Risk
Listing and project scoping now depend entirely on hub API shape and originUrl (still missing for many non–via-hub creates); --live adds hub-side provider probes with timeouts but wrong persisted state could still show until probed.

Overview
agentbox ls / list is now a thin hub client: it fetches a single GET /api/v1/boxes listing via fetchBoxListing and renders HubApiBox rows. The old client-side merge of local docker state with /admin/store registrations (list-merged, hub-merge, on hub / orphan qualifiers) is gone from this command; legacy admin listing remains only for dashboard.ts.

The hub Box API payload is expanded so the CLI can drive the table and dock without local probes: webUrl/vncUrl, agent activity and session titles, shellCount, and a non-secret adoption/reconstruction block (sandboxId, originUrl, publicHost, image, webPort, previewUrls, lastAgent, topology) for Step 4. --live is now ?live=1 on the hub (cloud SDK probeState per box on the in-process backend; Postgres plane ignores it).

Project-scoped ls uses exported boxInProject: folder match on projectRoot, or originUrl when the box path is absent or foreign (existsSync disambiguates local clones vs remote hub paths). Offline behavior keeps hub-boxes-cache.json on { boxes }, with no-token messaging for remote hubs missing AGENTBOX_HUB_API_KEY; admin registrations cache moved to hub-registrations-cache.json.

Reviewed by Cursor Bugbot for commit 83c74fa. Configure here.

…ep 3)

agentbox list now reads GET /api/v1/boxes only (via fetchBoxListing) and
renders straight off the enriched HubApiBox view model — no /admin/store
merge path. The Box payload gains the adoption/reconstruction fields cloud
boxes need (sandboxId, originUrl, publicHost, image, webPort, previewUrls,
lastAgent, topology) plus branch/shellCount, so Steps 4 and 7 can rebuild a
BoxRecord and read endpoints from the payload alone.

--live moves server-side as GET /api/v1/boxes?live=1, mirroring the
providers?freshness=1 opt-in-expensive pattern: the in-process host topology
runs an authoritative provider.probeState per cloud box (docker skipped,
4s/box timeout, best-effort) before mapping. The persisted-state plane
ignores it.

The offline fallback (~/.agentbox/hub-boxes-cache.json) is re-keyed onto the
API { boxes } payload. hub-merge.ts / list-merged.ts / fetchHubListing survive
because dashboard.ts still consumes them; deleting them and converting the
dashboard is deferred to Step 7/11 (noted in the plan doc). fetchHubListing's
cache was repointed to hub-registrations-cache.json to avoid a schema clash.

Claude-Session: https://claude.ai/code/session_01QcvgzAktjnWXx1hmieqeTe
@vercel

vercel Bot commented Jul 29, 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 29, 2026 3:29pm

Request Review

Comment thread apps/cli/src/commands/list.ts
Bugbot #281: after switching ls to HubApiBox, stateCell fell back to
`state ?? status` but agentSummary and cmuxStatusCell gated on the host-only
`state` alone. A read-only Postgres/plane box carries `status: 'running'` with
no `state`, so it read `running` in STATE yet `-` in AGENT and `[running]` in
the cmux glyph. Route all three through a shared effectiveState() helper so a
plane box renders its agent, while `state` still wins for paused/stopped boxes
and job rows still bracket their creating/error status.

Claude-Session: https://claude.ai/code/session_01QcvgzAktjnWXx1hmieqeTe
@madarco

madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread apps/cli/src/commands/list.ts Outdated
Bugbot #281 (High): project-scoped `ls` on a thin client silently dropped
every control-box-created box. The filter matched originUrl only when
projectRoot was absent, but host mapBox rows carry the hub's absolute
projectRoot (meaningless on the laptop) and so never matched the local root.

Extract the scope predicate as boxInProject and match by repo identity
(originUrl) for a box with no local projectRoot OR any box when the hub is
remote, while keeping folder-based scoping locally so two clones of one repo
stay apart. mapBox now threads each box's registration originUrl onto the
payload (was cloud-only). Unit-tested via boxInProject.

Residual noted for Step 4: registerBoxWithRelay doesn't send originUrl for
plain docker/direct-cloud creates (only --via-hub does), so capturing it at
registration across providers is the cross-step half of this fix.

Claude-Session: https://claude.ai/code/session_01QcvgzAktjnWXx1hmieqeTe
@madarco

madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed both Bugbot findings.

1. "Missing state breaks agent display" (Medium) — fixed in 9eb63a15. stateCell, agentSummary, and cmuxStatusCell now all route through one effectiveState(b) = b.state ?? b.status helper, so a plane box (status:'running', no host-only state) renders its agent instead of -/[running], while state still wins for paused/stopped and job rows still bracket their creating/error status. (The re-report of this on 9eb63a15 looks like a stale anchor — the fix moved the check into the shared helper.) Regression test added.

2. "Thin client project scope broken" (High) — fixed in 71578a60, in the parts that live in this step's file set:

  • list.ts: the scope filter is extracted as a pure, unit-tested boxInProject. It now matches by repo identity (originUrl) for a box with no local projectRoot or any box when the hub is remote — a remote box's projectRoot is the control box's path and can't equal the laptop's, so the old projectRoot === undefined-gated origin branch dropped every control-box-created box from a thin client's scoped ls. Folder-based scoping is kept for a local hub so two clones of one repo stay apart.
  • hub-backend.ts: mapBox now threads each box's registration originUrl onto the payload (was cloud-only).

Residual (genuinely cross-step, noted in docs/hub-api-single-path-plan.md): registerBoxWithRelay (packages/sandbox-docker/src/create.ts + the cloud create paths) doesn't send originUrl today, so only --via-hub creates carry it; capturing it at registration for every provider is a provider-create-flow change outside Step 3's file set, folded into Step 4 (thin-client box resolution/adoption). The list.ts/mapBox pieces here are correct and become fully effective the moment registrations capture origin.

@madarco

madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread apps/cli/src/commands/list.ts Outdated
Bugbot #281 (Medium): the round-2 scope fix keyed "cross-machine" off
resolveHubTarget().mode === 'remote', but `agentbox hub expose` is remote-MODE
reached over loopback on the SAME machine — its boxes keep local projectRoots.
So origin-only matching wrongly folded boxes from another local clone of the
same repo into the current project's listing.

Add a pure isCrossMachineHub(target): remote-mode AND a non-loopback URL host.
A local hub and an exposed loopback hub both scope by folder; only a configured
remote control box scopes by repo identity. Unit-tested.

Claude-Session: https://claude.ai/code/session_01QcvgzAktjnWXx1hmieqeTe
@madarco

madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

7f873ed2: fixed the "Loopback hub breaks folder scoping" (Medium) catch — good one. hub expose is remote-mode but same-machine (loopback URL), so its boxes keep local projectRoots. Added a pure isCrossMachineHub(target) = remote-mode AND non-loopback host; a local hub and a loopback-exposed hub now both scope by folder, only a configured remote control box scopes by repo identity. Unit-tested.

Re the re-reported "Missing state breaks agent display" on 9eb63a15/71578a60: that one is already fixed — list.ts:114 is if (effectiveState(b) !== 'running') return '-' and cmuxStatusCell gates on the same effectiveState(b) = b.state ?? b.status helper, so a plane box (status:'running', no state) renders its agent. Looks like a stale anchor re-surfacing the same bug id across commits rather than a live issue.

@madarco

madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread apps/cli/src/commands/list.ts Outdated
Bugbot #281 (Medium): keying "cross-machine" off the hub URL host can't tell a
same-machine `hub expose` (loopback) from a genuinely remote hub reached via an
SSH tunnel to 127.0.0.1 — the two are indistinguishable by URL, and whichever
way the heuristic leans breaks the other (drop a remote hub's boxes, or leak a
local clone's).

Replace the per-hub URL heuristic with a per-box filesystem probe:
boxInProject scopes by originUrl only when the box's projectRoot names no
directory on this machine (existsSync) — that path is a remote hub's own, so
folder matching would drop it; a local folder (local hub OR exposed loopback
hub) does exist and scopes by folder, keeping two same-origin clones apart.
This one signal disambiguates both loopback cases the URL cannot. Drops
isCrossMachineHub; boxInProject tests updated.

Claude-Session: https://claude.ai/code/session_01QcvgzAktjnWXx1hmieqeTe
@madarco

madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

83c74fa1: both loopback findings ("breaks folder scoping" and "breaks origin scoping") are two sides of one problem — a 127.0.0.1 hub URL is ambiguous between our own hub expose and an SSH tunnel to a real remote hub, and any URL-based heuristic breaks one case or the other. Replaced it with a per-box filesystem probe: boxInProject scopes by originUrl only when the box's projectRoot names no directory on this machine (existsSync) — a remote hub's own path. A local folder (local hub OR loopback-exposed hub) exists, so it scopes by folder and keeps two same-origin clones apart; a genuinely remote box's projectRoot never resolves locally regardless of how the hub is reached. One signal, both cases correct. isCrossMachineHub dropped; tests updated.

The re-reported "Missing state breaks agent display" remains a stale anchor — list.ts:114 is if (effectiveState(b) !== 'running') and cmuxStatusCell gates on the same effectiveState = b.state ?? b.status, so a plane box renders its agent (fixed in 9eb63a15).

Note: the ci red on 7f873ed2 was a flaky 5s-timeout in @agentbox/sandbox-cloud (agent-credentials/custody-seed) under parallel load — untouched by this PR, and green in isolation (23/23).

@madarco

madarco commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

bugbot run

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 83c74fa. Configure here.

@madarco
madarco merged commit 0cdd3e4 into feat/cli_api_consolidation Jul 29, 2026
5 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 29, 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