carlos / platform Public

docs: stage 1-2 rollout notes

Task 8 of the multi-region writer-map-and-certs plan: mark spec Part 1/2
rows implemented-dormant with commit refs, cross-reference the
bucket-writer audit doc, and fill the plan's rollout notes from the SDD
ledger — old-key deletion commands for both deployment buckets, the
teamtito runbook PR requirements, the T4/T5 gate-window pairing rule, the
grandfathered-fleet-box env drop, the TimeoutStopSec check, the
retention-coverage narrowing and its future fix ordering, #241's landing
gate, the tls-alpn-01 e2e residual, and the three T8-tagged
issue-worthy observability gaps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev c4ba8447671510e0c0611839082d0965bdf12c2f parent f7e67fe
2 files changed, +197 −9
  • docs/superpowers/plans/2026-08-20-multi-region-writer-map-and-certs.md +160 −4
  • docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md +37 −5
diff --git a/docs/superpowers/plans/2026-08-20-multi-region-writer-map-and-certs.md b/docs/superpowers/plans/2026-08-20-multi-region-writer-map-and-certs.md
index 4298c70a1..ce724a806 100644
--- a/docs/superpowers/plans/2026-08-20-multi-region-writer-map-and-certs.md
+++ b/docs/superpowers/plans/2026-08-20-multi-region-writer-map-and-certs.md
@@ -546,7 +546,163 @@ git commit -m "edge: e2e — issuer mints, every edge serves cert and challenge"
## Rollout notes (filled by Task 8 at execution)
-- Old-key deletion commands: (recorded at execution)
-- Runbook PR: (recorded at execution)
-- Issue comments: #158 close, pinfra #15 unblock, #207 interaction —
- per-box keys change the generation-window arithmetic note.
+Synthesized from the SDD ledger (`progress.md`) across all seven
+implementation tasks and their reviews. This plan ships stages 1-2
+dormant; nothing below is armed by merging this branch — it is the
+checklist for the rollout that follows.
+
+1. **Delete the old fixed snapshot keys — only after BOTH deployments run
+ this build.** Task 1 (`b466af4`) replaced the fixed
+ `control/registry-snapshot.{db,json}` + `control/registry-snapshots/`
+ keys with per-box ones; the old keys are abandoned in place, not
+ migrated, and stay live in both deployment buckets until every box in
+ both deployments has converged onto a release containing this branch
+ (verify each box has written its own `control/registry-snapshot/<box>.db`
+ before deleting the fixed keys — there is no CLI/console surface for
+ this today, so it's a direct-bucket check, consistent with CLAUDE.md's
+ carve-out for platform-internal infra work). Exact commands, one pair
+ of buckets (flagship profile `keymail`, tito profile `titogo` —
+ coordinates per CLAUDE.md/session memory):
+
+ ```bash
+ # flagship (carlos-flagship-271376211898)
+ aws --profile keymail s3 rm s3://carlos-flagship-271376211898/control/registry-snapshot.db
+ aws --profile keymail s3 rm s3://carlos-flagship-271376211898/control/registry-snapshot.json
+ aws --profile keymail s3 rm s3://carlos-flagship-271376211898/control/registry-snapshots/ --recursive
+
+ # tito (carlos-tito-platform-665106592142)
+ aws --profile titogo s3 rm s3://carlos-tito-platform-665106592142/control/registry-snapshot.db
+ aws --profile titogo s3 rm s3://carlos-tito-platform-665106592142/control/registry-snapshot.json
+ aws --profile titogo s3 rm s3://carlos-tito-platform-665106592142/control/registry-snapshots/ --recursive
+ ```
+
+2. **teamtito runbook PR is owed at that same moment** (Task 1 ledger).
+ `teamtito/carlos-infrastructure/RUNBOOK-edge-failover.md` parses the
+ snapshot keys and must land the key-shape update in the same window as
+ the deletion above, not before (old runbook still needs to name the old
+ keys until the deletion happens) and not long after (a runbook naming
+ dead keys is a live footgun during a restore). Two content requirements
+ beyond the key rename, both from the ledger:
+ - **Step 0 ordering rule**: restore-before-agent-start /
+ machine-id-reuse — a rebuilt standby must be restored from the dead
+ box's snapshot BEFORE the agent starts and mints a fresh machine id,
+ or it seeds its own empty registry under a new box label instead of
+ inheriting the dead box's key.
+ - **The agent-log "OVERWRITING a snapshot written by box …" line is NO
+ LONGER a clobber signal** for a runbook reader once keys are per-box:
+ it now only fires for a same-named box reusing a dead box's label
+ (the intended restore path), not for two different boxes colliding —
+ that collision is structurally impossible post-Task-1. The runbook
+ should say so explicitly so an operator doesn't panic at a line that
+ is now expected, not alarming.
+ - Flagship has no DR runbook today — this gap is recorded in the spec,
+ not hidden; the teamtito PR only touches tito's runbook.
+
+3. **Never deploy Task 4's issuer gate without Task 5's mirror.** The
+ HostPolicy gate (`errNotIssuer` on non-issuers) and the cert/challenge
+ mirror are two halves of one mechanism — a box running only the gate
+ with no mirror to serve from has a no-TLS window for exactly as long as
+ it isn't the issuer. Roll both in the same build, same deploy. (Task 4
+ T8 rollout rule, promoted from review.)
+
+4. **Drop `CARLOS_DEPLOYMENT_BUCKET` on grandfathered fleet boxes (the
+ Pi) before this rolls.** `internal/edge/certsync.go`'s `armCertIssuance`
+ comment: a fleet box (`CARLOS_FLEET` set) that still carries a
+ deployment-bucket env from before the fleet-placement cutover reads
+ `control/certs/**` directly over its own IAM and is gated + mirrored
+ like any other non-issuer edge — "a transition state, resolved by the
+ rollout dropping that env." Until that env is dropped, the Pi is a real
+ (if inert) reader of cert material outside the intended fleet-door
+ boundary. Task 4 fix-round-1 residual.
+
+5. **Don't let a pubkey-less bucket-backed box become housekeeper.** The
+ host-level housekeeper election (`internal/host/housekeeper.go`) has no
+ visibility into `CARLOS_RELEASE_PUBKEY` — a box with a deployment bucket
+ but no release pubkey CAN win the housekeeper lease at the host layer,
+ while its edge layer (`enableStaticFromEnv`) leaves `staticStore` nil for
+ want of a pubkey, which makes `armCertIssuance` treat it as "no
+ deployment store": it issues and serves its OWN certs unconditionally,
+ outside the gate entirely, orphaned from the shared mechanism — a second
+ uncoordinated ACME order stream running alongside whichever box the
+ deployment actually intends as issuer, defeating the one-elected-issuer
+ invariant. No code fix landed this round (Task 4 fix-round-1 residual,
+ tagged T8); the rollout mitigation is operational: audit every box's env
+ before arming a second housekeeper-eligible box, and exclude (or hand a
+ pubkey to) any bucket-backed box that lacks one.
+
+6. **Check `carlos-agent`'s systemd `TimeoutStopSec` covers the shutdown
+ chain (pinfra).** Housekeeper stand-down now runs AFTER `svc.Shutdown`
+ in `agent.go`'s post-drain block (`stopHousekeeper()` called after the
+ 60s-bounded `svc.Shutdown`), so a full stop can take up to ~80s
+ (60s child-shutdown budget + the housekeeper's own stand-down/lease
+ handling). If the unit's `TimeoutStopSec` is shorter, systemd SIGKILLs
+ mid-stand-down, stranding the housekeeper lease for up to its 15-minute
+ TTL instead of releasing it cleanly. Verify the pinfra unit file before
+ rolling to a second box. (Task 3 review, ledgered.)
+
+7. **Retention coverage narrows to the housekeeper's app set the moment a
+ second box arms.** `retentionPass` protects whatever apps are pinned in
+ the CALLING box's own registry (`internal/host/retention.go`) — today,
+ with one box, that registry is the deployment's whole app set. The
+ instant a second box exists and the housekeeper lease can move to it,
+ prune coverage narrows to whichever box currently holds the lease and
+ its own registry's view, not the deployment-wide union. Protection
+ never gets WEAKER (idempotent, keep-newest-N, fails toward keeping
+ data) but coverage can miss apps the current holder's registry doesn't
+ know about. Future fix, ordering matters: **union the pins across every
+ box's registry BEFORE enumerating the bucket for deletion candidates**,
+ not after — this is a real fix owed before arming a second box's
+ housekeeper eligibility in production, not merely a documentation note.
+ (Task 3 review, "concern-1" restated for rollout.)
+
+8. **#241 (restore-verify per-box keys) must land before a second platform
+ box arms `carlos-hibernate-verify.timer`.** The bucket-writer audit
+ found `control/restore-verify.json` is a single fixed key whose
+ `Merge` step deterministically drops any host not in the CALLING box's
+ own fleet view on every publish — not a race, a guaranteed clobber the
+ moment two boxes with different hibernating-instance sets both publish.
+ Ruled per-box keys (`control/restore-verify/<box>.json`, snapshots'
+ shape); implementation deferred to #241, gate: land it before stage 5
+ (arming a second box). Note the timer being INSTALLED on every box
+ (`internal/host/units/carlos-hibernate-verify.timer`) is not the same as
+ it being ARMED/enabled — it's not in the bootstrap `enabledUnits` set
+ today, which is the margin that makes "land #241 before a second box
+ arms it" an operational gate rather than an already-blown deadline.
+
+9. **tls-alpn-01 is not end-to-end proven — residual, not a blocker.**
+ Task 7's two-edge e2e (`f7e67fe`) proves challenge-anywhere over
+ http-01 only; tls-alpn-01 is autocert's PREFERRED challenge type, so
+ the headline "challenge-anywhere e2e-proven" claim is true only of the
+ second-choice path in production. Real unit coverage exists — four
+ named tests in `internal/edge/certsync_test.go`:
+ `TestMirrorAnswersAnotherEdgesALPNValidation`,
+ `TestMirrorALPNReadThroughOnlyForServedHosts`,
+ `TestMirrorNeverServesATokenCertToAnOrdinaryHello`,
+ `TestALPNTokenCertIsReadLiveNotFromTheSnapshot` — but not an e2e path.
+ Carried forward rather than looped on; worth an e2e pass before or
+ during stage 5 (arming flagship AU).
+
+10. **Issuer observability gap: which box holds the housekeeper lease has
+ no readout beyond one agent log line.** `internal/host/housekeeper.go`
+ logs `"agent: housekeeper: lease acquired — this box runs the
+ deployment's global duties"` on acquisition and nothing else durable —
+ no status-heartbeat field, no CLI/console surface. An operator asking
+ "which box is minting certs right now" today has to grep agent logs on
+ a specific box, which is exactly the box-local-only gap CLAUDE.md's
+ operate-via-API-and-CLI rule calls out. Task 4's review flagged three
+ issue-worthy items together, all tagged (T8), none filed as numbered
+ issues yet — the controller should decide whether to file them before
+ or alongside the stage-2 rollout:
+ - which-box-is-issuer observability (a `held` bool on the status
+ heartbeat) — the one described above;
+ - `domains` package's `hostCertExpiry` reads blank on non-issuer boxes
+ (it reads the local `DirCache` directly — `internal/domains/reconcile.go:446,452,724`
+ — which is empty on a box that never issues locally; it needs to
+ consult the mirror too);
+ - spent ACME-challenge token litter has no TTL sweep on the mirror
+ side — tokens are deleted from the bucket after validation by the
+ issuer, but a crashed order's token has no independent expiry check
+ from the mirror's perspective.
+
+**Issue comments** (drafted here; the controller posts them — see the
+task-8 report for the exact text of each).
diff --git a/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md b/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md
index 7de41a805..20ce37f69 100644
--- a/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md
+++ b/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md
@@ -55,20 +55,35 @@ deployment-global get a lease.** The status publisher's CAS-lease is the
one election mechanism, reused; no second election pattern may be
introduced.
+**Implementation status (2026-08-20): implemented, dormant.** Per-box
+snapshot keys (commits `b466af4`, `02f449a`) close #158 and retire the
+pinfra #15 blocker. The housekeeper lease (commits `45a2ea6`..`aadd18e`)
+moves retention prune behind a single elected holder, reusing
+`internal/lease.Acquire` — no second election mechanism was introduced.
+Both are inert on today's single-box deployments. Full bucket-writer
+audit: `docs/superpowers/specs/2026-08-20-bucket-writer-audit.md`
+(commits `3f88049`..`a4bb6d6`). Old fixed snapshot keys remain live in
+both deployment buckets until both run this build — deletion commands are
+in the plan's rollout notes, not code.
+
| Prefix | Writer today | Ruling |
|---|---|---|
| `apps/**`, `domains/*`, `control/serial`, restart-gen | console | unchanged — console stays the single control-plane writer, homed on one box; remote edges backhaul console traffic to it |
-| `control/registry-snapshot*` (fixed keys) | any platform box — **#158** | **per-box keys**: `control/registry-snapshot/<box>.db` + `<box>.json`; generations `control/registry-snapshots/<box>/<stamp>.db`. Every box snapshots its own registry. Closes #158; retires the pinfra #15 blocker |
-| `status/**` | lease-elected publisher | unchanged; verify the lease is deployment-scoped (it is bucket-scoped, which is the same thing) |
-| `releases/**` retention prune | every box | moves behind the **housekeeper lease** — five concurrent pruners racing a promote is a bug class we refuse to have |
-| usage metering day objects | the (single) edge | **audit**: if day-object keys are not box-scoped, they become `…/<box>/…` in the same shape as snapshots |
-| `control/certs/**`, `control/acme-challenges/**` (new) | housekeeper | Part 2 |
+| `control/registry-snapshot*` (fixed keys) | any platform box — **#158** | **per-box keys**: `control/registry-snapshot/<box>.db` + `<box>.json`; generations `control/registry-snapshots/<box>/<stamp>.db`. Every box snapshots its own registry. Closes #158; retires the pinfra #15 blocker. **Implemented, dormant** (`b466af4`, `02f449a`) |
+| `status/**` | lease-elected publisher | unchanged; verify the lease is deployment-scoped (it is bucket-scoped, which is the same thing). **Audited**: `internal/lease`'s `status-lease` election confirmed deployment-scoped (bucket-writer audit §1) |
+| `releases/**` retention prune | every box | moves behind the **housekeeper lease** — five concurrent pruners racing a promote is a bug class we refuse to have. **Implemented, dormant** (`45a2ea6`..`aadd18e`) |
+| usage metering day objects | the (single) edge | **audit**: if day-object keys are not box-scoped, they become `…/<box>/…` in the same shape as snapshots. **Confirmed already box-scoped** (`usage/edge/<box>/days/`, bucket-writer audit) |
+| `control/certs/**`, `control/acme-challenges/**` (new) | housekeeper | Part 2 — **implemented, dormant** (`7eff14b`..`f7e67fe`) |
| `control/edges/<box>.json` (new) | each box, own key only | Part 3 |
| `control/edge-mesh-key` (new) | housekeeper mints if absent (create-only CAS) | Part 3 |
Stage-1 audit deliverable: an enumerated list of EVERY bucket writer in
the codebase with its ruling from the table above, checked into the spec's
plan; anything discovered outside this table gets a ruling before stage 2.
+**Done**: `docs/superpowers/specs/2026-08-20-bucket-writer-audit.md`
+(2026-08-20). One writer found outside this table —
+`control/restore-verify.json` — is RULED per-box keys, deferred to issue
+#241 (must land before a second platform box arms the verify timer).
**Snapshot key migration** is a storage-shape change; the consumer list is
what PARSES it: the DR runbook (teamtito/carlos-infrastructure
@@ -100,6 +115,23 @@ the blast radius it insures against. Certs never appear in data buckets.
## Part 2 — certificates
+**Implementation status (2026-08-20): implemented, dormant.**
+Housekeeper-gated issuance publishing certs and tokens to the bucket
+(commits `7eff14b`, `7187b4d`); the edge cert mirror, `GetCertificate`
+layering and challenge-anywhere serving (commits `eea3f60`, `eb6332a`);
+the door ACL tripwire proving cert material never transits the fleet
+door (`8e43989`); the two-edge e2e proving mint-on-one/serve-on-the-other
+plus challenge-anywhere over http-01 (`f7e67fe`). Single-box deployments:
+the one box always holds the housekeeper lease, so behaviour is identical
+to today. Residual: tls-alpn-01 (autocert's PREFERRED challenge type) is
+NOT end-to-end proven — only http-01 is; tls-alpn-01 has real unit
+coverage (`TestMirrorAnswersAnotherEdgesALPNValidation`,
+`TestMirrorALPNReadThroughOnlyForServedHosts`,
+`TestMirrorNeverServesATokenCertToAnOrdinaryHello`,
+`TestALPNTokenCertIsReadLiveNotFromTheSnapshot` in
+`internal/edge/certsync_test.go`) but not the e2e proof — tracked as a
+residual, not a blocker (see the plan's rollout notes).
+
**Issuer.** The housekeeper runs today's issuance machinery unmodified:
per-host ACME HTTP-01, DNS-01 wildcards, the delegation zone
(`CARLOS_ACME_DELEGATION_ZONE`), and the HostPolicy rulings from PR #180