feat: forward cloud workspace ports to your computer, and list forwards with `superset ports`
Links
- Design options (where Forward/Stop lives, row designs): https://app.superset.sh/page/port-forwarding-ui-options-nmix5k
Summary
This PR bundles three related changes:
- Forward cloud workspace ports to this computer (desktop). Conductor-style: nothing is forwarded until someone clicks Forward. The control is in the Share popover ("Ports on this computer"), the sidebar ports card and the top-bar ports dropdown. If the local port is busy, Forward picks the next free one.
superset ports(realtime + API + CLI). The desktop reports its forwards to the workspace's realtime hub. A new read-onlycloudWorkspace.forwardsprocedure andsuperset ports list/superset ports port <n>show who forwarded which port to which local address. Agents inside a cloud workspace can use these.- Fix: forwards now reconnect after the host refuses a session (e.g. the sandbox was asleep). This also affects remote-device forwarding.
Part 1: Forwarding cloud ports (desktop)
Why
Cloud workspace ports weren't reachable from the laptop at all. Remote-device forwarding (relay, #6858) skipped cloud workspaces, and the sidebar never even listed their ports.
How
- Same engine as remote-device forwarding (
PortForwardManagerplus the/fwdmux on host-service), but routed through the sandbox gate with the workspace's ticket.SandboxAccessProviderhands the ticket to the main process. Gate hosts never fall back to the relay token. - Explicit forwards belong to no window. They survive navigation and end on Stop, sign-out or quit.
- Busy local port: Forward tries the same number, then up to 20 higher, then any free port. The row says "5173 was in use on this computer, so it moved to 5174". Remote-device auto-forwarding keeps its existing "Use another port" prompt.
- Host unreachable at Forward time (e.g. sleeping box): the forward still binds the local port and heals on the first connection that gets through.
- Ports that stop being detected: forwards on them still show in the Share popover ("Not detected in the workspace right now"), so they can be stopped.
- The open cloud workspace's ports now reach the sidebar. Its row comes from the cloud list, not a sidebar placement, so it was being filtered out.
- Dev only: Node doesn't resolve the dev gate's
*.localhost, so those connections go to127.0.0.1with the originalHostheader.
New components
CloudPortForwardActions: Forward/Stop, shared by the sidebar card, the ports dropdown and the Share section.CloudWorkspaceShareButton/components/SharePortsSection(SharePortRow,ShareStaleForwardRow).main/lib/port-forward/forward-reporter.ts.
Part 2: superset ports
Why
An agent inside a cloud workspace can't see which port people forwarded where. That mapping lives only on each person's laptop, the same way a Docker container can't see its published host port.
How
| Decision | Choice | Rationale |
|---|---|---|
| Where the mapping lives | Per-workspace WorkspaceHub Durable Object | No sandbox release needed; box sleep doesn't lose it; existing access checks, revocation and silent-socket sweeping are reused |
| How a laptop's entries expire | Its WebSocket closing | No heartbeat or TTL code. Quit, sleep or network loss drops the entries; the hub's sweep removes sockets that went silent |
| How the agent reads it | CLI → API cloudWorkspace.forwards → hub (shared secret) | The box's CLI signs in to the API, not to realtime. The API applies the same access rule as opening the workspace |
Realtime
- A new forwarder connection role. It isn't presence, isn't recorded as a visit and gets no broadcasts.
- Reports are stored in hub SQLite, not on the socket, because a hibernating socket's state is capped at 2 KB.
- Messages over 4 KB are refused, and unchanged reports skip the write.
- Losing access closes forwarder sockets, the same as viewers.
- A GET on the same path, checked with the shared secret (timing-safe compare), lists forwards for the API.
Desktop
- The reporter opens one socket per workspace that has explicit forwards, with the token in the
Authorizationheader. - The backoff only resets after a socket stays up, because the hub refuses by accepting and then closing.
- A 4401-refused token isn't retried until it changes. 4403 only backs off, since access can be granted later.
CLI
ports listdefaults to$SUPERSET_WORKSPACE_ID, which equals the cloud workspace id inside a sandbox (checked).ports port <n>prints barehttp://localhost:Naddresses, one per person, and exits non-zero if nobody forwarded the port.
Part 3: Reconnect fix (mux-session.ts)
ws emits no close after a refused upgrade, so a session that got a non-101 answer was never marked dead. The transport then kept reusing it, and the local port stayed dead until Stop and Forward. This is now fixed.
Manual QA
All of this was verified by clicking through the desktop dev app on a cloud box. The box played the role of the sandbox, through a local wrangler dev gate and a pass-through shim in front of its host-service. The shim and stub are not in this PR. A fresh Vercel sandbox still runs the released host-service, so this was the only way to exercise it.
Forwarding
-
npm run devin a cloud workspace terminal → the port appears as5173 · cloudin the sidebar card and the Share popover - Forward with 5173 busy locally → it goes straight to
localhost:5174with the "moved" note; the page loads; hot reload works through the forward - Leave the workspace → the forward keeps serving; come back → Stop → the local port is freed
- Sleep/wake: box down (gate answers
/fwd 500) → box back → the same local port reconnects without clicks. This failed before themux-sessionfix. - The box restarted for real during testing → the app reconnected; after restarting the dev server, Forward worked
Reporting
- Forward → the hub lists
5173 → localhost:5174with the person's name; Stop → the hub list is empty (read via the realtime endpoint with the shared secret)
Not verified
superset ports listend to end against a dev API. The dev CLI's sandbox sign-in rules got in the way. The API procedure is a thin wrapper over the hub read verified above, and the CLI commands typecheck and are covered by the suite.- The fixes from the review round below weren't re-run in the app. They're covered by unit tests.
- A real Vercel sandbox through the deployed gate.
Testing
bunx tsc --noEmit: desktop, realtime, trpc, cli, shared all cleanbun test: desktop port-forward + dashboard components (264), realtime (18), trpc (834), cli (482)- New tests:
- explicit forwards outlive windows;
- an explicit forward moves to a free port when its own is busy;
- recovery after a failed first probe;
stopExplicit;- the reporter sends once per change, uses the auth header, closes when forwards end, and doesn't hammer a refusing hub;
- the hub forwards store (report, prune, keep
since, parse limits); - the cloud port query target.
bun run check:i18n: new strings translated in all 17 locales. The port-forwarding "Forward" has its owncontextso it doesn't reuse the navigation "Forward" translation.- Biome clean
An independent review pass (state machines, security, code) found no high-severity issues. Its medium findings are fixed in this PR:
- sign-out now tears down forwards, reporter sockets and tickets;
- the reporter's retry loop on refusals is gone;
- a forward no longer gets stuck in error after a failed first probe;
- forwards whose port disappeared can now be stopped.
Known Limitations
- Forwards on other workspaces can't be seen or stopped from where you are. They show only in their own workspace's Share popover. A global "forwards on this computer" list is the next step.
- Background workspaces still sleep. Forwarded traffic doesn't keep a sandbox awake, and the gate doesn't wake it. After sleep the dev server has to be started again.
- The hub has no per-connection message rate limit beyond the 4 KB cap and skipping unchanged reports. Forwarders share the per-user presence connect limit.
- "Forward a port…" for undetected ports is not included. It needs a host-service change:
/fwdonly allows ports attributed to the workspace.
Compatibility / Rollout
- The API change is additive (
cloudWorkspace.forwards, new and read-only). - Order: the realtime worker and API deploy on merge. The CLI
portscommands and the desktop changes ship in their next releases. Perpackages/trpc/AGENTS.mdrule 9, don't release the CLI before the API is live. - No sandbox release needed. The desktop uses the existing
/fwdroute on today's host-service. - A desktop released before the realtime deploy just backs off when reporting; forwarding itself works.
Rollback
- Revert the PR. Forwards are in memory on each laptop and hub rows are ephemeral, so there's no data to clean up.
Summary by cubic
Cloud workspace ports can now be forwarded to your machine with a click, and superset ports shows who forwarded what from inside the workspace.
New Features
- Forward cloud workspace ports from the Share popover, the sidebar ports card, or the top-bar ports dropdown; nothing forwards until you click Forward, and forwards survive navigation until Stop, sign-out, or quit.
- If the local port is busy, Forward picks the next free port and notes the move in the row.
- Cloud forwards go through the sandbox gate with the workspace's ticket and never fall back to the relay token, so a failed ticket hand-off fails closed.
- The desktop reports its forwards to the workspace's realtime hub, and a new read-only
cloudWorkspace.forwardsAPI procedure exposes them. - New CLI commands
superset ports listandsuperset ports port <n>list per-personhttp://localhost:Naddresses; they default to$SUPERSET_WORKSPACE_IDinside a workspace and are limited to a workspace's own sandbox callers. - The open cloud workspace's ports now appear in the sidebar.
Bug Fixes
- A refused WebSocket upgrade (for example, a sleeping sandbox answering
/fwd500) left a mux session never marked dead, so the local port stayed dead until the forward was restarted; it now reconnects on the next request. - Sign-out now clears every forwarding credential and evicts cached mux sessions, so a new account or a forward still starting cannot reuse the previous account's tunnel.
- The reporter's socket now has a handshake timeout, so an upgrade a proxy never completes falls into the normal retry path.
Summary by CodeRabbit
- New Features
- Added controls to start and stop port forwards for cloud workspaces, view detected and active forwards, and open available local addresses.
- Forwards can remain active until stopped, even when a port is no longer detected. If the requested local port is busy, another port may be assigned.
- Workspace sharing displays forwarded-port details, including who is forwarding each port and when forwarding began.
- Added CLI commands to list forwarded ports for a workspace or a specific port, with local addresses and forwarding details.
- Added translations for port-forwarding controls and status messages in multiple languages.
- Bug Fixes
- Improved cloud workspace port detection and forwarding status reporting.