cmux-api: PlatformMachines.resize, so cmux-platform can grow a machine in place

Merged
TE
6d ago
+97-2

Why

cmux-platform right-sizes NEXT Cloud machines automatically: it snapshots a machine, then grows it in place one size step. It reaches cmux-api's CloudDO only through the PlatformMachines service binding, which had snapshot but no way to resize an existing machine. Resize existed only inside create/fork's grow-to-fit phase and as the person-facing op cloud.machine.resize.

Leo, 2026-10-31: "we ofc wanted to have the thing be having snapshots of whatever vms to do the right-sizing"

cmux-platform: CloudDO (the team's machine ledger, plan checks and provider calls) still lives here and has not moved, so the binding method that reaches it lands here. The consumer side (MachinesBinding / MachineProvider.resize) is in manaflow-ai/cmux-platform.

Contract

resize(caller: PlatformCaller, machine: string, shape: PlatformShape): Promise<PlatformReply<PlatformMachine>>
  • Runs as the caller's own cloud.machine.resize ({ machine, size: { cpu, memory_mb, disk_mb } }), so the team's plan limits, grow-only and one-change-per-machine apply as they do for a person's machine. It is not a cleanup op: it answers only where PLATFORM_MACHINES is "on".
  • ok: true returns the machine's metadata with the provider-confirmed shape (the same answer get gives).
  • The shape is already at the target (read from the provider, not the row): it returns the metadata and makes no provider call, so a retry after success is a no-op success.
  • Refusals:
    • validation.invalid: bad caller, machine id or shape (positive safe integers, same check as create), before any team is reached
    • platform.machines_off: PLATFORM_MACHINES is not on
    • auth.forbidden: the user may no longer use the team's machines
    • cloud.machine.not_found: not a platform machine this user made, including another person's machine and the caller's own non-platform machine
    • cloud.provider.unsupported: a vm-api transport machine
    • mutation.indeterminate (retryable): the machine is still provisioning, another change on it is still running, or this resize's provider call has not settled yet
    • cloud.size.grow_only, cloud.size.locked, cloud.machine.not_running, cloud.plan.required: from the reducer, passed through
    • A final provider failure: the ledger's recorded error, retryable: false. The row is restored, so a later call starts a new attempt.
  • Keys: each call gets a fresh idempotency key. A per-revision key recorded a refusal (for example grow_only) under the key that the next call, for a different size or after a plan change, would reuse. That call then got idempotency.conflict, which the first test run caught. A duplicate grow can't happen because a pending change answers mutation.indeterminate before any intent (new PlatformHost.changing, read from CloudDO's pending set), the reducer's busy check covers races, and a finished grow is detected from the provider's shape.

Tests

test/platform-machines.test.ts:

  • grows a bound platform machine one step and returns the exact shape; a same-shape retry makes no second provider call; a shrink is refused with cloud.size.grow_only; a provider refusal is final and the next call grows; a provider call that doesn't answer stays pending (mutation.indeterminate on retry, no second intent) and the alarm finishes it
  • refuses another person's platform machine and the caller's own non-platform machine (not_found), and a user who left the team (auth.forbidden)
  • extends "validates every argument" (bad machine id, fractional, NaN or null shape) and the machines-off case

cmux-ci (Linux pool), at the head: bun run typecheck (all backend packages), bun run lint:size, and bun run test -- test/platform-machines.test.ts test/cloud-resize.test.ts test/cloud-start-pause.test.ts test/cloud-do.test.ts: 4 files, 38 tests passed.

Agent session: agent session

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds PlatformMachines.resize(caller, machine, shape) so cmux-platform can grow a machine it booted one size step, through the person's own cloud.machine.resize op.

  • Runs as the caller's own cloud.machine.resize, so the team's plan limits, grow-only, and one-change-per-machine rules apply.
  • A same-shape retry is a no-op success: the provider's confirmed shape (not the row) decides "already there", with no second provider call.
  • Uses a fresh idempotency key per call, so a refusal like grow_only is never recorded under a key a later call would reuse.
  • Pending changes answer retryable mutation.indeterminate before any intent is submitted, via the new PlatformHost.changing.
  • Failed provider calls return the ledger's recorded error with retryable: false, and the row is restored so a later call starts a new attempt.
  • Refusals mirror create and the resize op: validation.invalid, platform.machines_off, auth.forbidden, cloud.machine.not_found, cloud.provider.unsupported, and the reducer's cloud.size.grow_only, cloud.size.locked, cloud.machine.not_running, cloud.plan.required.
  • Tests cover growth, idempotent retry, shrink refusal, pending and failed provider states, and ownership and access checks.