feat(mobile): cloud workspaces — list, open, terminal, provisioning, actions

Merged
SA
3mo ago
+1622-198

Links

  • Decision log: apps/mobile/plans/20260817-cloud-workspaces.md
  • Follow-up ticket: SUPER-1915 (media/artifacts on mobile)

Summary

  • Cloud workspaces (Blaxel sandboxes) on iOS with parity to the desktop's cloud surface: a Cloud section on Home, open a sandbox and drive its terminal, provisioning/failed states, rename/delete via the cloud router, gated by the cloud-workspaces flag + the API's internal-only check.
  • Two fixes found on the way that affect desktop too: deleteSandbox never recognised the SDK's not-found (removing a failed workspace 500'd), and the builtin Claude agent refuses to launch as root in a sandbox (IS_SANDBOX=1 + baked Bypass Permissions acceptance; needs an image rebuild to land).

Why / Context

Desktop shipped cloud workspaces (#6505, #6555, #6566). Mobile reached host-service only through the relay by routing key; a sandbox has no host row, no presence, no relay path — it is reached directly at a brokered preview URL with a short-lived token. This PR gives mobile that second address source without cloud-specific code in every hook.

How It Works

  • A sandbox is a pseudo-host keyed by the cloud workspace id (as on desktop). hostServiceUrl(org, machineId) replaces buildRelayHostUrl and resolves sandbox addresses from lib/sandbox-access; the HTTP client attaches X-Blaxel-Preview-Token per URL. Every hook that addresses hosts by id — terminals, git status, projects, agents — reaches the sandbox unchanged.
  • useCloudWorkspaces (1s poll while provisioning) + useSandboxAccess mint for every ready row, re-mint at 80% TTL, on AppState active, and at dial time if stale (iOS freezes timers). useCloudWorkspaceItems restates each sandbox's served row under the cloud id; useWorkspaceHost resolves cloud ids first and lets the cloud row own the name.
  • The terminal WebView signs sandbox dials with bl_preview_token on every attempt. The desktop's terminal socket works only because the edge sets an HttpOnly bl_preview_token cookie that Electron replays; WKWebView has its own cookie store, so mobile can't inherit that (recorded in docs/cloud-sandbox-mismatches.md).
  • Home: Cloud section pinned above projects, visible regardless of the host filter and in the host-offline state; rows reuse WorkspaceRow (spinner while provisioning, red dot when failed, + disabled until ready); useHostsTerminals fans out over sandbox hosts; PR badges use cloudWorkspace.repoForProject.
  • Workspace screen: CloudWorkspaceProvisioningState (steps, elapsed, stuck hint; failed + Remove) until the sandbox serves the row. Row menu / actions sheet / header actions route rename and delete to cloudWorkspace.rename/delete; the actions sheet drops the Host row for cloud.

Manual QA Checklist (done on the iPhone 16e simulator against two live Blaxel sandboxes through this worktree's local API)

  • Cloud section lists live sandboxes; session marks and diff pills resolve over sandbox hosts
  • Open → terminal socket connects (query-param token) → composer terminal.send → output streams back
  • Row + → composer → agents.run in the sandbox → new tab opens
  • New-session sheet lists the sandbox's agents; sessions sheet close → terminal.killSession
  • Provisioning row + screen; failed row + screen; Remove from the failed screen deletes
  • Row long-press menu: Rename/Delete/Copy ID/Share on ready rows, Delete only on failed rows, nothing destructive while provisioning
  • Rename (row menu and actions sheet) writes the cloud row; list and title follow
  • Actions sheet: project from the sandbox, no Host row, Delete offered
  • Changes / files-changed diff / commits answer from the sandbox
  • Not exercised: an overnight background token-expiry cycle (re-mint paths are in place); PR-icon tap (needs an open PR on the branch)

Testing

  • bun run typecheck (mobile, host-service, trpc)
  • bun run lint
  • Manual, as above; screenshots in the internal report shared with @satya

Design Decisions

  • Pseudo-host + registry instead of threading URLs through every hook: the same shape desktop uses (hostId = cloud id, credentials keyed by URL); 16 hooks kept their signatures.
  • Explicit bl_preview_token on every terminal dial: correct regardless of the edge cookie; the desktop should do the same rather than rely on /events having dialled first.
  • Cloud row owns the name: rename never reaches the sandbox's copy, so the served row's name is overridden with the cloud row's.

Known Limitations

  • Create from mobile is PR B (interim cloudWorkspace.listProjects + App-token listBranches, sectioned project sheet). Externally created rows appear on the next 30s poll or pull-to-refresh.
  • The host-service/image change for Claude only reaches new sandboxes after an image rebuild.
  • cloud_workspaces.project_id still points at retired v2_projects; the environments direction is written into docs/cloud-sandbox-considerations.md.

Follow-ups

  • PR B: create from the composer.
  • Sandbox launches the agent from the prompt itself; sandbox attachments to blob storage (both in the docs).
  • SUPER-1915: media/artifacts on mobile.

Summary by cubic

Cloud workspaces on iOS: list, open, and run terminals by reaching each sandbox directly via a brokered preview URL with a short‑lived token. Previously mobile only reached relay hosts; now host resolution prefers a sandbox when the id is a cloud workspace, and terminals authenticate per dial.

  • Host routing and auth: hostServiceUrl(org, id) resolves a relay URL or a sandbox URL; the HTTP client attaches X-Blaxel-Preview-Token by URL. TerminalWebView now takes a host object and signs WebSocket dials with bl_preview_token when the host is a sandbox.
  • Cloud data and access: useCloudWorkspaces lists all rows (provisioning/ready/failed), polls at 1s while provisioning, and prunes stale sandbox grants. useSandboxAccess mints/refreshes addresses for ready rows (80% TTL, on foreground, and on dial if stale) and fully retires credentials on scope loss or URL changes (epoch guards stop mid‑flight mints from resurrecting retired grants). Delete failures on failed rows are surfaced.
  • Home: adds a Cloud section above projects, visible even if the selected host is offline (offline takeover waits for the cloud list). Rows reuse WorkspaceRow with provisioning/failed treatment. Session marks and attention fan out over machine + sandbox hosts via useHostsTerminals (30s for sandboxes on the list; an open screen observing the same key at 5s wins). PR badges for cloud rows use cloudWorkspace.repoForProject.
  • Workspace: shows a provisioning/failed screen until the sandbox serves; hides tabs until live. Rename/delete route to the cloud router; the Host row is omitted for cloud.
  • Fixes: packages/trpc deletion now recognizes provider not‑found. packages/host-service sets IS_SANDBOX=1 for sandbox PTYs; the sandbox image bakes Bypass Permissions acceptance for Claude. Docs record the edge‑cookie dependency, Claude findings, and the environments direction.
  • Gated by the cloud-workspaces PostHog flag and an API internal‑account check.

Migration

  • Replace buildRelayHostUrl with hostServiceUrl wherever host-service URLs or terminal dials are built.
  • Rebuild the sandbox image and create new sandboxes to pick up the Claude fix.
  • Ensure your org is entitled and the flag is enabled.

Summary by CodeRabbit

  • New Features

    • Added mobile cloud workspace support, including listing, search, sorting, refresh, terminal access, and workspace lookup.
    • Added provisioning and failure screens with progress details, recovery, and removal options.
    • Added cloud workspace rename and delete actions with confirmation prompts.
    • Added secure sandbox access with automatic credential refresh.
    • Added cloud-specific status indicators and disabled actions during provisioning.
  • Bug Fixes

    • Improved host routing and handling of missing sandbox workspaces.
    • Improved terminal environment setup and sandbox authentication.
    • Prevented cleared sandbox credentials from being restored by in-flight requests.