Skip to content

feat(compute): give every workspace its own volume, and bake a golden image - #88

Merged
pythonlearner1025 merged 8 commits into
mainfrom
feat/warm-box-volumes
Aug 28, 2026
Merged

feat(compute): give every workspace its own volume, and bake a golden image#88
pythonlearner1025 merged 8 commits into
mainfrom
feat/warm-box-volumes

Conversation

@pythonlearner1025

@pythonlearner1025 pythonlearner1025 commented Aug 27, 2026

Copy link
Copy Markdown
Member

Two changes that share one goal: a workspace should survive its VM, and a new
workspace should not pay for work a disk image can already hold.

Validated end to end on real Hetzner resources. Create, destroy and recreate
all run against real infrastructure, and a file written to /workspace survives
the machine being destroyed. Evidence is in the comments below.

Headline numbers, all measured

create → ready
Stock baseline (GAPS.md:35) 130.6 s
Golden image, first working version 64 s
Golden image, after the speed pass 45.3 s (recreate 45.9 s)

Supporting measurements taken live on cx23 @ hel1 on 2026-08-27:

Phase Stock ubuntu-24.04 Golden snapshot
POST /servers returns 0.43 s
Hetzner reports running 14.6 s 24.1 s
sshd accepts 32.3 s 40.9 s
apt-get update 18.3 s skipped
apt-get install docker.io 17.4 s skipped

Two corrections to figures already in the repo:

  • GAPS.md:77 records "VM-ready latency varies 46-71 s". That includes the apt
    work. It is not a boot floor, and reading it as one led two earlier analyses
    to conclude a warm pool was mandatory.
  • GAPS.md:35 records docker install at 9 s. This run measured 35.7 s for apt
    update plus install together.
  • A custom snapshot boots slower than a system image, by about 9 s. The image
    still wins by a wide margin, but that cost is real.

Phase 1 — the volume is the workspace's own disk

Every create on a volume-capable provider makes a 50 GB volume named after the
workspace and attaches it. /var/lib/blitz lives there, which is the docker
store and /workspace both, so a destroyed workspace can come back.

  • The volume goes in Hetzner's server-create volumes field instead of an
    attach call afterwards. This fixes a real race: the bootstrap scans
    /dev/disk/by-id once with no retry, so the old ordering could leave a box
    with no persistent disk and no error to show for it.
  • Workspace names are not unique and Hetzner volume names must be. The
    workspace name is tried first; a name carrying the workspace id is the
    fallback. Both paths fired during the live runs.
  • Destroy detaches and keeps the volume for seven days. A janitor reclaims it
    after that. A volume created through POST /volumes carries no clock and is
    never reclaimed.
  • GET /workspaces/history lists destroyed workspaces whose volume is alive.
    POST /workspaces/:id/recreate brings one back on that volume, and takes an
    optional sshPublicKey.
  • A create that fails after the volume exists deletes it, and so does an
    ownership row that cannot be written.

Phase 2 — HETZNER_SERVER_IMAGES names a golden snapshot per location

  • The bootstrap skips apt when docker and curl are already present, and skips
    the sshd relocation when the image already made it. The stock path is
    untouched, so a plain Ubuntu image still does both.
  • A refused snapshot falls back to stock Ubuntu with a warning. Hetzner answers
    before allocating, so that retry cannot duplicate a server. An ambiguous
    failure is never retried.
  • The image map reaches the deployment credential only. A snapshot lives in one
    project, so a BYOK organization boots stock Ubuntu.
  • scripts/bake-golden-image.mjs builds the snapshot. It shares the emitted
    image-setup bash and its shell helpers with the bootstrap rather than copying
    them, so the two sides of that contract cannot drift.

Five defects the live runs exposed, all fixed here

None were visible to unit tests.

  1. The bake hung for 30 minutes. The emitted image setup calls retry,
    which the bootstrap defines in its own preamble. The bake embedded the setup
    without it, so the builder died under set -e and never powered off.
  2. Recreate failed with "volume already attached". A Hetzner detach is an
    async action; detachVolume fired it and returned, so destroy reported
    success while the volume was still attached.
  3. A recreated workspace had no authorized SSH key. The row never stored
    one, so the restored box came up with no way in.
  4. The golden image expired root's password. Undoing it once does not hold;
    cloud-init re-expires on every clone, so the bake disables that default.
    Host debuggability only — workspaces were never affected.
  5. The bootstrap had no phase markers, flagged in GAPS.md:36 since
    2026-08-13. blitz_phase now logs seconds since start at four points.

What this does not do

It does not reach 30 s. About 27 s is Hetzner allocation and boot, which no
image can remove, and about 18 s is bootstrap tail. A warm pool of pre-booted
VMs is the only path below 30 s, and it was ruled out on cost: an idle slot
bills about $14.76 a month.

Two of the three speed levers paid nothing and are kept only on their merits:
disabling unused units moved boot to sshd by 0.4 s, inside noise, and shrinking
the snapshot reclaimed 11 MB, because the box image is the snapshot.

Not covered by the live runs: the seven-day janitor was not time-travelled on
real resources, and the BYOK path, arm64 and the hil location were not
exercised. The bake script does not verify its own snapshot; this one was
verified by booting a workspace on it.

Cost

Read from GET /v1/pricing on the deployment account (USD, VAT 0).

Item Rate Result
Volume $0.0767 / GB / month 50 GB = $3.84 per workspace per month
Seven-day retention tail $0.89 per destroyed workspace
Snapshot $0.0199 / GB / month $0.05 per month, fleet-wide

Gates

npm run typecheck, npm run lint:gate at the 102 anti-slop baseline with
max-lines back to 7 (hetzner.ts split into compute/hetzner-config.ts), 604
control-plane tests, and the python, bash and scripts conformance suites. Seven
new tests. Extracting boxImageSetupScript and BOX_IMAGE_SETUP_HELPERS out of
bootstrap.ts is a pure refactor: all 44 pinned bootstrap-byte tests pass
unchanged.

To turn Phase 2 on after merge

Snapshot 425047509 (hel1, 2.61 GB) is already baked and kept. Set
HETZNER_SERVER_IMAGES=hel1=425047509. Until that var is set, every create
takes the stock path and behaves exactly as it does today.

Note the live snapshot predates fix 4 above, so a rebake
(npm run golden:bake -- --location hel1) is worth doing before relying on host
SSH for debugging.

Review notes

  • migrations/0036_workspace_volumes.sql adds auto_created, detached_at
    and workspace_id to volume_ownership.
  • The bootstrap changes emitted bytes, which are a pinned contract.
    test/bootstrap-bash.test.mjs now slices at the docker-presence guard,
    because slicing at apt_watchdog update would cut the enclosing if in half.
  • test/org-compute-credentials.test.ts needed its fake to hand out unique
    volume ids and to answer the volume lifecycle. Workspaces now carry a volume
    on every cloud create, so the destroy path detaches one every time.

🤖 Generated with Claude Code

… image

Two changes that share one goal: a workspace should survive its VM, and a
new one should not pay for work a disk image can already hold.

Phase 1 — the volume is now the workspace's own disk.

Every create on a volume-capable provider makes a 50 GB volume named after
the workspace and attaches it. `/var/lib/blitz` lives there, which is the
docker store and `/workspace` both, so a destroyed workspace can come back.

- The volume goes in Hetzner's server-create `volumes` field instead of an
  attach call afterwards. The bootstrap scans /dev/disk/by-id once with no
  retry, so the old ordering raced that scan and could leave a box with no
  persistent disk and no error to show for it.
- Workspace names are not unique and Hetzner volume names must be, so the
  workspace name is tried first and a name carrying the workspace id is the
  fallback.
- Destroy detaches and keeps the volume for seven days. A janitor reclaims
  it after that. A volume the operator made through POST /volumes carries no
  clock and is never reclaimed.
- GET /workspaces/history lists destroyed workspaces whose volume is alive.
  POST /workspaces/:id/recreate brings one back on that volume.
- A create that fails after the volume exists deletes it. So does an
  ownership row that cannot be written. Neither may leave a monthly bill
  nothing can reach.

Phase 2 — HETZNER_SERVER_IMAGES names a golden snapshot per location.

Measured on cx23@hel1 on 2026-08-27, a stock boot spends 18.3 s on apt
update, 17.4 s installing docker, and about 58 s downloading and loading the
box image. A snapshot that already holds all three removes that work.

- The bootstrap skips apt when docker and curl are already present. The
  stock path is untouched, so a plain Ubuntu image still installs them.
- A refused snapshot falls back to stock Ubuntu with a warning. Hetzner
  answers before allocating, so the retry cannot duplicate a server. An
  ambiguous failure is never retried.
- The image map reaches the deployment credential only. A snapshot lives in
  one project, so a BYOK organization boots stock Ubuntu.
- scripts/bake-golden-image.mjs builds the snapshot. It shares the emitted
  image-setup bash with the bootstrap rather than copying it, so the two
  sides of that contract cannot drift.

Gates: typecheck, lint ratchet at the 102 baseline with max-lines back to 7
(hetzner.ts split into hetzner-config.ts), 604 control-plane tests, and the
python/bash/scripts conformance suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pythonlearner1025
pythonlearner1025 marked this pull request as draft August 27, 2026 16:47
@pythonlearner1025

Copy link
Copy Markdown
Member Author

Do not merge yet — not validated against real Hetzner.

Every gate in this PR is unit-level. No part of the golden-snapshot path has run against real infrastructure:

  • scripts/bake-golden-image.mjs has never executed. The account holds zero snapshots.
  • The golden image boot and the bootstrap apt skip have never run on a real snapshot.
  • The volumes field in POST /servers has never been exercised live.
  • The 50 GB auto-create and its mount at /var/lib/blitz have never run live.
  • The retention janitor, history list and recreate route have unit tests only.
  • The 40-55 s create-to-ready figure is derived from a boot probe, not measured on this code.

The only live measurement behind this PR is a boot-floor probe on a stock ubuntu-24.04 cx23. It measured the baseline and exercised none of the new code.

Moving to draft until a real create, destroy and recreate cycle passes on real Hetzner resources.

Ran the full cycle against real Hetzner on 2026-08-27: bake a snapshot, create
a workspace on it, write a marker into /workspace, destroy, recreate on the
surviving volume, read the marker back. Each of these stopped that cycle.

1. The bake hung for thirty minutes.

   `boxImageSetupScript` emits a call to `retry`, which `buildBootstrapScript`
   defines in its own preamble. The bake script embedded the setup without it,
   so the builder died on "retry: command not found" under `set -e` and never
   powered off. The bake then waited for a shutdown that could not come.

   `BOX_IMAGE_SETUP_HELPERS` now holds that helper, the bootstrap emits it from
   there, and the bake script emits it too. One source, both callers. The
   builder also traps ERR and powers off, so a broken bake costs two minutes
   instead of thirty.

2. Recreate failed with "volume already attached".

   `detachVolume` fired the detach and returned. A Hetzner detach is an async
   action, so destroy reported success while the volume was still attached, and
   an immediate recreate on it was refused. It now polls until no server holds
   the volume. A timeout returns quietly: the janitor retries, and a destroy
   must not wedge on a volume Hetzner is slow to release.

3. A recreated workspace had no authorized SSH key.

   The workspace row never stored one, so the restored box came up with no way
   in and the owner could not reach their own restored disk. `POST
   /workspaces/:id/recreate` now takes an optional `sshPublicKey`. An empty
   body still restores the workspace as it was. The key belongs to whoever is
   asking, not to the row being restored.

Evidence from the passing run: snapshot 425005770 (2.62 GB), volume
`e2e-golden` 50 GB in hel1 attached during server create, create to ready
69.9 s, apt skipped on the golden image, /var/lib/blitz on /dev/sdb, volume
survived the destroy, recreate to ready 68.4 s, and the marker file read back
intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pythonlearner1025
pythonlearner1025 marked this pull request as ready for review August 27, 2026 17:30
@pythonlearner1025

Copy link
Copy Markdown
Member Author

Validated against real Hetzner — full cycle passes

Ran the complete cycle on real resources on 2026-08-27, through the branch's control plane (run locally behind a cloudflared tunnel; a branch is never deployed to canary).

1. create           volume e2e-golden, 50 GB, hel1
                    server 163802707 image=425005770 type=snapshot volumes=[106723600]
                    OK: golden snapshot booted, volume attached during create
2. ready            69.9 s
3. marker           wrote /workspace/e2e-marker-1787851244212
4. golden image     apt skipped: true
                    /var/lib/blitz -> /dev/sdb  49G  464K  47G  1%
5. destroy          HTTP 200, volume 106723600 survived, server=null
6. history          GET /workspaces/history lists it
7. recreate         same volume, ready in 68.4 s
8. marker survived  nonce-cq7kss2g8v6 read back intact

create to ready: 69.9 s, down from the 130.6 s baseline. The golden snapshot is 2.62 GB and costs about $0.05 a month.

The run found three real defects, now fixed in b5d0533

  1. The bake hung for 30 minutes. boxImageSetupScript emits a call to retry, which the bootstrap defines in its own preamble. The bake embedded the setup without it, so the builder died on retry: command not found under set -e and never powered off. Both callers now emit BOX_IMAGE_SETUP_HELPERS, and the builder traps ERR so a broken bake costs two minutes.
  2. Recreate failed with "volume already attached". A Hetzner detach is an async action; detachVolume fired it and returned, so destroy reported success while the volume was still attached. It now polls until no server holds the volume.
  3. A recreated workspace had no authorized SSH key. The row never stored one, so the restored box came up with no way in. POST /workspaces/:id/recreate now takes an optional sshPublicKey.

None of these were visible to unit tests. All three are now fixed and the cycle passes end to end.

Gates after the fixes

typecheck, lint ratchet at the 102 baseline, 604 control-plane tests, python 1/1, bash 4/4, scripts 59/59.

State left behind

Snapshot 425005770 (hel1) is kept so HETZNER_SERVER_IMAGES=hel1=425005770 can be set when this lands. Every test server and volume is deleted.

Create to ready falls from 64 s to 45.3 s, measured on real Hetzner across two
full cycles. Against the 130.6 s stock baseline that is a 65% cut.

Three levers went in together; only one paid.

- **Bake the sshd move (the win).** The bootstrap moved host sshd to 2222 and
  waited for port 22 to free on every boot. A golden image can hold that state
  already, so the bake writes the port drop-in and masks ssh.socket, and the
  bootstrap skips the whole block when a listener is on 2222 and 22 is free.
  Worth about 19 s, well over the 5-10 s estimated.
- **Disable unused units (no measurable gain).** snapd, unattended-upgrades,
  multipathd and the apt timers are off in the image. Boot to sshd moved from
  40.9 s to 41.3 s, inside the run-to-run noise. Kept because a workspace never
  uses them, not because it is faster.
- **Shrink the snapshot (no gain).** Reclaiming apt lists, docs and caches took
  2.6157 GB to 2.6050 GB. The box image is the snapshot, so there is nothing
  else to give back. Estimated 2-5 s; delivered none.

Also: phase markers. `blitz_phase` logs seconds since the script started at the
apt, volume, sshd and image points. tools/e2e/GAPS.md has flagged their absence
since 2026-08-13, and without them the tail could only be attributed by
subtraction, which made every tuning decision an estimate.

Also: root's password is no longer expired in the golden image. Hetzner expires
it on an image built without an SSH key, and undoing that once does not hold —
cloud-init re-expires it on every clone, so the bake disables that default
instead. This is host debuggability only; workspaces were never affected,
because cloud-init runs the bootstrap as root rather than over SSH.

Verified end to end on snapshot 425047509: create 45.3 s, marker written to
/workspace, destroy, recreate 45.9 s on the surviving volume, marker read back
intact.

Remaining budget is about 27 s of Hetzner allocation and boot, which no image
can remove, and about 18 s of bootstrap tail. Under 30 s still needs a warm
pool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pythonlearner1025

Copy link
Copy Markdown
Member Author

Speed pass: 130.6 s → 45.3 s, measured

Re-baked and re-ran the full cycle on real Hetzner (snapshot 425047509).

create → ready
Stock baseline (GAPS.md:35) 130.6 s
Golden image, before this pass 64 s (57.7 / 61.8 / 69.9 / 68.4)
Golden image, after this pass 45.3 s (recreate 45.9 s)

Three levers went in together. Only one paid, and I am reporting the other two honestly rather than claiming the whole 19 s.

  1. Bake the sshd move — the win, ~19 s. The bootstrap relocated host sshd to 2222 and waited for port 22 to free on every boot. The image now holds that state, and the bootstrap skips the block when a listener is on 2222 and 22 is free. Estimated 5-10 s; delivered about 19 s.
  2. Disable unused units — no measurable gain. Boot to sshd went 40.9 s → 41.3 s, inside run-to-run noise. Kept because a workspace never uses snapd or the apt timers, not because it is faster.
  3. Shrink the snapshot — no gain. 2.6157 GB → 2.6050 GB. The box image is the snapshot; there was nothing to reclaim. Estimated 2-5 s; delivered none.

Two supporting fixes

  • Phase markers. blitz_phase now logs seconds since start at the apt, volume, sshd and image points. GAPS.md:36 has flagged their absence since 2026-08-13; without them the tail could only be attributed by subtraction.
  • Root password expiry in the image. Hetzner expires root's password on an image built without an SSH key, and undoing it once does not hold — cloud-init re-expires on every clone, so the bake disables that default. Host debuggability only; workspaces were never affected.

Measured, and a correction to an earlier claim

A golden snapshot boots slower than a stock system image: 24.1 s vs 14.6 s to running, 40.9 s vs 32.3 s to sshd. Hetzner materializes a custom snapshot more slowly. The image still wins by far, but that ~9 s cost was not in my earlier estimate.

Where the remaining 45 s goes

About 27 s of Hetzner allocation and boot, which no image can remove, and about 18 s of bootstrap tail. Under 30 s still needs a warm pool, which was ruled out on cost.

State

Snapshot 425047509 is live and kept: set HETZNER_SERVER_IMAGES=hel1=425047509. The previous snapshot is deleted. Every test server and volume is gone; the 8 pre-existing workspace servers and their 2 volumes are untouched.

Two loose ends from the speed work.

The snapshot id is now set in `canary.yml` beside
`BLITZ_DEPLOY_VAR_CLOUD_WORKSPACE_CREDENTIAL_POLICY`, which is where the other
per-deployment non-secret settings already live. It was never a secret — it is
a plain [vars] entry — so nobody should have to set it by hand after a merge.
A rebake changes the value in one place, in version control.

It cannot be a default in code. `DEFAULT_HETZNER_MACHINE_TYPES` holds names
Hetzner offers to everyone; a snapshot id exists in exactly one project, so a
hardcoded default would make every self-hoster and every BYOK org attempt an
image they cannot see. The adapter would fall back correctly, and log a warning
on every create forever.

`plans/SUBSCRIPTION-COMPUTE.md` covers the second loose end. The golden image
reaches the deployment credential only, and both hosted deployments run
byok-required, so today it reaches nobody. The intended product behaviour — a
subscribed org runs on our infrastructure — is not implemented: the credential
policy is one deployment-wide env var with no per-org lookup, and the billing
seam can write only `seat_limit` and `orgs.vm_limit`, so the billing service has
no way to say "this org may use our key".

The plan closes that with one integer in the entitlements seam and one branch in
`resolve()`, keeping the rule that core never learns a plan name. It also states
what a lapse must not do: `compute_credential_source` is pinned per row, so a
downgrade refuses the next create and never touches a running workspace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pythonlearner1025

Copy link
Copy Markdown
Member Author

The snapshot id no longer needs setting by hand

2d38929 pins it in canary.yml, beside BLITZ_DEPLOY_VAR_CLOUD_WORKSPACE_CREDENTIAL_POLICY:

BLITZ_DEPLOY_VAR_HETZNER_SERVER_IMAGES: hel1=425047509

It was never a secret — it is a plain [vars] entry, and this is where the other per-deployment non-secret settings already live. Merging now deploys it. A rebake changes one line, in version control.

It deliberately is not a default in code. DEFAULT_HETZNER_MACHINE_TYPES holds names Hetzner offers everyone; a snapshot id exists in exactly one project. A hardcoded default would make every self-hoster and every BYOK org attempt an image they cannot see — the adapter falls back correctly, but logs a warning on every create forever.

release.yml gets no such line yet: client prod needs a bake in its own Hetzner project first.

And the reason it is still inert

Both hosted deployments run byok-required, and the golden image reaches the deployment credential only, so today it reaches no customer.

The intended behaviour — a subscribed org runs on platform infrastructure — is not implemented. Verified three ways: the credential policy is one deployment-wide env var read in two places with no per-org lookup (org-credentials.ts:293,402,426); org_entitlements holds only seat_limit, so the billing service can write seats and orgs.vm_limit and nothing else (migrations/0031); and nothing under core/compute/ references a plan, tier or subscription.

plans/SUBSCRIPTION-COMPUTE.md closes that with one migration, one integer in the entitlements seam, and one branch in resolve() — keeping the rule that core never learns a plan name. It also fixes what a lapse must not do: compute_credential_source is pinned per row, so a downgrade refuses the next create and never touches a running workspace.

pythonlearner1025 and others added 4 commits August 28, 2026 00:04
# Conflicts:
#	packages/control-plane/test/core-imports.test.ts
Main gained 0036_box_token_grace, 0036_template_repo_private and
0037_workspace_repos while this branch was open, so 0036_workspace_volumes
collided with two of them. It is 0038 now.

The only merge conflict was the module count in core-imports.test.ts: both
sides added modules. Resolved by counting the real files (99), not by picking
a side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught what my local gates could not. blitzdev-emitter.test.ts is
vendor-only and skips unless BLITZDEV_MANAGED=1, which CI sets and I did not,
so the emitted-file manifest went unchecked through four green local runs.

core/workspace-volumes.ts and core/compute/hetzner-config.ts are emitted by the
managed build, so both belong in the pinned list, in CORE_MANIFEST order. The
file count moves to 102.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(entitlements): let a subscribed org run on platform compute

A paying organization can now create cloud workspaces without bringing its
own Hetzner key. That is also the only way the golden image ever reaches a
customer: a snapshot lives in one project, and until now every hosted create
resolved to a project we do not own.

One integer, not a plan name. `org_entitlements.platform_compute` is 0 or 1
and the billing service translates its plan into it through the existing
`PUT /orgs/:id/entitlements`. A plan name in core would be the first crack in
the seam migration 0031 built, and the enforcement here would then depend on
a string only the billing side can define. The column is appended by 0037 with
`DEFAULT 0`, so a missing row and a written 0 say the same thing — the same
reasoning 0031 uses for `seat_limit`.

An organization credential still wins. `resolve()` reads the entitlement only
after the org-credential branch has already returned, and only under
`byok-required`: a team that adds its own key keeps using it while subscribed,
which preserves the strict source pinning that stops a deployment key from
touching an organization's resources. Under `deployment-fallback` the resolver
has already fallen back, so the extra lookup would buy nothing and is skipped.

`providerStatuses()` learned the same flag, which the plan did not call for.
Without it a subscribed organization would pass `resolve()` and still be shown
an empty machine catalog, because `/machine-types` excludes any provider whose
access reads `credential-required`. The refusal would then happen one screen
earlier than the resolver decides it, and for a reason that no longer holds.

A lapse never touches a running workspace. Writing the flag back to 0 only
refuses the next create: every workspace row pins `compute_credential_source`,
so destroy and the janitors keep resolving the key that made the VM. Sweeping
running VMs on downgrade would delete a customer's work to collect a bill. The
spend ceiling stays `orgs.vm_limit`, already enforced inside the create
transaction, so no new cap is introduced.

The write body is now parsed into a named `EntitlementsRequest` on both sides
of the wire contract rather than field by field, because the house rule is that
external data becomes a named type at the boundary. `platformCompute` is
optional there and absent means 0: the body states an organization's whole
entitlement, so a billing service that omits the flag is saying the
organization does not have it, exactly as a missing row does. Treating absent
as "leave alone" would make one write mean two different things.

`GET /orgs/:id/usage` reports the flag unconditionally, so an admin reading
that page can see why a create was refused or allowed instead of inferring it.

Fixtures gain a subscribed case on both the write and the usage read, and a
rejected case for a non-boolean flag, because the billing service pins the same
corpus and a field renamed on one side has to fail in a test rather than in a
checkout page nobody can reach. `corpus.sha256` was regenerated — the
checked-in value is not reproducible from any plain reading of its own README,
so the README now states the exact recipe that produces the new one.

The blitzdev teenybase schema mirrors the new column so the managed deployment
does not drift from the migration.

The plan's webapp string is deliberately not implemented. It asked the 402 to
say "your plan includes compute" instead of "add a credential", gated on the
usage field — but an organization with the flag set never receives that 402,
and one without it has no such plan. The branch would be unreachable in any
correct configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe6oN7cZ37CLcoMGb7147s

* docs(plan): amend SUBSCRIPTION-COMPUTE where the build proved it wrong

Two corrections the implementation earned.

The webapp string is struck: the branch cannot be reached. An org with the flag
never sees the 402, and an org without it has no plan to name.

providerStatuses() is added: /machine-types drops any provider whose access
reads credential-required, so a subscribed org would have passed resolve() and
still seen an empty machine catalog. The plan missed it; the build found it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: merge the updated base, and renumber the entitlements migration

Main gained 0037_workspace_repos while this branch was open, and the volumes
branch moved its own migration to 0038. platform_compute is 0039 now, and the
plan's schema section names the new number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants