api: platform forwarder behind PLATFORM_SURFACES (port plan 1d), default off
Port plan step 1d (manaflow-ai/cmux-platform docs/port-cloud-worker.md 3.1, row 1d): the strangler forwarder in cmux-api, wired for NEXT Cloud's machine surface. Default off: nothing changes until a deploy adds the binding and the var.
Leo, 2026-10-31: "try to not disrupt classic;s cloud code too much there's still stuff on there we'll migrate them eventually but next cloud can just yolo yolo yolo in cmux-platform"
Leo, 2026-10-31: "best if you guys just merge stuff tbh and write more in rust instead of swift and put code into cmux-platform if it's cloud related"
What it does
- New
backend/apps/api/src/platform-forward.ts.PLATFORM_SURFACES(comma-separated, per environment) names the surfaces the platform serves; the only one wired so far iscloud. When a request belongs to a listed surface, it goes toenv.PLATFORM.fetch(request), the service binding tocmux-platform-<env>. Every other request runs the existing handler. - The
cloudsurface covers:- the hand-written Cloud machine routes:
/v1/cloud/bind,/v1/cloud/keyset,/v1/cloud/machines/:vm/acpmux/*,/v1/cloud/machines/:vm/client-session,/v1/cloud/machines/:vm/{exec,fs/*}; /v1/opsand/v1/read, routed by op owner rather than by path (3.1). The forwarder readsopfrom a clone of the body and looks it up in the@cmux/protocolcatalog. Only acloud:CloudDOop of the endpoint's class is forwarded, with the original body intact. A body that does not parse, a body with no stringop, an unknown op, or an op on the wrong endpoint stays in cmux-api, so the existingvalidation.invalidanswers do not change.
- the hand-written Cloud machine routes:
- Entry order (3.1):
spendTicketAtEntrystill runs first on every path, and the forwarder runs after it. A forwarded request therefore carries a ticket that cmux-api has already spent, and each ticket is spent once./v1/wire/cloudbelongs to the separatewiresurface (plan 2b/2c) and is not forwarded here. - If a surface is listed but the
PLATFORMbinding is absent, the request stays local and the Worker logsplatform.forward.unbound, so setting the var before the binding exists breaks nothing. If the platform throws, the Worker answers a retryable 503owner.unreachableand does not replay the request locally, because the platform may already have acted on it. env.tsgains two optional fields:PLATFORM?: FetcherandPLATFORM_SURFACES?: string.
Why classic and NEXT are unchanged by default
wrangler.jsoncis untouched. No environment declares the binding or the var, soplatformForwardreturns on its first line without reading the request.- Classic Cloud (
web/services/*, Next.js/api/vm) is not touched. - Tests (
backend/apps/api/test/platform-forward.test.ts):- With the var unset, empty, or naming only unknown surfaces (each tried with the binding present and absent), every Cloud route, Cloud op, staying op and
/v1/healthgets the same status from the existing handler, and the fake platform is never called. - With the surface on, every Cloud route plus a CloudDO mutation and a CloudDO read is forwarded with its URL, method and body intact.
- With the surface on, these stay local with a status and body identical to the off case: an op of an owner that stays (mutation and read), a malformed body,
null, a non-stringop, an unknown op, a read on/v1/ops, a mutation on/v1/read,GET /v1/ops,/v1/healthand/v1/wire/cloud. - With the surface listed and no binding, requests stay local and
platform.forward.unboundis logged. - When the platform throws, the answer is a retryable 503.
- A forwarded request with a fresh ticket spends it exactly once (counted at
WIRE_TICKET_DO), before the forward, and the replayed ticket is then refused on its socket. A request that stays local also spends its ticket once.
- With the var unset, empty, or naming only unknown surfaces (each tried with the binding present and absent), every Cloud route, Cloud op, staying op and
The deploy step (not in this PR)
-
Who deploys cmux-api.
.github/workflows/backend.yml, using theCLOUDFLARE_API_TOKENsecret of each GitHub environment. A push tofeat-cmux-nextdeployscmux-api-development(environmentcmux-next-development) andcmux-api-staging(cmux-next-staging, jobdeploy-staging). Production (cmux-next-production) is deployed bypromote-productionafter gates G1-G7, theCMUX_NEXT_PRODUCTION_HOLDvar and 6 h spacing, or by a push tomain. None of these environments requires reviewers; each has only a branch policy. -
The flip, staging first. One cmux PR adds this to
env.staginginbackend/apps/api/wrangler.jsonc:"services": [{ "binding": "PLATFORM", "service": "cmux-platform-staging" }], and"PLATFORM_SURFACES": "cloud"inenv.staging.vars.
Merging it to
feat-cmux-nextdeploys it throughdeploy-staging. Production gets the same change withcmux-platform-productionafter the staging soak (plan 5.3-5.4). Rollback is removing the var. -
Targets.
cmux-platform-development,-stagingand-productionall exist: cmux-platform'sci.ymldeploys all three on every push to itsmain, and recent runs are green. -
Blocked on the platform. cmux-platform does not serve the Cloud machine routes or the CloudDO ops yet. The ports of the cloud driver, VM API and CloudDO layers are in progress. Under plan rule 5, the
cloudsurface is switched on only after those surfaces and their parity tests have landed there.
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Adds a strangler forwarder that hands cmux-api routes to the platform Worker through a new optional PLATFORM service binding, gated behind the PLATFORM_SURFACES environment variable. Default is off: no environment sets the var or the binding, so every request runs the existing handler unchanged.
Behavior
- With the
cloudsurface on, the Cloud machine routes (/v1/cloud/bind,/v1/cloud/keyset, the acpmux, client-session, exec, and fs paths) andcloud:CloudDOops on/v1/opsand/v1/readgo to thePLATFORMbinding with URL, method, and body intact. - Ops are routed by owner rather than path: a malformed body, an unknown op, or an op on the wrong endpoint stays local with its existing
validation.invalidanswer. - The entry ticket spend still runs first; a forwarded request carries its already-spent ticket, spent exactly once.
/v1/wire/cloudis not forwarded here. - A listed surface with no binding stays local and logs
platform.forward.unbound; a platform failure answers a retryable 503 without replaying locally.
Rollout
- Switching the surface on is a separate deploy step that adds the binding and
PLATFORM_SURFACES: "cloud"to staging first, then production; rollback is removing the var. - The
cloudsurface goes on only after cmux-platform serves the Cloud machine routes and CloudDO ops.