carlos / platform Public

spec: instance update settings rev 5 — answers Astra S3 (digest-bound evidence for every new install, last_executed written before exec, returning residence)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 1ac76d2c2cc3f243095bf0f1be98039e30b77e7c parent 1460e35
2 files changed, +124 −28
  • docs/superpowers/specs/2026-10-07-instance-update-settings-design.md +92 −28
  • docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S3.md +32 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md b/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
index 486491912..b213cb52c 100644
--- a/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
+++ b/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
@@ -1,6 +1,6 @@
# Instance update settings: auto-update, channel, major hold, pin
-Status: draft rev 4, 2026-10-07. Split out of the zero-downtime deploys
+Status: draft rev 5, 2026-10-07. Split out of the zero-downtime deploys
spec (`2026-10-07-zero-downtime-deploys-design.md`) at its rev 6. Rev 1 was
a design with console-signed per-instance directives. Astra's round 6
(`CODEX-FINDINGS-ZERO-DOWNTIME-R6.md`) rejected it with 6 findings.
@@ -15,8 +15,9 @@ findings. Astra closed all six on rev 2 and raised five new ones
the only judge of eligibility**, recording only facts on the record, and
adding a durable signed record of every promoted build (§4.1). Astra's S2
review of rev 3 (`CODEX-FINDINGS-SETTINGS-S2.md`) closed two of those five
-and left three partial. Rev 4 answers those three and one new finding
-(§10).
+and left three partial. Rev 4 answered those three and one new finding
+(§10). S3 (`CODEX-FINDINGS-SETTINGS-S3.md`) closed one and left three
+partial. Rev 5 answers them (§11).
## 1. Why
@@ -118,6 +119,22 @@ that V was authorized for A:**
signs pointers. The digest is the sha256 of the canonical manifest bytes,
which carry each artifact's sha256.
+**Which evidence applies.** Only today's ordinary adoption of the
+**current channel head** may rely on the signed pointer, as it does today.
+**Every install this spec adds needs the digest-bound promotion record:**
+- a pin;
+- a resolved install request whose build is no longer the head;
+- a recovery install of the build state's build.
+
+Pointers sign version names without digests, so a replayed old pointer
+plus a reused version name could authorize bytes nobody promoted. That
+weakness exists in the release chain today, independent of this spec. It
+is filed as #253, and this spec never widens it.
+
+**Backfill.** When this feature rolls out, the console writes promotion
+records for every version still in each channel's signed history. Their
+digests are taken from the canonical manifests at that time.
+
Promotion is the act that authorizes a build, so the record keeps the
authorization valid after V falls out of the pointer's ten-entry history.
Root checks that the manifest it is about to stage hashes to the record's
@@ -140,21 +157,37 @@ is the zero-downtime spec's retained identity.
| `install_request` | provision record | the console, on "Update now" | an id that only ever increases |
| **build state** | an object beside the instance's replica in the bucket | the agent, under the instance's lease | see below |
-The **build state** object holds three things:
-- the **installed build**;
-- the **last install request root resolved**, with the build it resolved
- to;
-- whether that request has **completed**.
-
-The agent rewrites the object each time root stamps an install or resolves
-a request (§4.3 rule 2), before reporting either as done. It is trusted
-the way settings are (§2 decision 5). A forged object can at most choose
-another authorized build of the same app.
-
-The object replaces rev 3's status-derived `held_build`. It is written in
-step with the install itself, not on the five-minute status cycle, so
-turning `manual` on can't record a build the instance has already moved
-past.
+The **build state** object holds two things:
+- **`last_executed`**: the build whose process last opened the instance's
+ database;
+- **the install request root last resolved**, as {R, T}, and whether it
+ has completed.
+
+**When it is written.** Before any generation runs a build X against the
+instance's database, the agent writes the build state with
+`last_executed = X` and the current request state. That covers a wake, a
+swap's successor, a fallback, a worker or sidecar spawn, and a recycle.
+The agent holds the instance's lease at that point: a wake takes the lease
+before it spawns, and a swap keeps it. If the write fails, the generation
+doesn't start. The wake fails as a wake fails today, or the swap is
+abandoned.
+
+So **the build state always names the build that last touched the
+database**, or one about to. A cold install by root on a sleeping instance
+doesn't touch the database and doesn't change the build state. If the box
+is lost before that instance wakes, recovery installs the build the data
+was last used with, which is right.
+
+**Lost requests.** Root records {R, T} locally before staging (§4.3). If
+the box is lost after resolving R but before T ever ran, nothing was
+written and T never touched the database. The replacement box resolves R
+again, against the head at that time. That is an install request doing
+what it says.
+
+The object is trusted the way settings are (§2 decision 5). A forged object
+can at most choose another authorized build of the same app (§4.1). The
+agent keeps a copy in the registry beside the route, refreshed at every
+reconcile and before every wake, and root reads that copy.
### 4.3 The rules
@@ -181,16 +214,28 @@ build its box holds as failed unless it has already served that build.
- **If the head is still baking**, R stays unresolved and the reason is
reported.
3. **`manual`, or `major = N` with a head whose semver tag is absent or has
- another major:** don't move.
- - **Nothing installed on this box** (a move, a restore or a fresh box):
- install the build state's installed build, if §4.1 authorizes it. Bake
- doesn't apply, because the instance already ran that build.
- - **No build state but the instance has a replica** (it has data, so it
- isn't new): its selection is **unknown**. Install nothing, report
- *build unknown: use Update now or set a pin*, and leave the instance
- down rather than guess.
- - **Neither build state nor replica:** the instance is new and gets the
- eligible channel head.
+ another major:** the instance is **held**, and its target is:
+ - the build state's **`last_executed`**, if there is one. If the build
+ installed on this box differs (a move, a restore, or a return to a
+ box the instance left with an old live link still in place), root
+ installs `last_executed`, if §4.1 authorizes it. Bake doesn't apply,
+ because the data was last used with that build.
+ - **No build state, but a build installed on this box:** that build.
+ This is today's state on a box upgraded from before this feature.
+ - **No build state and nothing installed, but the instance has a
+ replica:** **unknown**. Install nothing, report *build unknown: use
+ Update now or set a pin*, and leave the instance down rather than
+ guess.
+ - **None of these:** the instance is new and gets the eligible channel
+ head.
+
+ **The wake checks too.** Before it spawns a held instance, the agent
+ compares the installed build with the build state's `last_executed`. If
+ they differ, it doesn't run the installed build. It runs
+ `last_executed` from its staged path if the box has it. Otherwise it
+ touches the stage relay and holds the request until root has installed
+ it, within the wake's normal bound. So a stale live link never runs
+ against data a newer build has used.
4. **Otherwise:** today's adoption, unchanged.
The console refuses "Update now" on a pinned instance, naming the pin.
@@ -296,6 +341,16 @@ build whatever these settings say.
box. A request interrupted after resolving installs the same T. An
existing instance with a replica but no build state is held as unknown,
never moved to the head.
+- **Build state before execution:** every spawn path writes
+ `last_executed` under the lease before exec, and a failed write blocks the
+ spawn. A cold install on a sleeper leaves the build state alone, and
+ recovery after losing that box installs the previous build.
+- **Returning residence:** a held instance moving back to a box with a
+ stale live link installs `last_executed`. The wake never runs the stale
+ link.
+- **Evidence:** a pin of a pruned and reused version name, with a replayed
+ pointer, is refused, because it lacks a matching promotion record.
+ Backfill writes records for builds still in history.
- **Root pass:**
- a manual or major-held instance on a fresh box installs the build
state's installed build, not the head;
@@ -326,3 +381,12 @@ build whatever these settings say.
| S1-2 (partial), 2 | Status-derived `held_build` can roll a manual instance back | §4.2: build state written under the lease in step with each install, replacing `held_build`; §4.3: unknown selection is held, never moved to the head |
| S1-3 (partial), 3 | Older housekeepers delete protected builds | §4.4: the console refuses the settings until every box can protect them; retention goes off before any rollback |
| 4 | A completed install request runs again after a move | §4.3 rule 2: {R, T} and completion persist locally and in the build state; resolved requests replay to the same T |
+
+## 11. Astra S3 findings (rev 4), answered by rev 5
+
+| # | Finding | Answer |
+|---|---------|--------|
+| S2-1 (partial), 1 | The pointer path bypasses the digest binding | §4.1: every install this spec adds needs the digest-bound promotion record; only today's head adoption uses the pointer. The pointer-replay weakness predates this spec and is filed separately; backfill is defined |
+| S2-2 (partial), 2 | The build state is written after execution can begin | §4.2: `last_executed` is written under the lease before any generation runs, and a failed write blocks it; cold installs don't touch it; losing a resolution before T ran is harmless |
+| S2-4 (partial) | A locally resolved request is lost before it is mirrored | §4.2 *Lost requests*: T never ran, so the replacement resolves again |
+| 3 | Returning to a previous box keeps a stale installation | §4.3 rule 3: the held target is `last_executed`, root reinstalls on a mismatch, and the wake refuses a stale link |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S3.md b/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S3.md
new file mode 100644
index 000000000..8a600d428
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S3.md
@@ -0,0 +1,32 @@
+REJECT
+
+S2-1 / carried S1-1: **PARTIAL** — Promotion records bind digests, but the alternative pointer path bypasses that binding.
+S2-2 / carried S1-2: **PARTIAL** — Build state replaces status, but installation ordering and returning residences remain unsafe.
+S2-3 / carried S1-3: **CLOSED** — The upgrade gate and retention-off rollback ordering address older housekeepers.
+S2-4: **PARTIAL** — Mirrored requests survive relocation; locally resolved requests can still be lost before mirroring.
+
+1. **HIGH — §4.1: The pointer alternative bypasses the new digest binding.**
+
+ **Code:** [internal/release/release.go:119](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:119), [release.go:632](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:632), [internal/release/adopt.go:1421](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:1421).
+
+ **Failure:** Save a valid signed pointer naming V. After V leaves history and is pruned, upload arbitrary replacement bytes and a matching unsigned manifest under V, then replay the saved pointer. §4.1 permits authorization through that pointer alone: it signs version names, without manifest digests. Root therefore accepts the replacement even though V’s promotion record would reject its digest. These bytes were never authorized; this exceeds decision 5.
+
+ **Smallest fix:** Require digest-bound signed evidence for every fresh installation. A digest-less pointer must not substitute for the promotion record; define migration for legacy releases.
+
+2. **HIGH — §§4.2–4.3, §6: Writing build state before reporting completion does not protect execution.**
+
+ **Code:** [internal/release/adopt.go:1072](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:1072), [adopt.go:1258](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:1258), [internal/activator/activator.go:1153](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/activator.go:1153).
+
+ **Failure:** Root installs and stamps B while the bucket’s build state still names A. The accepted scheduler consumes that stamp independently; the spec requires mirroring before *reporting* completion, without requiring it before execution. B can run and replicate a database migration before the mirror succeeds. If the box fails, manual recovery installs A against B’s data. The unknown-selection hold does not help because valid, stale state exists. Likewise, losing locally resolved `{R,B}` before mirroring lets the replacement resolve R against a later head.
+
+ Sleeping installs expose the same coordination gap: root installs without waking, while the instance lease required for the agent’s write is acquired during wake.
+
+ **Smallest fix:** Durably persist request resolution before staging and the recoverable installed selection before admitting execution. Define the root/agent acknowledgement and sleeping-instance lease path; recovery must honor any pending durable selection.
+
+3. **HIGH — §4.3 rule 3: Returning to a previous box preserves its obsolete installation.**
+
+ **Code:** [internal/host/movespass.go:491](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/host/movespass.go:491), [internal/registry/registry.go:832](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/registry/registry.go:832), [internal/release/versions.go:290](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/versions.go:290).
+
+ **Failure:** Manual instance H runs A on X, moves to Y, is pinned to B, then has its pin cleared while remaining manual. Its durable build state correctly names B. H moves back to X. Departure purged X’s database and removed its registry row, but retained its live symlink to A. Rule 3 consults build state only when nothing is installed locally, so X keeps A and boots it against the restored B database. No request replay or malicious actor is needed.
+
+ **Smallest fix:** On relocation or database restore, reconcile the installation against durable build state before execution. Invalidate local selection on departure, or bind it to the current residence; a surviving symlink alone cannot establish the held build.
\ No newline at end of file