spec: zero-downtime deploys rev 12 — answers Astra round 11 (recovery uses retained identity, legacy sidecars bound to the serving artifact)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+67
−5
docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md+45 −5docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R11.md+22 −0
CI passed — run details
diff --git a/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md b/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md| index 05bee5e4c..dfc14e673 100644 |
| --- a/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md |
| +++ b/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md |
| @@ -1,6 +1,6 @@ |
| # Zero-downtime deploys: overlap swaps and paced rollout |
| -Status: draft rev 11, 2026-10-07. Discussions #231 (deploy pickup) and #230 |
| +Status: draft rev 12, 2026-10-07. Discussions #231 (deploy pickup) and #230 |
| (per-app restart pace, first-route health step) feed into this. Astra |
| rejected rev 1 with 17 findings, rev 2 with 12 new ones, rev 3 with 7, rev 4 |
| with 3 and rev 5 with 4. The findings files are |
| @@ -121,7 +121,8 @@ names a version, not a manifest hash (`ChannelPointer`, |
| `internal/release/release.go`). A version name can be shipped again with |
| different content after pruning. And root reuses an existing staged |
| directory without comparing it with the canonical artifact |
| -(`adopt.go`'s staging). So before any attempt onto B: |
| +(`adopt.go`'s staging). So before any **adoption** attempt onto B (causes |
| +`release`, `install` or `retry`, onto a build the instance hasn't served): |
| - **The agent compares the staged executable's sha256** with the |
| canonical manifest's artifact digest for the box's platform. |
| - **On a match**, the manifest's `swap` applies to those bytes. |
| @@ -142,8 +143,18 @@ directory without comparing it with the canonical artifact |
| "version name reused with different content, ship under a new name". |
| So a staged copy that something refers to never changes underneath it. |
| -Fallback runs exactly the bytes that passed that instance's health check |
| -(§8.2). |
| + |
| +**Recovery isn't checked against the canonical manifest.** Recovery means |
| +fallback to the good build, worker recycles, and `restart` or `config` |
| +attempts onto the serving build. All of these run an artifact this box |
| +has already verified and run. The good-build record (§8.5) and each |
| +generation's captured identity keep three things: the staged path, the |
| +sha256, and the swap mode that applied when it passed. Recovery checks the |
| +staged file against that **retained digest** and uses that **retained |
| +mode**. A later re-ship of the same name, with a different canonical |
| +manifest, never revokes local recovery. If the retained digest doesn't |
| +match the file, the staged copy has been damaged. The recovery is refused |
| +and reported, which is what any failed wake does today. |
| **If the canonical manifest can't be read, or the digests can't be |
| compared, the attempt runs in `hold`.** Missing or unverifiable policy |
| @@ -408,6 +419,20 @@ retiring generations. Each is tracked until its termination is proven. |
| 8. A legacy co-spawned sidecar (`<host>-sidecar.env`) is restarted by |
| stop-then-start onto the same staged build. |
| +**Every legacy co-spawned sidecar runs its instance's serving artifact.** |
| +Today the sidecar is started from the live symlink, in both the flat and |
| +the nested start (`backend_exec.go`), in four places: |
| +- the wake tail (`activator.go`); |
| +- late attachment; |
| +- its crash supervisor (`StartSidecar`); |
| +- the swap above. |
| + |
| +All four now exec the **current generation's captured staged path**. After |
| +a fallback (§8.2), that is the good build, so the sidecar never runs the |
| +failed build beside its fallback server. Sidecar spawns join the entry's |
| +operation set (§7.4), so a supervisor restart can't race the swap's |
| +sidecar replacement. |
| + |
| There is one successor per entry at a time. |
| ### 7.6 The nested layout |
| @@ -648,7 +673,8 @@ Clearing: |
| ### 8.5 The good build |
| -`GoodVersion` is a new registry column. It is set when a generation on |
| +`GoodVersion` is a new registry column, with two companions: the good |
| +artifact's sha256 and its swap mode at the time it passed (§4.2). It is set when a generation on |
| build B becomes current after passing its health check. For a canary, that |
| means after the settle; for a host with no socket, it means when it is |
| ready (§6.5). It is written with an expected-current condition. Fallback |
| @@ -787,6 +813,13 @@ keep timer names stable across the two builds. Otherwise deploy with |
| - **Restage refusal:** a reused version name with different bytes is not |
| restaged over a copy that a good build or a live generation refers to. |
| It is refused and reported. An unreferenced stale copy is restaged. |
| +- **Recovery identity:** a good build whose version name was re-shipped |
| + with different bytes still falls back, checked against its retained |
| + digest. A damaged staged copy refuses recovery and reports it. |
| +- **Legacy sidecar binding:** after a first-wake fallback, the |
| + co-spawned sidecar runs the good build at the wake tail, after late |
| + attachment, and after a supervised restart. It never runs the failed |
| + installed build. |
| - **Worker identity:** a worker reporting an empty or app-chosen version |
| still recycles onto its captured spawn build, and records `GoodVersion` |
| from that build. |
| @@ -1006,3 +1039,10 @@ answer for each. |
| |---|---------|--------| |
| | 1 | Restaging can overwrite another instance's good build | §4.2: a mismatched staged copy that anything refers to is never overwritten; the reused name is refused on that box and reported | |
| | 2 | A worker's reported version can't identify its executable | §7.9: serving build = the agent-captured spawn build; the reported version is display metadata only | |
| + |
| +### Round 11 |
| + |
| +| # | Finding | Answer | |
| +|---|---------|--------| |
| +| 1 | The canonical digest check forbids the retained good build | §4.2: the canonical check applies to adoption only; recovery checks the retained digest and mode (§8.5) | |
| +| 2 | Legacy co-spawned sidecars run the failed installed build beside the fallback | §7.5: every legacy sidecar spawn execs the current generation's captured path, inside the operation set | |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R11.md b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R11.md| new file mode 100644 |
| index 000000000..8263ed58c |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R11.md |
| @@ -0,0 +1,22 @@ |
| +REJECT |
| + |
| +R10-1: **CLOSED** — §4.2 prohibits overwriting referenced staged artifacts. |
| +R10-2: **CLOSED** — §7.9 uses the agent-captured executable identity for worker recovery and `GoodVersion`. |
| + |
| +No split/removal regression found in the earlier closures covering fallback without repointing, channel-installed failure holds, cold sleepers, `GoodVersion` versus `PrevVersion`, or stage-trigger retransmission. The canonical-digest rule introduces a separate fallback failure below. |
| + |
| +1. **HIGH — §§4.2, 8.2, 8.5: The canonical-digest check can forbid the retained good build.** |
| + |
| + **Code:** [internal/release/release.go:632](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:632) permits re-shipping different bytes under a pruned version name. [internal/release/versions.go:53](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/versions.go:53) identifies the staged executable by account, app and version name. Bucket retention protects installed and previous versions at [internal/host/retention.go:41](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/host/retention.go:41); §8.5 adds good-build protection only to the local sweep. |
| + |
| + **Failure:** H retains good build A while root advances its installed build. A is pruned from the bucket and re-shipped with different bytes. Revision 11 correctly preserves H’s original staged A. But when H subsequently wakes directly onto A because its installed build failed, §4.2 requires comparison against the new canonical A manifest **before any attempt** and refuses the mismatch. Root must refuse to overwrite the pinned copy, so neither retry nor the stage relay resolves it. H cannot boot its intact, previously healthy fallback. |
| + |
| + **Smallest fix:** Distinguish adoption of a canonical release from recovery of an already verified local artifact. Persist the good artifact’s digest and verified policy, and validate fallback against that retained identity. A replacement canonical manifest under the same name must not revoke local recovery. |
| + |
| +2. **HIGH — §§7.5 step 8, 7.9, 8.2: Legacy co-spawned sidecars can execute the failed installed build beside the fallback.** |
| + |
| + **Code:** A successful wake co-spawns its legacy sidecar at [internal/activator/activator.go:1347](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/activator.go:1347). The flat implementation executes the live symlink at [internal/activator/backend_exec.go:1324](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:1324); the nested implementation does likewise at [backend_exec.go:1421](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:1421). Its separate crash supervisor calls `StartSidecar` at [activator.go:2680](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/activator.go:2680). |
| + |
| + **Failure:** Root installs B; B fails its first wake; the instance falls back to A while its symlink remains B. The ordinary wake tail then starts **B’s sidecar against A’s server**. A sidecar requiring B’s API fails its work despite successful HTTP fallback, and every supervised restart selects B again. §7.5 fixes only the sidecar restart after a successful overlap swap; §7.9’s `StartWorker` recycling changes do not cover these legacy paths. |
| + |
| + **Smallest fix:** Bind every legacy sidecar spawn—including initial wake, fallback, late attachment and crash recovery—to its owning instance’s captured serving artifact. Serialize these spawns with the swap’s sidecar replacement through the operation set. |
| \ No newline at end of file |