writeonce/docs/superpowers/specs/2026-08-03-blue-green-vm-design.md
shoney.arickathil c0b0dbb846 docs: audit all markdown against the code, fix findings, flatten status folders
- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet
  available"; "no package manager" contradicted [deps]; the deps example
  would not have compiled (the key IS the module name)
- runtime/README: leads with wovm, wo-rt.c demoted to a historical section;
  dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18
- employee + log-watcher READMEs claimed "does not compile"; both are gates
- error catalog: +10 emitted codes incl WO-E250, the only diagnostic the
  shipped query surface raises; recorded why the sweep rotted
- language-surface: group-by parses, then the typechecker refuses it
- 00-code-review + 00-link-audit re-run; history kept, not rewritten
- 48 dead Rust-era exploration links de-linked rather than re-pointed (their
  prose names the retired plan by number); successor map -> discarded.md
- 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty
- releasing.md: dropped a --draft step the workflow never had
- new docs/00-doc-audit.md: findings + disposition, incl one row where the
  audit was wrong and the doc it accused was right
- status folders removed: 34 stories flat, status only in frontmatter; 252
  links recomputed from resolved paths; board/board-views/structure retaught
- story 24 -> in-progress, since frontmatter is now the only truth
- new iteration 38: fs mutation verbs + net.connect, the two capability
  families no iteration owned
- new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called
  CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no
  source of randomness at all
- linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:20:22 +02:00

11 KiB
Raw Permalink Blame History

Blue/Green VM deployment — design spec

Date: 2026-08-03 Status: approved (2026-08-03); ⏸ on hold (2026-08-21, developer decision — story iteration 26 carries status: hold); implementation plan deferred until plans 5 + 6 ship Scope: the writeonce runtime's in-process deployment subsystem Supersedes: §5–§6 of docs/plan/exploration/blue-green-vm/00-vision.md

Motivation

The writeonce executable is a systemd service that never stops. It embeds its own source; a developer manages that source remotely through the wo CLI over an exposed management port. An approved change — code and its database schema consequence — compiles and migrates inside the runtime, loads into the idle VM slot, and deploys by an atomic switch. If anything fails, the active VM is untouched. Rollback is the same switch back, because the previous version never left memory.

Decisions locked during brainstorming

Question Decision
Blue/Green semantics Fixed slots; activity alternates. Blue and Green are two physical VM slots; each deploy loads the idle one and switches to it. "Which is live" is status info (wo remote status).
Schema migration Auto-diff, additive-only in v1. Additive changes migrate automatically; destructive changes reject the deploy. Script-based migrations are a recorded later phase.
Management protocol HTTP + JSON + SSE on a loopback management transport, bearer token — hand-rollable on the runtime's own HTTP machinery, curl-debuggable. MCP wrapper for agents is a later layer over the same endpoints.
Edit model Local-first, CLI push. wo remote pull fetches the embedded source; the developer edits locally; wo remote propose pushes a staged proposal. The runtime never hosts a mutable workspace.
Architecture Approach A: management plane as a runtime module + thin CLI. In-process slots; rejected: external deployer daemon (violates two-VMs-in-one-runtime, splits data), self-hosted .wo deploy logic (bootstrap problem — later).

1. Scope & positioning

In scope: two VM slots, the deploy state machine, additive schema migration, the management HTTP+SSE surface, wo remote CLI verbs, the source-in-binary trailer section.

Out of scope (recorded extensions): script-based/destructive migrations, an in-runtime editing workspace, the MCP/agent wrapper, fibers (plan 4 owns them).

Prerequisites: plan 5 (DB engine binding — a catalog to diff), plan 6 (HTTP machinery). This spec is written ahead; its implementation plan is authored when those ship.

2. Runtime anatomy

  • Two fixed VM slots — Blue and Green. One is active: all new requests dispatch into it. The other is standby: the previous version, loaded and warm, the instant-rollback target. Activity alternates on each deploy.
  • VMs own code; the engine owns data. Tables, WAL, subscriptions, and arena slabs live below both slots. A switch swaps the dispatch pointer at the request boundary; in-flight work drains on the old slot under a bounded timeout. No data moves on deploy or rollback.
  • A runtime is not tied to a port. Transports (application HTTP, the management endpoint, future ones) attach at boot; systemd socket activation is supported — the unit owns the sockets, the runtime accepts whatever fds it inherits. The runtime also runs with zero listeners.
  • The binary contains its source. The woc build trailer carries the .wo source tree beside the .wob image. After a successful deploy the runtime rewrites its own trailer (write temp, fsync, rename) so the binary on disk always matches the active slot. wo remote pull serves from this section; there is no "which commit is prod running" question.
  • Always running. The executable maps to a systemd service (Restart=always). Deploys never restart the process. After a crash or reboot, the unit boots the active version from the trailer; the standby slot refills on the next deploy.
  • Recipe-box rule. Each capability here (transports, slots, proposal store, differ, deploy machine) stays a separable runtime module — a custom web framework in later phases composes them; the runtime stays framework-agnostic.

3. Deploy pipeline

States, WAL-logged at every transition:

DRAFT ──propose──► STAGED ──approve──► COMPILING ──ok──► MIGRATING ──ok──► LOADING
                     │                     │fail             │fail            │fail
                     │reject               ▼                 ▼                ▼
                     ▼                  FAILED (active slot untouched, standby unchanged)
                  REJECTED
LOADING ──ok──► HEALTH ──ok──► SWITCHING ──drained──► DEPLOYED
                  │fail            (atomic dispatch swap;
                  ▼                 old active becomes standby)
               FAILED
  • propose — the CLI pushes the changed file set plus the base-version hash it was edited against. A stale base (someone deployed since the pull) rejects at propose time, not after approval. The runtime computes the catalog diff immediately and stores it with the proposal.
  • approve — an explicit CLI verb covering BOTH the code change and the migration plan: wo remote diff shows the source diff and the schema consequence, so approval is informed. Approval triggers the pipeline.
  • compile — in-runtime woc emit against the proposal. Any diagnostic → FAILED; diagnostics stream to the developer; the active slot is never touched.
  • migrate — additive catalog changes apply to the engine (§4), before the switch, while old code still serves. Safe because additive changes are invisible to old code.
  • load + health — the new image passes the standard loader validation battery (a bad image cannot boot), then a smoke subset runs against the idle slot.
  • switch — dispatch-pointer swap; drain with bounded timeout; the old active becomes standby; the trailer rewrites.
  • rollback — wo remote rollback switches back to standby: no compile, no load, code already resident. The schema stays as migrated — additive-only (§4) guarantees the older code runs correctly against it.
  • streaming — every stage's output (compiler diagnostics, migration progress, health results, switch/drain status) streams to the developer as it happens (§5).

Failure semantics, uniform: a failure at any stage leaves the active slot serving and the standby slot unchanged; the proposal lands in FAILED with its stream and WAL trail intact.

4. Migration rules (additive-only v1)

The engine diffs the old catalog against the proposal's catalog at propose time:

Change Verdict
new class or type auto-migrates
new field with a default auto-migrates — backfilled with the default in RAM + WAL
new index auto-migrates (built before switch)
new interface / method / service block auto-migrates (code-only)
new field without a default reject: "add a default"
drop / rename / retype a field reject in v1
drop a class; remove a union variant; flip @gc reject in v1
  • Rejections happen at propose time and name the offending declaration — the developer never waits for an approval to learn the change can't ship.
  • The migration itself is a WAL-logged engine transaction: backfills run per shard and ack like any commit. A crash mid-migration replays or discards the whole migration on boot — never half.
  • The additive-only rule is exactly what makes rollback unconditional: the previous version ignores fields and classes it never knew.
  • Script-based migrations (destructive changes, data transforms, down scripts) are the recorded follow-up phase; nothing in this design blocks them.

5. Management surface & CLI

Endpoints under /manage on the management transport (loopback by default):

Endpoint Verb Purpose
/manage/source GET the embedded source tree (paths + contents) of the active slot
/manage/proposals POST submit a proposal (file set + base hash) → id, catalog diff computed
/manage/proposals/<id> GET proposal state, source diff, schema diff
/manage/proposals/<id>/approve POST approve → pipeline starts
/manage/proposals/<id>/stream GET SSE: stage events, compiler diagnostics, migration progress, health, switch/drain — replayable from the WAL after reconnect
/manage/deploy/rollback POST switch back to standby
/manage/status GET which slot is active, versions (source hashes), drain state, last deploy record

wo CLI verbs mapping 1:1: wo remote pull / status / propose / diff / approve / deploy-log / rollback. propose prints the id; approve attaches to the SSE stream and renders it live — the developer watches compile, migration, health, and switch scroll by; a failure shows the exact diagnostics inline.

6. Security & audit

  • Management transport binds loopback by default; reaching it remotely is an SSH tunnel (the log-watcher posture). Bearer token from the service's environment file — config stays world-readable, the secret does not.
  • The application transport and management transport are separate listeners: app traffic can never reach /manage routes.
  • Approval is a deliberate second step, not implied by propose — and the two verbs can carry distinct tokens later (agent proposes, human approves) without changing the design.
  • Everything is WAL-logged: proposals, diffs, approvals, every stage transition, switches, rollbacks. The deploy history survives crashes like any other committed data and is queryable via /manage/status.

7. Testing

  • State-machine unit tests (runtime): every transition and every failure edge — compile fail, migration reject, load reject, health fail, drain timeout — asserting the active slot is untouched after each.
  • Catalog-differ table tests: one fixture per row of the §4 verdict table, both verdicts.
  • Deploy e2e (corpus harness): boot a fixture app, propose an additive change, approve, assert the SSE stream's stage sequence, assert new code serves and old data survives; then rollback and assert the previous behavior returns with the migrated schema intact.
  • Crash battery: kill the process in COMPILING / MIGRATING / SWITCHING / trailer-rewrite; on reboot the runtime serves the correct version and the WAL shows whole migrations only.
  • Stale-base test: two pulls, one deploys, the other's propose rejects.
  • ASan on the runtime side throughout, as established by the wovm gate.

Success criteria

  1. A fixture app deploys an additive change with zero dropped requests (in-flight drain proven by the e2e harness).
  2. Every failure stage leaves the active slot serving and is visible in the SSE stream and the WAL audit trail.
  3. wo remote rollback restores the previous version in under one second with no compile and no data change.
  4. Kill -9 at any pipeline stage: reboot serves a consistent version; no half-applied migration exists.
  5. The binary's trailer always matches the active slot after DEPLOYED (verified by wo remote pull hash comparison in the e2e).