carlos / platform Public

spec: console bake controls — the production flag and fast-track

Per-app production flag (default off, ratcheted off-switch, deletion- and
replay-proof), root-key fast-track recorded on the pointer like -hotfix,
console controls behind the existing passkey step-up, and the edge:0 fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
Paul Campbell 660ba46dda0e10d43549b2d00e231e75c80411ee parent 3b8e323
1 file changed, +257 −0
  • docs/superpowers/specs/2026-08-04-console-bake-controls-design.md +257 −0
diff --git a/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md b/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
new file mode 100644
index 000000000..b42b9934a
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
@@ -0,0 +1,257 @@
+# Console bake controls — the production flag and fast-track
+
+2026-08-04. Extends the pipelines spec (2026-08-02-pipelines-design.md §3, §5)
+and the scoped signing keys spec (2026-08-03). Touches `internal/release`
+(bake.go, adopt.go, release.go) and `internal/console` (server.go, ladder.go,
+view.go, templates).
+
+## 1. Problem
+
+The supply-chain bake window (beta 24h, stable 72h) protects subscribers of an
+app that is actually in production. On a rapid-iteration deployment, edge, beta
+and stable are effectively the same audience, and the hold is pure friction —
+today the only escape is root on each box, and even that is unwired (the
+`ignore` parameter of `Hold` has no caller that passes true).
+
+Two adjacent defects surfaced while designing this:
+
+- `DefaultHolds` has no `edge` entry, so `HoldFor` fail-closes edge to 72
+ hours. The pipelines spec defines edge as "picked up as it lands — no human
+ in the loop, by definition". This spec fixes it to 0.
+- The "per-deployment overrides come from host config" promised in bake.go's
+ comment was never wired; that stays unwired (non-goal, §10).
+
+## 2. The production flag
+
+Each app carries a **production flag**. `production: true` means the
+supply-chain bake applies to the app's beta and stable channels;
+`production: false` (the default) means its boxes adopt immediately on every
+channel.
+
+**Default is off.** A new app is not in production, and the flag should mean
+"someone declared this production" — a real audit fact — rather than "nobody
+got around to opting out". Nothing on either current deployment loses
+protection: the entire fleet runs on `canary/*` channels, which never bake.
+The silent-failure risk of default-off (a production app quietly missing its
+window) is answered structurally in §7: promote-to-stable asks the question at
+the exact moment it matters, and prod-wiring detection nags persistently.
+
+The flag gates ONLY the bake window. The ladder, its sign-off gates, the
+passkey step-up on stable, and route wiring are all untouched — a
+non-production app climbs edge → beta → stable with the same recorded
+promotions; its boxes just don't sit out the hold.
+
+## 3. Wire shapes — both additive
+
+Live pointers and manifests exist in production buckets; everything written
+before this change must still parse (repo rule). Both shapes here are
+additive, and both fail SAFE on old binaries: a box that predates this feature
+keeps baking.
+
+**The flag object** is a new signed bucket object per app, in the same Ed25519
+envelope as channel pointers, root-key-signed:
+
+```json
+{"account": "<sqid>", "app": "<app>", "production": true,
+ "actor": "<address>", "set_at": "<rfc3339>"}
+```
+
+Stored at `apps/<acct>/<app>/production.json` (the `appPrefix` shape) —
+deliberately NOT under `channels/`, which ChannelsPrefix lists and whose
+entries the console and CLI parse as pointers. A missing object means the
+default: not production — but see §4 for why absence never *downgrades* a box
+that has seen `true`. A scoped key must not be able to mint it — enforced at
+mint and at verification, like everything else the root key reserves.
+
+**Fast-track** is a new optional field on the channel pointer itself,
+mirroring `Hotfix`:
+
+```
+FastTrack bool `json:"fast_track,omitempty"`
+```
+
+No second object to fetch during adopt; inherently scoped to one
+(account, app, channel, version) so it cannot be replayed onto a later
+release; recorded on the very artifact it waives, next to the `Actor` trail.
+
+Consumer list for the pointer shape (everything that PARSES it, per the repo
+rule): `verifyPointer` and the adopt pass, console `ChannelView`/`view.go`,
+CLI status output, `laddercheck`. All are additive parses; old parsers ignore
+the field, which means old boxes bake — the safe direction.
+
+## 4. The ratchet on turning bake off
+
+Flipping an app to `production: false` is honored by each box only after the
+flag object has sat unchanged for the **stable window (72h) on that box's own
+first-seen clock** — the flag defends itself with the mechanism it controls.
+A stolen root key cannot insta-disable baking; it can only start a loud 72h
+timer that the console displays for the whole window (§7).
+
+Mechanics: the adopt pass runs the flag object through the same `FirstSeen`
+clock, under a synthetic key that cannot collide with channel keys (the
+`clockSep` argument from bake.go applies). First sighting of a given
+`production: false` object starts the clock; `Hold`-style eligibility uses the
+stable hold with the box's jitter.
+
+Flipping to `production: true` is honored **immediately** — the fail-safe
+direction always wins, and a re-enable must never itself bake.
+
+Two tamper cases the ratchet must survive, both handled with small local
+state in the box's bake-clock file:
+
+- **Deletion is not a downgrade.** Once a box has seen a verified
+ `production: true` for an app, it holds that verdict until a verified
+ `production: false` passes the ratchet. A missing, unreadable or
+ unverifiable object never downgrades a box that once saw `true` — else
+ anyone with bucket write but no key could delete the flag and skip the
+ ratchet entirely. The box persists "this app was production" alongside its
+ first-seen clock.
+- **A stale flag cannot be replayed.** The box records the newest `set_at` it
+ has honored per app and refuses an object older than that — a signed
+ `production: false` from a past era, re-written to the bucket by someone
+ with bucket access alone, cannot roll back a newer `true`. (It would still
+ cost the replayer the full 72h ratchet even without this; the monotonicity
+ check just closes the loop.)
+
+Carve-out: if the app has no beta or stable pointer at the moment a box
+evaluates the flag, `production: false` takes effect immediately — there is
+nothing at a baked head to protect. With default-off this is rarely exercised
+(it matters only for an app flipped on and then off while young), but it keeps
+the ratchet honest about what it protects.
+
+## 5. Fast-track a release
+
+For a production app whose beta or stable head is inside its window, the
+console offers **fast-track**: re-promote the same version with
+`FastTrack: true`, rewriting the pointer in place. Boxes then skip the hold
+for exactly that version on that channel.
+
+- `PromoteOptions` gains `FastTrack bool`. The promote no-op check learns one
+ nuance: same version but *gaining* fast_track is a write, not a repeat.
+- **Root key only, enforced at verification, not just at mint** — a
+ scoped-key-signed pointer bearing `fast_track` is refused by verifiers,
+ matching the grant model's mint-AND-verify rule. Otherwise one leaked
+ scoped stable key would defeat the window it bakes under.
+- `fast_track` is set per write and never inherited: a later promotion of a
+ new version onto the channel starts clean, and a rollback writes whatever
+ the rollback path writes today (rollback never bakes anyway).
+
+Rollback needs none of this: a pull-back to a previously-seen version is
+already immediately eligible (bake.go keeps past heads for exactly this).
+
+## 6. Adopt changes (box side)
+
+In `adoptHost`, before the existing `Hold` check: resolve the app's flag
+(verified read, with §4's deletion and replay rules), and pass `ignore=true`
+to `Hold` when either
+
+- the app is non-production and past its ratchet (§4), or
+- the channel pointer carries a root-signed `fast_track` for the version
+ being considered.
+
+Fail-safe detail: for a box with no memory of `true`, a flag object that
+exists but will not read or verify is treated as `production: true` — an
+attacker who can corrupt but not sign must not be able to shorten anything.
+The bake clock still ticks regardless of the flag (`Observe` runs as today):
+observations are cheap, and flipping the flag back on must not restart
+anyone's 72h from zero.
+
+The wired `ignore` finally gives the box-local override a caller; a
+`carlos adopt` flag for root-on-box use remains a non-goal (§10).
+
+## 7. Console surface
+
+All on the app page's ladder section, all through the existing gates in the
+repo's mandated order — session → CSRF → owner (byte-identical 404) →
+`requireFreshAuth` LAST. No new auth machinery: these are three more callers
+of the existing step-up gate.
+
+- **Bake status line**: "Supply-chain bake: off (default)" / "on" /
+ "turning off — hosts honor this from ~<date>" during the ratchet, the date
+ computed from the flag's `set_at` plus the stable hold (an estimate — each
+ box's own clock and jitter govern, as the adopt nag already words it).
+- **Turn on**: any member, instant, no step-up. The safe direction is the
+ cheap one, deliberately frictionless.
+- **Turn off**: owner-only + passkey step-up, audited, and the ratchet
+ message shown on confirmation.
+- **Fast-track**: shown on a beta/stable channel row currently inside its
+ window (the row already knows PromotedAt + `HoldFor`); owner-only +
+ step-up; audited; the row carries a permanent "fast-tracked" badge, like
+ the hotfix badge.
+- **Promote-to-stable prompt** (the structural mitigation for default-off):
+ when the flag is off, the stable promote flow — already owner + passkey —
+ asks inline: "This puts a build in front of every user of this app. Is this
+ app in production now? Enabling the supply-chain bake is recommended." One
+ checkbox, recorded through the same confirmed action. It asks; it never
+ flips the flag on its own.
+- **Prod-wiring nudge**: the signals `prodWiredCanaryHosts` already computes
+ (a route bound to the same environment as a stable route, or sitting on the
+ app's apex) plus "has a stable route at all": an app tripping them while
+ bake is off gets a persistent line on its ladder section — "this app looks
+ production-wired but adopts releases immediately" — and the same line in
+ the fleet view. Detection recommends; it never flips the flag (implicit
+ gates are the thing this platform keeps refusing).
+
+## 8. The edge fix
+
+`DefaultHolds` gains `"edge": 0`, restoring the pipelines spec's definition of
+edge. Independent of everything above and safe to land first.
+
+## 9. Security posture — what changed and what held
+
+bake.go's ported doctrine was "deliberately NO publisher-side bypass". This
+spec knowingly relaxes it, the way ladder.go already did for the ladder: an
+explicit, recorded bypass beats an invisible one, and beats the workaround
+culture that grows around a gate with no legitimate exit.
+
+What an attacker holding a **scoped** key can do: nothing new. Fast-track and
+the flag are root-reserved at mint and at verification.
+
+What an attacker holding the **root** key gains: an instant path past the
+window — fast-track — that is loud by construction: the pointer names the
+actor and wears the badge forever. The standing flag gains them nothing
+instant: flipping it off starts a 72h ratchet the console displays the whole
+time, which preserves the discovery window the bake existed to buy. The
+honest loss is exactly fast-track on production apps: before this spec a
+root-key thief had no publisher-side way past the hold; now they have one
+that cannot be used quietly.
+
+Console compromise short of the root key gets nothing: every control sits
+behind owner + fresh passkey, server-side, fail-closed when Keymail is
+unreachable (stepup.go's existing properties).
+
+## 10. Non-goals
+
+- CLI fast-track flag (a root-key holder at a terminal has stronger powers
+ already; add later if wanted).
+- Per-deployment hold overrides via host config — bake.go's comment keeps
+ promising it; still unwired.
+- A `carlos adopt -ignore-bake` root-on-box flag (the `ignore` parameter now
+ has its console-driven caller; the local flag can follow if ever needed).
+- Any canary behavior change. Canary remains bake-free and outside all of
+ this.
+
+## 11. Testing
+
+- `bake_test.go`: ratchet timing (off honored only after stable hold +
+ jitter), on honored immediately, the no-beta/stable carve-out, corrupt
+ flag object treated as production, deletion-is-not-a-downgrade, stale
+ `set_at` replay refused.
+- Wire tests: pre-change pointer parses; pointer with `fast_track` parses in
+ the old struct shape (field ignored).
+- Grant/verify tests: scoped-key-signed `fast_track` refused; scoped key
+ cannot write the flag object.
+- Promote tests: same-version-gaining-fast_track is a write, not a no-op.
+- Console tests: gate ordering for all three controls (non-member gets the
+ byte-identical 404 before any step-up redirect), promote-to-stable prompt
+ shown only when the flag is off, nudge appears on prod-wired signals.
+- `laddercheck_test.go`'s pinned production channels: untouched and still
+ green — the fleet runs on `canary/*` and nothing here may refuse them.
+
+## 12. Operational follow-up (not part of the build)
+
+Once shipped: flip the flag ON for flagship's four apps (console, kass,
+keymail, platform-site) and tito's production apps — these are the apps the
+flag is for. Until their routes follow beta/stable channels the flag changes
+nothing day-to-day, but the declaration should exist before the migration off
+`canary/*` ever happens, not after.