spec: settings rev 9 — §4.6 answers Astra S6 (digest provenance rule, FastTrack verbatim, writers-before-readers rollout)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+69
−10
docs/superpowers/specs/2026-10-07-instance-update-settings-design.md+62 −10docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S6.md+7 −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 070772d26..3b403dd2f 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: rev 8, 2026-10-07. Rev 7 was accepted by Astra (S5: accept with fixes, the fix applied) and **approved by Paul**. Rev 8 adds §4.6, which closes #253 at Paul's request, and is in review for that section. Split out of the zero-downtime deploys |
| +Status: rev 9, 2026-10-07. Rev 7 was accepted by Astra (S5: accept with fixes, the fix applied) and **approved by Paul**. Rev 8 adds §4.6, which closes #253 at Paul's request, and is in review for that section. 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. |
| @@ -291,18 +291,44 @@ installs bytes nobody promoted. |
| These are additive, like `account` was (`internal/release/release.go`). |
| The signature covers the raw payload bytes, so binaries that predate the |
| -fields ignore them and verify as before. Every promotion and rollback |
| -writes them. A rollback swaps the digests along with the versions. They |
| -are signed by whoever signs the pointer today, the root key or a scoped |
| -grant. A promotion record signed under a scoped grant carries the same |
| -grant chain. |
| - |
| -**Root checks before staging.** When the pointer that authorizes a build |
| -carries a digest for it, root checks that the canonical manifest hashes to |
| +fields ignore them and verify as before. They are signed by whoever signs |
| +the pointer today, the root key or a scoped grant. A promotion record |
| +signed under a scoped grant carries the same grant chain. |
| + |
| +**Where a digest may come from.** A digest in a new pointer has exactly two |
| +possible sources: |
| +- **it is the newly promoted version's**, computed at promotion time from |
| + the manifest the publisher shipped; |
| +- **it is copied verbatim** from the verified, signed predecessor pointer. |
| + |
| +A writer **never computes a digest for an entry it inherits.** If the |
| +predecessor entry had none, the new entry has none: `history_digests` holds |
| +an empty string at that position. Automatic backfill is thereby ruled out |
| +for pointers as well as for promotion records. |
| + |
| +**Every writer follows this rule:** |
| +- promotion; |
| +- rollback, which swaps versions and digests together; |
| +- `FastTrack`, which signs a fresh pointer for the same version |
| + (`release.go`'s fast-track path, called from the console); |
| +- `carlos channels resign`; |
| +- the direct-store CLI paths (`cli.go`). |
| + |
| +`FastTrack` copies the head and history digests verbatim and never fetches |
| +new ones. |
| + |
| +**Root checks before staging.** When the pointer entry that authorizes a |
| +build carries a digest, root checks that the canonical manifest hashes to |
| that digest before staging. Staging's artifact sha check then binds the |
| bytes. On a mismatch, root refuses the build and reports it. This covers |
| ordinary head adoption, history-based adoption and rollbacks. |
| +**Entries without a digest.** A head without a digest is handled by the |
| +ratchet and the flag below. A **history entry** without a digest is never |
| +a rollback target once its channel has any digest. A rollback onto it is |
| +refused with *run `carlos channels resign` first*. That command re-signs |
| +the entry with a digest the operator has confirmed. |
| + |
| **Pointers without digests.** Every pointer written before this change has |
| none. Three things close that gap without trusting the bucket's current |
| contents automatically: |
| @@ -317,6 +343,25 @@ contents automatically: |
| current pointer with digests. Like `carlos releases authorize`, it shows |
| the manifest digests and signs only on an operator's confirmation. |
| +**Writers before readers: the rollout order.** Once a box's ratchet is set, |
| +it refuses a pointer without digests from any writer. So digest emission |
| +starts only after every pointer writer has been upgraded: |
| +1. **Upgrade every writer.** That means every console instance, and every |
| + direct-store CLI or deploy script that promotes, rolls back or |
| + fast-tracks. |
| +2. **Turn emission on.** A root-signed marker, |
| + `releases/<acct>/pointer-digests`, is written, and upgraded writers |
| + start emitting digests only once they see it. Writers from before this |
| + change don't know the marker, which is why step 1 comes first. |
| +3. **Watch for stragglers.** The console reports any pointer without |
| + digests that is written after the marker, naming the channel. That |
| + means an old writer is still in use. |
| + |
| +Rolling a writer back below this version stops it writing pointers that |
| +boxes accept. The roll docs say so, and the marker can be removed to |
| +suspend emission while a writer is being fixed. Removing it doesn't clear |
| +the box ratchets, so it only helps until those boxes have seen digests. |
| + |
| **The fleet-wide switch.** `CARLOS_REQUIRE_POINTER_DIGEST` is a box flag, |
| off by default. With it on, a box refuses any pointer without digests, |
| even for a channel it has never seen. That closes the fresh-box case. The |
| @@ -445,7 +490,14 @@ build whatever these settings say. |
| - with `CARLOS_REQUIRE_POINTER_DIGEST` on, a fresh box refuses pointers |
| without digests; |
| - a rollback carries the digests; |
| - - a pointer signed under a scoped grant carries digests. |
| + - a pointer signed under a scoped grant carries digests; |
| + - promoting over a legacy pointer leaves inherited entries without |
| + digests, never computed; |
| + - `FastTrack` keeps the signed digests verbatim; |
| + - a rollback onto a history entry without a digest is refused until it |
| + is resigned; |
| + - writers emit digests only once the marker exists, and the console |
| + flags a pointer without digests written after the marker. |
| - **Evidence:** a pin of a pruned and reused version name, with a replayed |
| pointer, is refused, because it lacks a matching promotion record. A |
| build promoted before the feature is recovered from a staged copy, is |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S6.md b/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S6.md| new file mode 100644 |
| index 000000000..a45d478ed |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S6.md |
| @@ -0,0 +1,7 @@ |
| +REJECT |
| + |
| +1. **High — §4.6, legacy history has no safe migration rule.** Code: [internal/release/release.go:764](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:764), [internal/release/release.go:1109](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:1109). Promotion retains the previous head and history, but a legacy pointer supplies no digests to carry forward. Requiring every new pointer to contain those digests therefore conflicts with forbidding automatic backfill. Replay a legacy pointer naming a pruned, re-shipped V, then legitimately promote W: hashing the inherited V automatically authorizes its substituted bytes, which a later rollback installs despite the ratchet and flag. **Smallest fix:** require confirmed `channels resign` before promoting or rolling back a legacy pointer whose entries must be retained; preserve existing signed digests verbatim. Specify that prerequisite explicitly. |
| + |
| +2. **High — §4.6, `FastTrack` is an uncovered pointer writer.** Code: [internal/release/release.go:1160](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:1160), called by [internal/console/server.go:4419](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/console/server.go:4419). This operation constructs and signs a fresh pointer independently of promotion and rollback. Implementing the specified writers leaves it dropping both digest fields. After a box observes a digest-bearing promotion, clicking “skip the delay” replaces that pointer with one the ratchet rejects, blocking adoption instead of accelerating it. **Smallest fix:** explicitly require `FastTrack` to preserve the signed head/history digests without fetching replacement digests; define legacy handling through the confirmed resign operation. |
| + |
| +3. **High — §4.6, rollout checks readers and current objects but not older writers.** Code: [cli.go:2493](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/cli.go:2493), [cli.go:2946](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/cli.go:2946). An older direct-store CLI can verify a digest-bearing pointer, then successfully promote or roll back using its old `ChannelPointer` struct, writing no digests. Upgraded boxes reject that legitimate release once their ratchet is set—even with the fleet flag off. An empty console inventory does not prevent the next old writer from doing this. **Smallest fix:** require upgrading or retiring every pointer writer, including direct-store deployment scripts and console instances, before emitting digest-bearing pointers; document that rolling a writer back below this version stops compatible writes. |
| \ No newline at end of file |