carlos / platform Public

merge: console bake controls — the production flag and fast-track (#47)

Console bake controls — the production flag and fast-track
Paul Campbell 088358d7f1edcf34f34ec074f029f3e4365c2029 parent 6d5f71aparent 4f7c7c9
21 files changed, +4599 −32
  • accounts.go +46 −11
  • docs/superpowers/plans/2026-08-04-console-bake-controls.md +1761 −0
  • docs/superpowers/specs/2026-08-04-console-bake-controls-design.md +277 −0
  • e2e_test.go +94 −0
  • internal/console/bakecontrols_test.go +354 −0
  • internal/console/ladder.go +5 −0
  • internal/console/ladder_test.go +206 −0
  • internal/console/server.go +171 −1
  • internal/console/server_test.go +33 −5
  • internal/console/templates/app.html +1 −1
  • internal/console/templates/ladder.html +65 −1
  • internal/console/view.go +91 −0
  • internal/console/view_test.go +8 −1
  • internal/release/adopt.go +109 −5
  • internal/release/adopt_test.go +320 −0
  • internal/release/bake.go +178 −5
  • internal/release/bake_test.go +251 −0
  • internal/release/production.go +177 −0
  • internal/release/production_test.go +240 −0
  • internal/release/release.go +69 −2
  • internal/release/release_test.go +143 −0
diff --git a/accounts.go b/accounts.go
index 3b3d9911f..a703033c4 100644
--- a/accounts.go
+++ b/accounts.go
@@ -275,7 +275,7 @@ func runAccountsMigrate(args []string) {
// Where the app's objects are RIGHT NOW. This is the whole hazard of the
// verb, so it is resolved the way the runtime resolves them and not by
// assuming a prefix.
- plan, srcAcct := migrationPlan(ctx, store, *app, *from, *to, dst)
+ plan, srcAcct, hadProduction := migrationPlan(ctx, store, *app, *from, *to, dst)
if len(plan) == 0 {
fmt.Printf("nothing under %s — nothing to copy\n", release.AppPrefix(srcAcct, *app))
}
@@ -349,6 +349,14 @@ func runAccountsMigrate(args []string) {
}
fmt.Printf("copied %s -> %s\n", c.from, c.to)
}
+ // The one object that does not travel. Loud, because nothing downstream
+ // will notice: a destination with no flag simply defaults to "not
+ // production", so the app stops baking silently rather than failing.
+ if hadProduction {
+ fmt.Println("\nNOT COPIED:")
+ fmt.Printf(" %s: carried a production declaration — re-declare it at the destination "+
+ "(the flag is signed per-account and cannot be copied)\n", *app)
+ }
if !*dryRun {
verifyMigratedPointers(ctx, store, *app, srcAcct, *to, keptPointers)
}
@@ -401,6 +409,26 @@ type copyOp struct {
// pointer rather than an immutable release object.
func isChannelPointer(rel string) bool { return strings.HasPrefix(rel, "channels/") }
+// productionFlagRel is the production flag's key relative to an app prefix —
+// the ONE object this verb must not carry across. Its signed payload NAMES
+// the source account, so a byte copy fails validateProductionFlag at the
+// destination forever: the console shows an unreadable flag, and every box
+// fails toward baking, holding the app's beta and stable heads with no expiry
+// and nothing that reads like an error (verifyMigratedPointers checks channel
+// pointers only). It is re-declared at the destination instead — one POST on
+// the app page, or the same by hand.
+const productionFlagRel = "production.json"
+
+// planCopy adds one object to the migration plan, dropping the production
+// flag and recording that the source had one so the verb can say so.
+func planCopy(out []copyOp, hadProduction *bool, from, rel, dst string) []copyOp {
+ if rel == productionFlagRel {
+ *hadProduction = true
+ return out
+ }
+ return append(out, copyOp{from, dst + rel, isChannelPointer(rel)})
+}
+
// putOverwrite writes body at key whatever is there now: read the etag, put
// with it, one retry on a lost race (the CAS-guarded read-then-write the
// fleet publisher and the console's manifest writer both use). Reports
@@ -449,9 +477,13 @@ func putOverwrite(ctx context.Context, s release.Store, key string, body []byte)
// under both.) `-from` overrides it with a single explicit prefix, for moving
// between two non-ops accounts.
//
-// Returns the copies to make and the account the app is being moved FROM (for
-// the post-copy verification).
-func migrationPlan(ctx context.Context, store release.Store, app, from, to, dst string) ([]copyOp, string) {
+// The production flag is the one object deliberately NOT copied — see
+// productionFlagRel. The third return says the source carried one, so the
+// verb can tell the operator to re-declare it.
+//
+// Returns the copies to make, the account the app is being moved FROM (for
+// the post-copy verification), and whether a production flag was skipped.
+func migrationPlan(ctx context.Context, store release.Store, app, from, to, dst string) ([]copyOp, string, bool) {
// The account-less keys, named explicitly. Checked BEFORE ValidAccount:
// acctRe is [a-z]{3,16}, which "legacy" satisfies, so the order is what
// keeps the sentinel from being read as an account sqid.
@@ -466,11 +498,12 @@ func migrationPlan(ctx context.Context, store release.Store, app, from, to, dst
}
fmt.Printf("source: %s (the legacy, account-less keys)\n", src)
var out []copyOp
+ hadProduction := false
for _, k := range keys {
rel := strings.TrimPrefix(k, src)
- out = append(out, copyOp{k, dst + rel, isChannelPointer(rel)})
+ out = planCopy(out, &hadProduction, k, rel, dst)
}
- return out, release.LegacySource
+ return out, release.LegacySource, hadProduction
}
if from != "" {
if !release.ValidAccount(from) {
@@ -485,11 +518,12 @@ func migrationPlan(ctx context.Context, store release.Store, app, from, to, dst
log.Fatalf("accounts migrate: list %s: %v", src, err)
}
var out []copyOp
+ hadProduction := false
for _, k := range keys {
rel := strings.TrimPrefix(k, src)
- out = append(out, copyOp{k, dst + rel, isChannelPointer(rel)})
+ out = planCopy(out, &hadProduction, k, rel, dst)
}
- return out, from
+ return out, from, hadProduction
}
if to == release.OpsAccount {
@@ -523,19 +557,20 @@ func migrationPlan(ctx context.Context, store release.Store, app, from, to, dst
// per-key precedence described above.
seen := map[string]bool{}
var out []copyOp
+ hadProduction := false
for _, k := range opsKeys {
rel := strings.TrimPrefix(k, opsPrefix)
seen[rel] = true
- out = append(out, copyOp{k, dst + rel, isChannelPointer(rel)})
+ out = planCopy(out, &hadProduction, k, rel, dst)
}
for _, k := range legacyKeys {
rel := strings.TrimPrefix(k, legacyPrefix)
if seen[rel] {
continue // the qualified copy is the live one
}
- out = append(out, copyOp{k, dst + rel, isChannelPointer(rel)})
+ out = planCopy(out, &hadProduction, k, rel, dst)
}
- return out, release.OpsAccount
+ return out, release.OpsAccount, hadProduction
}
// verifyMigratedPointers re-reads every channel pointer after the copy and
diff --git a/docs/superpowers/plans/2026-08-04-console-bake-controls.md b/docs/superpowers/plans/2026-08-04-console-bake-controls.md
new file mode 100644
index 000000000..480b55ebb
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-console-bake-controls.md
@@ -0,0 +1,1761 @@
+# Console Bake Controls Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Per-app production flag (default off) gating the supply-chain bake window, a ratcheted off-switch, a root-key fast-track waiver on channel pointers, console controls behind the existing passkey step-up, and the `edge: 0` hold fix.
+
+**Architecture:** Spec is `docs/superpowers/specs/2026-08-04-console-bake-controls-design.md` — read it first. The release package gains a signed per-app flag object (`apps/<acct>/<app>/production.json`), a `fast_track` field on channel pointers, box-side flag memory in the bake clock, and adopt wiring that finally passes `Hold`'s `ignore` parameter. The console gains two handlers, view plumbing, and ladder-section controls behind `requireFreshAuth`.
+
+**Tech Stack:** Go stdlib only; Ed25519 via the existing `signedWrapper` discipline; html/template.
+
+## Global Constraints
+
+- Working directory is the worktree root (`.claude/worktrees/console-bake-controls-spec`). Never `cd` to the main checkout.
+- Go commands need: `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off` (and may need the sandbox disabled — "read-only file system" from the toolchain is the tell).
+- All wire changes are ADDITIVE: pointers/flags written before this change must still parse, and pointers written by this change must parse in old struct shapes (fields dropped, boxes bake — the safe direction).
+- `internal/release/laddercheck_test.go`'s pinned production channels must stay green untouched.
+- `gofmt -l` clean for every file you touch.
+- Commit messages end with:
+ `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>` and
+ `Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK`
+- Test style: package-internal tests (`package release` / `package console`), table-driven where natural, comments explaining WHY in the repo's voice.
+
+**Delegation (CLAUDE.md policy):** Task 1 → Haiku. Tasks 2, 3, 4, 6, 8 → Sonnet. Tasks 5, 7, 9 → Opus (adopt is the production install path; handlers are security gates; 9 is the reviewer pass).
+
+---
+
+### Task 1: `edge` bakes zero (spec §8)
+
+**Files:**
+- Modify: `internal/release/bake.go` (the `DefaultHolds` map, ~line 36)
+- Test: `internal/release/bake_test.go`
+
+**Interfaces:**
+- Consumes: `HoldFor(holds map[string]time.Duration, channel string) time.Duration` (bake.go)
+- Produces: `DefaultHolds["edge"] == 0` — Tasks 5/6 rely on edge (and canary) being hold-free.
+
+- [ ] **Step 1: Write the failing test** (append to `internal/release/bake_test.go`)
+
+```go
+// The pipelines spec defines edge as "picked up as it lands — no human in
+// the loop, by definition". Without its own entry, HoldFor fail-closes edge
+// to 72h — longer than beta, and a spec violation (2026-08-04 console bake
+// controls spec, §1/§8).
+func TestHoldForEdgeIsZero(t *testing.T) {
+ if d := HoldFor(DefaultHolds, "edge"); d != 0 {
+ t.Fatalf("edge hold = %v, want 0", d)
+ }
+}
+```
+
+- [ ] **Step 2: Run it, verify it fails**
+
+Run: `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/release/ -run TestHoldForEdgeIsZero -count=1`
+Expected: FAIL, `edge hold = 72h0m0s, want 0`
+
+- [ ] **Step 3: Add the entry** — in `DefaultHolds`, add `"edge": 0,` between `"canary/*"` and `"beta"`, and extend the map's doc comment block (bake.go lines 12–16) with one sentence: `edge has no window either — merged to main is picked up as it lands (pipelines spec §3).`
+
+- [ ] **Step 4: Run the package tests** — `go test ./internal/release/ -count=1` (with the env above). Expected: PASS (no existing test pins edge's 72h).
+
+- [ ] **Step 5: Commit** — `git add internal/release/bake.go internal/release/bake_test.go && git commit -m "release: edge bakes zero — merged to main is picked up as it lands"` (with the trailer lines from Global Constraints).
+
+---
+
+### Task 2: the signed production flag object (spec §2, §3)
+
+**Files:**
+- Create: `internal/release/production.go`
+- Test: `internal/release/production_test.go`
+
+**Interfaces:**
+- Consumes: `signedWrapper` (release.go), `ValidAccount`, `ValidApp`, `appPrefix`, `Store`, `ErrCASConflict`.
+- Produces (later tasks call these exactly):
+ - `type ProductionFlag struct { Schema int; Account, App string; Production bool; Actor string; SetAt time.Time }`
+ - `func ProductionKey(acct, app string) string`
+ - `func SetProduction(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app string, production bool, actor string) error`
+ - `func GetProduction(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) (*ProductionFlag, error)` — missing object is `(nil, nil)`; any other failure is the error, which callers treat as production=true (fail toward baking).
+
+- [ ] **Step 1: Write the failing tests** (`internal/release/production_test.go`)
+
+```go
+package release
+
+// The production flag (2026-08-04 console bake controls spec §2–§3): a
+// root-signed per-app object deciding whether the supply-chain bake applies.
+// The properties tested here, in the order they'd hurt if lost: missing is
+// the default (nil, nil); a set flag round-trips; a scoped key's wrapper is
+// refused even with a valid root grant; a tampered payload is refused; the
+// key sits OUTSIDE channels/ so pointer listings never try to parse it.
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+ "testing"
+)
+
+func TestProductionFlagDefaultIsMissing(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ _, pub := testKeys(t)
+ f, err := GetProduction(context.Background(), s, pub, "bab", "kass")
+ if err != nil || f != nil {
+ t.Fatalf("GetProduction on empty store = %v, %v; want nil, nil (the default: not production)", f, err)
+ }
+}
+
+func TestProductionFlagRoundTrips(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, "paul@keymail.dev"); err != nil {
+ t.Fatalf("SetProduction: %v", err)
+ }
+ f, err := GetProduction(ctx, s, pub, "bab", "kass")
+ if err != nil {
+ t.Fatalf("GetProduction: %v", err)
+ }
+ if f == nil || !f.Production || f.Actor != "paul@keymail.dev" || f.SetAt.IsZero() {
+ t.Fatalf("flag = %+v, want production on, actor recorded, set_at stamped", f)
+ }
+ // Flip it off over the existing object — the CAS path.
+ if err := SetProduction(ctx, s, priv, "bab", "kass", false, "paul@keymail.dev"); err != nil {
+ t.Fatalf("SetProduction off: %v", err)
+ }
+ if f, _ = GetProduction(ctx, s, pub, "bab", "kass"); f == nil || f.Production {
+ t.Fatalf("flag = %+v after flipping off, want production false", f)
+ }
+}
+
+// The flag key lives beside channels/, never inside it: ChannelsPrefix
+// listings are parsed as pointers by the console and CLI.
+func TestProductionKeyIsOutsideChannels(t *testing.T) {
+ if k := ProductionKey("bab", "kass"); k != "apps/bab/kass/production.json" {
+ t.Fatalf("ProductionKey = %q", k)
+ }
+ if strings.HasPrefix(ProductionKey("bab", "kass"), ChannelsPrefix("bab", "kass")) {
+ t.Fatal("production flag key sits under channels/ — pointer listings would try to parse it")
+ }
+}
+
+// A scoped key must not be able to mint the flag: a wrapper carrying ANY
+// grant is refused before signature checking, and a wrapper signed by a key
+// other than the root fails signature checking. Together: root only.
+func TestProductionFlagRefusesScopedAndForeignKeys(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+
+ // Build a validly-shaped flag payload signed by a DIFFERENT key.
+ otherPub, otherPriv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _ = otherPub
+ payload, _ := json.Marshal(ProductionFlag{Schema: 1, Account: "bab", App: "kass",
+ Production: false, SetAt: timeNowTestHelper()})
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ wrapper, _ := json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(otherPriv, []byte(b64))),
+ })
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "kass"); err == nil {
+ t.Fatal("a flag signed by a non-root key verified")
+ }
+
+ // A wrapper WITH a grant is refused outright — even a grant the root
+ // really signed. Mint a real scoped key and have it sign the flag.
+ seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ scopedKey, err := ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sg, err := parseGrantBlob(grantB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ wrapper, _ = json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(scopedKey, []byte(b64))),
+ Grant: sg,
+ })
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, etagOfKey(t, s, "bab", "kass")); err != nil {
+ t.Fatal(err)
+ }
+ _, err = GetProduction(ctx, s, pub, "bab", "kass")
+ if err == nil || !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a grant-bearing flag wrapper verified (err=%v), want a root-only refusal", err)
+ }
+}
+
+// etagOfKey reads the current etag of an app's flag so a test can CAS over it.
+func etagOfKey(t *testing.T, s Store, acct, app string) string {
+ t.Helper()
+ _, etag, err := s.Get(context.Background(), ProductionKey(acct, app))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return etag
+}
+
+func TestProductionFlagRefusesWrongApp(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, ""); err != nil {
+ t.Fatal(err)
+ }
+ // Copy kass's flag to keymail's key — a cross-app replay must be refused
+ // because the SIGNED payload names the app.
+ raw, _, err := s.Get(ctx, ProductionKey("bab", "kass"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "keymail"), raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "keymail"); err == nil {
+ t.Fatal("another app's flag verified under keymail's key")
+ }
+}
+```
+
+Add this small helper at the bottom of the test file (tests must not depend on wall-clock ordering):
+
+```go
+// timeNowTestHelper is a stamped, truncated now — what signProductionFlag
+// itself writes — for tests that hand-build payloads.
+func timeNowTestHelper() time.Time { return time.Now().UTC().Truncate(time.Second) }
+```
+
+(and add `"time"` to the imports.)
+
+- [ ] **Step 2: Run tests, verify they fail** — `go test ./internal/release/ -run TestProductionFlag -count=1` (env from Global Constraints). Expected: compile errors (`undefined: ProductionKey` etc.).
+
+- [ ] **Step 3: Implement** (`internal/release/production.go`)
+
+```go
+// The per-app production flag — the console bake controls spec
+// (docs/superpowers/specs/2026-08-04-console-bake-controls-design.md, §2–§3).
+//
+// production: true means the supply-chain bake window (bake.go) applies to
+// this app's held channels; false — and MISSING, the default — means its
+// boxes adopt immediately. The object is root-signed in the pointer's own
+// signedWrapper envelope, and root-ONLY: a wrapper carrying a key grant is
+// refused before its signature is even checked, because a scoped key that
+// could flip the flag could skip the very window it bakes under.
+//
+// What verification here does NOT decide: whether a missing or unreadable
+// flag downgrades a box that once saw production: true. It never does —
+// that memory, the ratchet and the replay floor are the box's own
+// (bake.go's production memory), because anyone with bucket write but no
+// key can delete an object, and deletion must not be a downgrade (spec §4).
+package release
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+)
+
+// ProductionFlag is the signed payload: one app's declaration that it is (or
+// is not) in production, and therefore whether its releases bake.
+type ProductionFlag struct {
+ Schema int `json:"schema"` // 1
+ Account string `json:"account"`
+ App string `json:"app"`
+ Production bool `json:"production"`
+ // Actor is who flipped it — a console member's address. Recorded for the
+ // same reason the pointer's audit trail exists: an invisible bypass is a
+ // broken gate.
+ Actor string `json:"actor,omitempty"`
+ SetAt time.Time `json:"set_at"`
+}
+
+// ProductionKey is where an app's flag lives: beside channels/ and
+// releases/, deliberately NEVER under ChannelsPrefix — those listings are
+// parsed as channel pointers by the console and the CLI.
+func ProductionKey(acct, app string) string {
+ return appPrefix(acct, app) + "production.json"
+}
+
+// SetProduction writes the flag, root key only, CAS over whatever is there
+// (a concurrent flip surfaces as ErrCASConflict — re-ask, never overwrite
+// blind). SetAt is stamped here; boxes use their OWN sighting for the
+// ratchet and this stamp only as a replay floor.
+func SetProduction(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app string, production bool, actor string) error {
+ if len(priv) != ed25519.PrivateKeySize {
+ return fmt.Errorf("set production: no usable release signing key")
+ }
+ if !ValidAccount(acct) {
+ return fmt.Errorf("set production: invalid account %q", acct)
+ }
+ if !ValidApp(app) {
+ return fmt.Errorf("set production: invalid app %q", app)
+ }
+ key := ProductionKey(acct, app)
+ _, etag, err := s.Get(ctx, key)
+ if errors.Is(err, os.ErrNotExist) {
+ etag = "" // first write: create-only
+ } else if err != nil {
+ return fmt.Errorf("set production: read %s: %w", key, err)
+ }
+ f := &ProductionFlag{
+ Schema: 1, Account: acct, App: app, Production: production,
+ Actor: actor, SetAt: time.Now().UTC().Truncate(time.Second),
+ }
+ if err := validateProductionFlag(f, acct, app); err != nil {
+ return fmt.Errorf("set production: %w", err)
+ }
+ payload, err := json.MarshalIndent(f, "", " ")
+ if err != nil {
+ return err
+ }
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ wrapper, err := json.MarshalIndent(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(priv, []byte(b64))),
+ }, "", " ")
+ if err != nil {
+ return err
+ }
+ _, err = s.Put(ctx, key, wrapper, etag)
+ return err
+}
+
+// GetProduction resolves an app's flag. A MISSING object is (nil, nil): the
+// default, not production — but never let a nil collapse the distinction the
+// spec draws: any OTHER failure (unreadable, unverifiable, misnamed) is the
+// error, and callers on the adopt path treat it as production=true, failing
+// toward baking.
+func GetProduction(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) (*ProductionFlag, error) {
+ raw, _, err := s.Get(ctx, ProductionKey(acct, app))
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return verifyProductionFlag(pub, raw, acct, app)
+}
+
+// verifyProductionFlag is verifyPointer's discipline for the flag: refuse a
+// grant outright, check the signature against the ROOT key alone, decode
+// after, validate the payload names the account/app whose key it sits at.
+func verifyProductionFlag(pub ed25519.PublicKey, wrapper []byte, wantAcct, wantApp string) (*ProductionFlag, error) {
+ var sw signedWrapper
+ if err := json.Unmarshal(wrapper, &sw); err != nil {
+ return nil, fmt.Errorf("production flag wrapper: %v", err)
+ }
+ if sw.Grant != nil {
+ return nil, fmt.Errorf("production flag %s/%s: signed by a scoped key — the flag takes the root key, because a delegate that could flip it could skip the window it bakes under", wantAcct, wantApp)
+ }
+ if len(pub) != ed25519.PublicKeySize {
+ return nil, fmt.Errorf("production flag: no usable release public key")
+ }
+ sig, err := base64.StdEncoding.DecodeString(sw.Sig)
+ if err != nil {
+ return nil, fmt.Errorf("production flag sig: %v", err)
+ }
+ if !ed25519.Verify(pub, []byte(sw.Payload), sig) {
+ return nil, fmt.Errorf("production flag signature verification failed")
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ return nil, fmt.Errorf("production flag payload: %v", err)
+ }
+ var f ProductionFlag
+ if err := json.Unmarshal(payload, &f); err != nil {
+ return nil, fmt.Errorf("production flag payload: %v", err)
+ }
+ if err := validateProductionFlag(&f, wantAcct, wantApp); err != nil {
+ return nil, err
+ }
+ return &f, nil
+}
+
+func validateProductionFlag(f *ProductionFlag, wantAcct, wantApp string) error {
+ if f.Schema != 1 {
+ return fmt.Errorf("unsupported production flag schema %d", f.Schema)
+ }
+ if f.Account != wantAcct || f.App != wantApp {
+ return fmt.Errorf("production flag names %s/%s, expected %s/%s", f.Account, f.App, wantAcct, wantApp)
+ }
+ if !ValidAccount(f.Account) || !ValidApp(f.App) {
+ return fmt.Errorf("production flag has invalid account/app %s/%s", f.Account, f.App)
+ }
+ if f.SetAt.IsZero() {
+ return fmt.Errorf("production flag has no set_at")
+ }
+ return nil
+}
+```
+
+- [ ] **Step 4: Run tests, verify pass** — `go test ./internal/release/ -run TestProduction -count=1`. Expected: PASS. Then the whole package: `go test ./internal/release/ -count=1`.
+
+- [ ] **Step 5: Commit** — `git add internal/release/production.go internal/release/production_test.go && git commit -m "release: the per-app production flag — root-signed, missing means not production"` (+ trailers).
+
+---
+
+### Task 3: fast_track on the pointer, and the FastTrack verb (spec §3, §5)
+
+**Files:**
+- Modify: `internal/release/release.go` (ChannelPointer ~line 68, Promotion ~line 90, verifyPointer ~line 344, signPointer ~line 263, new verb after Rollback ~line 850)
+- Test: `internal/release/release_test.go` (append)
+
+**Interfaces:**
+- Consumes: `signedWrapper`, `signPointer(sg Signer, p *ChannelPointer)`, `verifyPointer`, `GetQualified`, `prependCapped`, `Rung`, `RootSigner`.
+- Produces:
+ - `ChannelPointer.FastTrack bool` (json `fast_track,omitempty`) — Task 5's adopt and Task 6's views read it.
+ - `Promotion.FastTrack bool` (json `fast_track,omitempty`).
+ - `func FastTrack(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app, channel, actor string) (string, error)` — returns the version waived; no-op success if already fast-tracked. Task 7's handler calls it.
+
+- [ ] **Step 1: Write the failing tests** (append to `internal/release/release_test.go`; reuse its existing helpers — read the file's top for `testKeys` and whatever ship/promote helpers exist and follow suit)
+
+```go
+// Fast-track (2026-08-04 console bake controls spec §5): re-sign the head
+// with the bake waiver — same version, same history, one new audit entry —
+// and never from a scoped key, at mint or at verification.
+func TestFastTrackWaivesTheHeadInPlace(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ shipRelease(t, s, "bab", "kass", "v2", "linux-amd64", []byte("b2"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v2"); err != nil {
+ t.Fatal(err)
+ }
+
+ ver, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev")
+ if err != nil {
+ t.Fatalf("FastTrack: %v", err)
+ }
+ if ver != "v2" {
+ t.Fatalf("FastTrack waived %q, want the head v2", ver)
+ }
+ p, err := Channel(ctx, s, pub, "bab", "kass", "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !p.FastTrack || p.Version != "v2" {
+ t.Fatalf("pointer = version %s fast_track %v, want v2 waived", p.Version, p.FastTrack)
+ }
+ // History survives — a fast-tracked head must stay rollbackable.
+ if len(p.History) == 0 || p.History[0] != "v1" {
+ t.Fatalf("history = %v, want v1 preserved", p.History)
+ }
+ // The waiver is recorded on the trail it waives.
+ if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
+ t.Fatalf("audit[0] = %+v, want a fast_track entry naming the actor", p.Audit)
+ }
+ // Idempotent: a second fast-track is a no-op success.
+ if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev"); err != nil {
+ t.Fatalf("repeated FastTrack: %v", err)
+ }
+ // The NEXT promotion starts clean — fast_track is per write, never
+ // inherited (spec §5).
+ shipRelease(t, s, "bab", "kass", "v3", "linux-amd64", []byte("b3"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v3"); err != nil {
+ t.Fatal(err)
+ }
+ if p, _ := Channel(ctx, s, pub, "bab", "kass", "stable"); p.FastTrack {
+ t.Fatal("fast_track survived onto the next promotion")
+ }
+}
+
+// Root only, both directions (mint AND verification, like the grant cap):
+// a scoped signer cannot SIGN a fast-tracked pointer, and a hand-built
+// grant-bearing wrapper whose payload carries fast_track does not VERIFY.
+func TestFastTrackIsRootOnly(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "edge", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ scoped, err := ScopedSigner(key, grantB64, pub)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Mint side: the scoped signer refuses the payload.
+ if _, err := signPointer(scoped, &ChannelPointer{
+ App: "kass", Channel: "edge", Version: "v1", FastTrack: true,
+ }); err == nil || !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a scoped signer signed a fast-tracked pointer (err=%v)", err)
+ }
+ // Verify side: build the wrapper by hand, exactly as a patched-out mint
+ // check would, and watch verification refuse it.
+ payload, _ := json.Marshal(ChannelPointer{Schema: 1, App: "kass", Channel: "edge",
+ Version: "v1", FastTrack: true, PromotedAt: timeNowTestHelper()})
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ sg, err := parseGrantBlob(grantB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ wrapper, _ := json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(key, []byte(b64))),
+ Grant: sg,
+ })
+ if _, err := verifyPointer(pub, wrapper, "kass", "edge"); err == nil ||
+ !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a scoped-signed fast_track pointer verified (err=%v)", err)
+ }
+}
+
+// The wire change is additive both ways: a pre-change pointer still parses
+// (no fast_track key at all), and a fast-tracked pointer parses in a struct
+// that predates the field — the old binary simply keeps baking.
+func TestFastTrackIsAdditiveOnTheWire(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", ""); err != nil {
+ t.Fatal(err)
+ }
+ raw, _, err := GetQualified(ctx, s, ChannelKey("bab", "kass", "stable"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var sw signedWrapper
+ if err := json.Unmarshal(raw, &sw); err != nil {
+ t.Fatal(err)
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The old binary's shape: every pre-fast_track field, nothing else.
+ var old struct {
+ Schema int `json:"schema"`
+ App string `json:"app"`
+ Channel string `json:"channel"`
+ Version string `json:"version"`
+ }
+ if err := json.Unmarshal(payload, &old); err != nil {
+ t.Fatalf("an old struct shape failed to parse a fast-tracked payload: %v", err)
+ }
+ if old.Version != "v1" {
+ t.Fatalf("old shape read version %q", old.Version)
+ }
+}
+```
+
+(If `release_test.go` lacks any imports these use — `strings`, `encoding/base64`, `encoding/json`, `crypto/ed25519` — add them. If `timeNowTestHelper` from Task 2 isn't visible because Task 2 hasn't merged, define the same helper locally; delete the duplicate at integration.)
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/release/ -run TestFastTrack -count=1`. Expected: compile error (`unknown field FastTrack`).
+
+- [ ] **Step 3: Implement.** Four edits in `internal/release/release.go`:
+
+(a) `ChannelPointer` — after the `Audit` field:
+
+```go
+ // FastTrack waives the supply-chain bake window for THIS version on THIS
+ // channel (2026-08-04 console bake controls spec §5). Root-key-only at
+ // mint AND at verification — a scoped key must never skip the window it
+ // bakes under. Set per write and never inherited: the next promotion
+ // starts clean. ADDITIVE: old pointers have no key; old binaries drop the
+ // field and keep baking, the safe direction.
+ FastTrack bool `json:"fast_track,omitempty"`
+```
+
+(b) `Promotion` — after the `Hotfix` field:
+
+```go
+ // FastTrack records that this write waived the bake window — same
+ // reasoning as Hotfix: a bypass nobody can see is a broken gate.
+ FastTrack bool `json:"fast_track,omitempty"`
+```
+
+(c) `signPointer` — after the `sg.allows(...)` check:
+
+```go
+ if p.FastTrack && sg.scope != nil {
+ return nil, fmt.Errorf("fast-track is root-key-only — key grant %q cannot waive the bake window", sg.scope.Name)
+ }
+```
+
+(d) `verifyPointer` — after the `validatePointer` call and before the `if scope != nil` block:
+
+```go
+ if p.FastTrack && sw.Grant != nil {
+ return nil, fmt.Errorf("channel pointer %s/%s: fast_track is root-key-only — a scoped key cannot waive the bake window", p.App, p.Channel)
+ }
+```
+
+(e) The verb — after `Rollback` (follow its shape exactly):
+
+```go
+// FastTrack re-signs a channel's current pointer with the bake-window
+// waiver set — same version, same history, one new audit entry (the
+// Rollback precedent: a write that is not a promotion stays out of the
+// ladder). Root key only: the window defends subscribers against exactly
+// the keys that are not the root, so this takes priv directly and never a
+// Signer. A head already waived is a no-op success. Returns the version
+// waived either way.
+//
+// signPointer re-stamps PromotedAt, which is correct here: the bake clock
+// keys on the box's own first sighting of the VERSION (bake.go), which this
+// write does not change, and the console suppresses its bakes-until
+// estimate for a waived head.
+func FastTrack(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app, channel, actor string) (string, error) {
+ sg := RootSigner(priv)
+ if !ValidAccount(acct) {
+ return "", fmt.Errorf("fast-track: invalid account %q", acct)
+ }
+ if !ValidApp(app) || !ValidChannel(channel) {
+ return "", fmt.Errorf("fast-track: invalid app/channel %s/%s", app, channel)
+ }
+ key := ChannelKey(acct, app, channel)
+ cur, etag, err := GetQualified(ctx, s, key)
+ if err != nil {
+ return "", fmt.Errorf("fast-track: %w", err)
+ }
+ old, err := verifyPointer(sg.verifyPub(), cur, app, channel)
+ if err != nil {
+ return "", fmt.Errorf("fast-track: existing pointer at %s: %w", key, err)
+ }
+ if old.FastTrack {
+ return old.Version, nil
+ }
+ rung, _ := Rung(channel)
+ next := &ChannelPointer{
+ App: app, Channel: channel, Version: old.Version,
+ History: old.History, FastTrack: true,
+ Audit: prependCapped(Promotion{
+ Version: old.Version, From: rung, To: rung, Actor: actor,
+ FastTrack: true, At: time.Now().UTC().Truncate(time.Second),
+ }, old.Audit, auditMax),
+ }
+ wrapper, err := signPointer(sg, next)
+ if err != nil {
+ return "", err
+ }
+ if _, err := s.Put(ctx, key, wrapper, etag); err != nil {
+ return "", err
+ }
+ return old.Version, nil
+}
+```
+
+- [ ] **Step 4: Run the package** — `go test ./internal/release/ -count=1`. PASS, including `laddercheck_test.go` and `golden_test.go` (the golden test pins root-signed bytes for pointers WITHOUT fast_track — `omitempty` keeps those byte-identical; if it fails, the field placement is wrong, not the golden file).
+
+- [ ] **Step 5: Commit** — `git add internal/release/release.go internal/release/release_test.go && git commit -m "release: fast_track waives the bake window — root-only at mint and at verification"` (+ trailers).
+
+---
+
+### Task 4: the box's production memory and the ratchet (spec §4)
+
+**Files:**
+- Modify: `internal/release/bake.go` (FirstSeen struct ~line 70, LoadFirstSeen ~line 118; new section at the bottom)
+- Test: `internal/release/bake_test.go` (append)
+
+**Interfaces:**
+- Consumes: `ProductionFlag` (Task 2), `HoldFor`, `RungStable`, `clockSep`, `FirstSeen.Jitter`.
+- Produces (Task 5 calls these exactly):
+ - `func (st *FirstSeen) ObserveProduction(acct, app string, flag *ProductionFlag, now time.Time) bool` — folds a VERIFIED flag into memory; nil flag is a no-op; returns changed (caller Saves).
+ - `func (st *FirstSeen) ProductionBake(acct, app string, flagErr error, hasBakeableHead bool, holds map[string]time.Duration, now time.Time) bool` — whether the window applies to this app on this box right now. Call ObserveProduction FIRST.
+
+- [ ] **Step 1: Write the failing tests** (append to `internal/release/bake_test.go`)
+
+```go
+// The box-side production memory (2026-08-04 console bake controls spec §4).
+// Scenario table, because the security content IS the state machine: the
+// default is off; on is instant; off ratchets on the box's own clock; a
+// DELETED flag never downgrades a box that saw true; a stale replayed off
+// cannot roll back a newer on.
+func TestProductionMemoryRatchet(t *testing.T) {
+ base := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
+ flag := func(prod bool, setAt time.Time) *ProductionFlag {
+ return &ProductionFlag{Schema: 1, Account: "bab", App: "kass",
+ Production: prod, SetAt: setAt}
+ }
+ newClock := func() *FirstSeen {
+ st := LoadFirstSeen(filepath.Join(t.TempDir(), "clock.json"))
+ st.Jitter = 0 // determinism: eligibility is exactly firstSeen + hold
+ return st
+ }
+
+ t.Run("default is off", func(t *testing.T) {
+ st := newClock()
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("an app with no flag ever seen bakes")
+ }
+ })
+
+ t.Run("unverifiable flag fails toward baking", func(t *testing.T) {
+ st := newClock()
+ if !st.ProductionBake("bab", "kass", fmt.Errorf("sig"), true, DefaultHolds, base) {
+ t.Fatal("a corrupt flag read as the default instead of production")
+ }
+ })
+
+ t.Run("on is instant, deletion is not a downgrade", func(t *testing.T) {
+ st := newClock()
+ if !st.ObserveProduction("bab", "kass", flag(true, base), base) {
+ t.Fatal("first observation did not change the clock")
+ }
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("production: true did not apply immediately")
+ }
+ // The object vanishes from the bucket (flag == nil every later pass):
+ // the box holds its verdict.
+ st.ObserveProduction("bab", "kass", nil, base.Add(time.Hour))
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(time.Hour)) {
+ t.Fatal("deleting the flag object downgraded a box that saw true")
+ }
+ })
+
+ t.Run("off ratchets for the stable window", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ off := flag(false, base.Add(time.Hour))
+ st.ObserveProduction("bab", "kass", off, base.Add(2*time.Hour)) // box sees it an hour late
+ hold := HoldFor(DefaultHolds, RungStable)
+ // Still baking just inside the window, measured from the box's OWN
+ // sighting (base+2h), not the flag's set_at (base+1h).
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold-time.Minute)) {
+ t.Fatal("off was honored before the ratchet elapsed")
+ }
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold+time.Minute)) {
+ t.Fatal("off was not honored after the ratchet elapsed")
+ }
+ })
+
+ t.Run("no bakeable head skips the ratchet", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ st.ObserveProduction("bab", "kass", flag(false, base.Add(time.Hour)), base.Add(time.Hour))
+ if st.ProductionBake("bab", "kass", nil, false, DefaultHolds, base.Add(time.Hour)) {
+ t.Fatal("carve-out: nothing at a held head, yet off still ratcheted")
+ }
+ })
+
+ t.Run("a stale off cannot be replayed over a newer on", func(t *testing.T) {
+ st := newClock()
+ staleOff := flag(false, base)
+ st.ObserveProduction("bab", "kass", staleOff, base)
+ st.ObserveProduction("bab", "kass", flag(true, base.Add(time.Hour)), base.Add(time.Hour))
+ // Attacker re-writes the old signed off object; the box refuses to
+ // regress below the newest set_at it has honored.
+ if st.ObserveProduction("bab", "kass", staleOff, base.Add(2*time.Hour)) {
+ t.Fatal("a replayed stale flag changed the clock")
+ }
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(100*time.Hour)) {
+ t.Fatal("a replayed stale off eventually turned the bake off")
+ }
+ })
+
+ t.Run("memory survives a save/load round trip", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ if err := st.Save(); err != nil {
+ t.Fatal(err)
+ }
+ st2 := LoadFirstSeen(st.path)
+ if !st2.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("production memory did not survive persistence")
+ }
+ })
+}
+```
+
+(`filepath`, `fmt` may need importing; `st.path` is unexported but the test is in-package.)
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/release/ -run TestProductionMemoryRatchet -count=1`. Expected: compile errors.
+
+- [ ] **Step 3: Implement** — three edits in `internal/release/bake.go`:
+
+(a) `FirstSeen` struct, after `Channels`:
+
+```go
+ // Production is the per-app production-flag memory (2026-08-04 console
+ // bake controls spec §4), keyed "<acct>|<app>" with clockSep — the same
+ // collision argument as channelClockKey. ADDITIVE: old clock files have
+ // no key, old binaries drop this one.
+ Production map[string]*prodMemory `json:"production,omitempty"`
+```
+
+(b) `LoadFirstSeen`, next to the `st.Channels == nil` guard:
+
+```go
+ if st.Production == nil {
+ st.Production = map[string]*prodMemory{}
+ }
+```
+
+(c) New section at the bottom of the file:
+
+```go
+// ---- production memory ------------------------------------------------------
+
+// prodMemory is one app's production-flag memory on this box. It exists to
+// hold two properties no bucket read can: DELETION IS NOT A DOWNGRADE (once
+// a verified production: true is seen, absence never clears it — anyone
+// with bucket write but no key can delete an object), and A STALE FLAG
+// CANNOT BE REPLAYED (an off from a past era, re-written by someone with
+// bucket access alone, must not roll back a newer on).
+type prodMemory struct {
+ // WasProduction latches on the first verified production: true.
+ WasProduction bool `json:"was_production,omitempty"`
+ // NewestSetAt is the replay floor: the newest set_at this box has
+ // honored. Anything at or below it is old news.
+ NewestSetAt time.Time `json:"newest_set_at,omitempty"`
+ // OffSetAt identifies the current off-flag edition; OffFirstSeen is when
+ // THIS BOX first saw it — the ratchet clock, the box's own observation,
+ // never the flag's timestamp (same argument as the bake clock itself).
+ OffSetAt time.Time `json:"off_set_at,omitempty"`
+ OffFirstSeen time.Time `json:"off_first_seen,omitempty"`
+}
+
+func prodKey(acct, app string) string { return acct + clockSep + app }
+
+// ObserveProduction folds one VERIFIED flag read into this box's memory.
+// nil (no object in the bucket) changes nothing — absence is not evidence.
+// Returns whether anything changed; the caller persists via Save. Never
+// call it with a flag that failed verification: like Observe, the memory
+// only ever ticks for signed documents.
+func (st *FirstSeen) ObserveProduction(acct, app string, flag *ProductionFlag, now time.Time) bool {
+ if flag == nil {
+ return false
+ }
+ key := prodKey(acct, app)
+ mem := st.Production[key]
+ if mem == nil {
+ mem = &prodMemory{}
+ }
+ // The replay floor. Equal set_at is the object already recorded — the
+ // every-pass re-read — so nothing changes; older is a replay and is
+ // ignored outright.
+ if !flag.SetAt.After(mem.NewestSetAt) {
+ return false
+ }
+ mem.NewestSetAt = flag.SetAt
+ if flag.Production {
+ mem.WasProduction = true
+ mem.OffSetAt, mem.OffFirstSeen = time.Time{}, time.Time{}
+ } else {
+ mem.OffSetAt = flag.SetAt
+ mem.OffFirstSeen = now.UTC().Truncate(time.Second)
+ }
+ st.Production[key] = mem
+ return true
+}
+
+// ProductionBake is the adopt pass's one question: does the supply-chain
+// window apply to this app on this box right now? Call ObserveProduction
+// first — this reads memory, plus flagErr, which is non-nil when a flag
+// object EXISTS but would not read or verify: that fails toward baking (an
+// attacker who can corrupt but not sign must not shorten anything).
+// hasBakeableHead is the spec §4 carve-out: with no held channel head
+// resolved this pass, an off-flag needs no ratchet — there is nothing at a
+// baked head to protect.
+func (st *FirstSeen) ProductionBake(acct, app string, flagErr error, hasBakeableHead bool, holds map[string]time.Duration, now time.Time) bool {
+ if flagErr != nil {
+ return true
+ }
+ mem := st.Production[prodKey(acct, app)]
+ if mem == nil {
+ return false // never declared production — the default
+ }
+ if mem.OffSetAt.IsZero() {
+ // The newest verified word is production: true — or the flag has
+ // vanished since, which never downgrades (WasProduction latches).
+ return mem.WasProduction
+ }
+ // The newest verified word is production: false.
+ if !mem.WasProduction {
+ return false // off, and never was on: nothing to ratchet
+ }
+ if !hasBakeableHead {
+ return false // the carve-out
+ }
+ // The ratchet: off is honored only after the stable window has elapsed
+ // on this box's own sighting of the off-flag, jittered like any hold —
+ // the flag defends itself with the mechanism it controls.
+ d := HoldFor(holds, RungStable)
+ eligible := mem.OffFirstSeen.Add(time.Duration(float64(d) * (1 + st.Jitter)))
+ return now.Before(eligible)
+}
+```
+
+- [ ] **Step 4: Run the package** — `go test ./internal/release/ -count=1`. PASS.
+
+- [ ] **Step 5: Commit** — `git add internal/release/bake.go internal/release/bake_test.go && git commit -m "release: production memory on the bake clock — off ratchets, deletion never downgrades, replays refused"` (+ trailers).
+
+---
+
+### Task 5: adopt wiring — the `ignore` parameter finally gets its caller (spec §6) — OPUS
+
+**Files:**
+- Modify: `internal/release/adopt.go` (PassFilter ~lines 113–253, adoptHost ~lines 303–376)
+- Test: `internal/release/adopt_test.go` (append; also UPDATE `TestPassStableHeldByBakeWindow` ~line 187)
+
+**Interfaces:**
+- Consumes: `GetProduction`, `ObserveProduction`, `ProductionBake` (Tasks 2/4), `ChannelPointer.FastTrack` (Task 3), `HoldFor`, `Hold`.
+- Produces: `adoptHost` gains a trailing `ignore bool` parameter. No exported surface changes.
+
+**Behavior change to own explicitly:** with the flag defaulting to off, a beta/stable head with no production flag installs immediately. `TestPassStableHeldByBakeWindow` currently pins the old behavior and MUST be updated (not deleted): it becomes the production-on case.
+
+- [ ] **Step 1: Update the existing test and add the new ones** (in `internal/release/adopt_test.go`)
+
+Rewrite `TestPassStableHeldByBakeWindow` (keep the name — the property survives, it just needs the flag on now):
+
+```go
+func TestPassStableHeldByBakeWindow(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatalf("Promote: %v", err)
+ }
+ // The app is DECLARED production (2026-08-04 spec §2): the window is no
+ // longer unconditional — it is what the flag buys.
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatalf("SetProduction: %v", err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false) // "" = stable
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ // A brand-new stable head is inside its 72h supply-chain window: the
+ // clock started this pass, nothing installs.
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want unmoved (bake hold)", v)
+ }
+ // The pass persisted the first sighting so the window is measured from
+ // the box's own clock.
+ st := LoadFirstSeen(FirstSeenPath(opt))
+ if _, ok := st.EligibleAt("bab", "myapp", "stable", "v1", DefaultHolds); !ok {
+ t.Fatalf("Pass did not tick the first-seen clock")
+ }
+}
+```
+
+Append the new tests:
+
+```go
+// The default: no flag, no window (2026-08-04 spec §2). The whole fleet
+// runs on canary/* today, so nothing live loses protection — and a
+// rapid-iteration app's stable is just another channel.
+func TestPassNoProductionFlagInstallsImmediately(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
+ t.Fatalf("live = %q, want v1 — no production flag means no window", v)
+ }
+}
+
+// Fast-track: a production app's held head installs the moment its pointer
+// carries the root-signed waiver (spec §5).
+func TestPassFastTrackSkipsTheWindow(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want held (production, no waiver yet)", v)
+ }
+ if _, err := FastTrack(ctx, s, priv, "bab", "myapp", "stable", "test"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
+ t.Fatalf("live = %q, want v1 — the waiver did not skip the window", v)
+ }
+}
+
+// A production app whose flag OBJECT rots (exists, won't verify) keeps
+// baking — an attacker who can corrupt but not sign shortens nothing.
+func TestPassCorruptProductionFlagFailsTowardBaking(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "myapp"), []byte("not a wrapper"), ""); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want held — a corrupt flag must read as production", v)
+ }
+}
+```
+
+- [ ] **Step 2: Run, verify the new tests fail** — `go test ./internal/release/ -run 'TestPassNoProductionFlag|TestPassFastTrack|TestPassCorruptProduction' -count=1`. Expected: FAIL (`TestPassNoProductionFlag` fails because today the head is held; the others fail or pass vacuously — confirm at least one real failure before implementing).
+
+- [ ] **Step 3: Implement in `PassFilter`.** After the pointer-resolution loop and BEFORE the bake-clock tick block, insert:
+
+```go
+ // Resolve each app's production flag once per pass — but only for apps
+ // with a HELD head resolved this pass (spec §6). The 2-3s canary fast
+ // loop filters to canary/* routes, which have no hold, so it never pays
+ // this read; the hourly pass pays one read per held app.
+ type acctApp2 struct{ acct, app string }
+ type prodState struct {
+ flag *ProductionFlag
+ err error // non-nil: object exists but would not read or verify
+ }
+ bakeable := map[acctApp2]bool{}
+ for k, ptr := range pointers {
+ if ptr != nil && HoldFor(DefaultHolds, k.channel) > 0 {
+ bakeable[acctApp2{k.acct, k.app}] = true
+ }
+ }
+ prod := map[acctApp2]prodState{}
+ for ak := range bakeable {
+ f, err := GetProduction(ctx, s, pub, ak.acct, ak.app)
+ prod[ak] = prodState{flag: f, err: err}
+ }
+```
+
+NOTE: `PassFilter` already declares a local `type acctApp struct{ acct, app string }` for the migration cache — REUSE that type instead of declaring `acctApp2` (the snippet names it `acctApp2` only because the plan cannot see your merge; in the file, use the existing `acctApp`, moving its declaration up if needed so both uses see it).
+
+In the bake-clock tick block (`for k, ptr := range pointers { ... }`), add after that loop, before `if changed {`:
+
+```go
+ for ak, ps := range prod {
+ if ps.err == nil && bake.ObserveProduction(ak.acct, ak.app, ps.flag, now) {
+ changed = true
+ }
+ }
+```
+
+Then compute `ignore` at both `adoptHost` call sites (awake and hibernating). Replace each call with:
+
+```go
+ ak := acctApp{RouteAccount(rt), rt.App}
+ ignore := !bake.ProductionBake(ak.acct, ak.app, prod[ak].err, bakeable[ak], DefaultHolds, now)
+ adoptHost(ctx, reg, s, optDir, bake, poisoned, rt, ptr, restart, now, cfg.nag, ignore)
+```
+
+(hibernating site passes `nil` for restart, as today.)
+
+- [ ] **Step 4: Implement in `adoptHost`.** Change the signature's tail from `now time.Time, nag io.Writer)` to `now time.Time, nag io.Writer, ignore bool)`, document the parameter in the function comment (`ignore is the app-level bake verdict from ProductionBake — the production flag's doing, never a publisher field`), and change the Hold call to:
+
+```go
+ if hold := Hold(bake, RouteAccount(rt), rt.App, channel, ver, DefaultHolds, ignore || ptr.FastTrack, now); hold != "" {
+```
+
+- [ ] **Step 5: Run the whole package** — `go test ./internal/release/ -count=1`. PASS, including the updated `TestPassStableHeldByBakeWindow` and everything in `laddercheck_test.go`.
+
+- [ ] **Step 6: Commit** — `git add internal/release/adopt.go internal/release/adopt_test.go && git commit -m "release: adopt honors the production flag and fast_track — Hold's ignore finally has its caller"` (+ trailers).
+
+---
+
+### Task 6: console view plumbing (spec §7 data)
+
+**Files:**
+- Modify: `internal/console/view.go` (AppView ~line 60, ChannelView ~line 126, Assemble ~lines 184–258, assembleChannels ~lines 290–335), `internal/console/ladder.go` (LadderChannel ~line 51, assembleLadder ~line 124)
+- Test: `internal/console/view_test.go` if it exists, else assert through `internal/console/ladder_test.go` in Task 8's page tests (view fields are exercised there; this task still needs `go build` + existing tests green)
+
+**Interfaces:**
+- Consumes: `release.GetProduction`, `release.HoldFor`, `release.DefaultHolds`, `release.RungStable`, `release.Rung`, `prodWiredCanaryHosts` (ladder.go), `ChannelView`, `AppView`.
+- Produces (Task 7/8 read these exactly):
+ - `type ProductionView struct { On, TurningOff bool; EffectiveAt, SetAt time.Time; Nudge bool; Err string }` in view.go
+ - `AppView.Production ProductionView`
+ - `ChannelView.FastTrack bool`
+ - `LadderChannel.FastTrack bool` and `LadderChannel.BakedUntil time.Time`
+
+- [ ] **Step 1: Add the types.** In `view.go`, add to `ChannelView` after `Hotfix`:
+
+```go
+ // FastTrack is the pointer's bake waiver (release.ChannelPointer) — the
+ // badge is permanent, like the hotfix badge.
+ FastTrack bool
+```
+
+Add after the `ChannelView` type:
+
+```go
+// ProductionView is the app's production-flag state as the console shows it
+// (2026-08-04 console bake controls spec §7).
+type ProductionView struct {
+ // On is the flag as the bucket has it; the default (no object) is off.
+ On bool
+ // TurningOff marks an off-flag younger than the stable window while the
+ // app still has held channels: hosts may still be ratcheting.
+ // EffectiveAt is the ~estimate shown (set_at + the stable hold) — each
+ // box's own clock and jitter govern, exactly as BakedUntil is only ever
+ // an approximation.
+ TurningOff bool
+ EffectiveAt time.Time
+ SetAt time.Time
+ // Nudge: the app looks production-wired while bake is off. Detection
+ // recommends; it never flips the flag.
+ Nudge bool
+ // Err is a flag object that exists but will not verify — shown, and
+ // boxes treat it as production (they keep baking).
+ Err string
+}
+```
+
+Add `Production ProductionView` to `AppView` (after `Ladder`).
+
+In `ladder.go`, add to `LadderChannel` after `Hotfix`:
+
+```go
+ // FastTrack and BakedUntil carry the channel's waiver and its ~hold
+ // estimate into the rung row, where the fast-track control lives.
+ FastTrack bool
+ BakedUntil time.Time
+```
+
+- [ ] **Step 2: Wire assembly.** In `assembleChannels`, in the ChannelView build, add `FastTrack: p.FastTrack` to the `cv := ChannelView{...}` literal.
+
+In `Assemble`'s per-app loop, replace the `out = append(out, AppView{...})` block with:
+
+```go
+ pv := assembleProduction(ctx, s, pub, acct, app, channels, byApp[app], now)
+ if pv.Err == "" && !pv.On {
+ // No production flag (or off): nothing bakes, so no estimate. An
+ // Err keeps the estimate — boxes fail toward baking.
+ for i := range channels {
+ channels[i].BakedUntil = time.Time{}
+ }
+ }
+ for i := range channels {
+ if channels[i].FastTrack {
+ channels[i].BakedUntil = time.Time{} // waived — nothing to wait out
+ }
+ }
+ out = append(out, AppView{
+ App: app,
+ Routes: byApp[app], // List orders by host already
+ Domains: domainsByApp[app],
+ Channels: channels,
+ Canaries: assembleCanaries(channels, byApp[app]),
+ Ladder: assembleLadder(channels, byApp[app], now),
+ Production: pv,
+ RouteAccount: mismatch,
+ })
+```
+
+Add the helper (bottom of view.go):
+
+```go
+// assembleProduction reads the app's flag and derives the console's view of
+// it: on/off, the turning-off ratchet estimate, and the prod-wiring nudge.
+func assembleProduction(ctx context.Context, s release.Store, pub ed25519.PublicKey, acct, app string, channels []ChannelView, routes []RouteView, now time.Time) ProductionView {
+ pv := ProductionView{}
+ flag, err := release.GetProduction(ctx, s, pub, acct, app)
+ switch {
+ case err != nil:
+ pv.Err = err.Error()
+ pv.On = true // what the boxes will do: fail toward baking
+ return pv
+ case flag != nil:
+ pv.On = flag.Production
+ pv.SetAt = flag.SetAt
+ }
+ held := false
+ for _, cv := range channels {
+ if cv.Err == "" && release.HoldFor(release.DefaultHolds, cv.Name) > 0 {
+ held = true
+ break
+ }
+ }
+ if flag != nil && !flag.Production && held {
+ hold := release.HoldFor(release.DefaultHolds, release.RungStable)
+ if until := flag.SetAt.Add(hold); now.Before(until) {
+ pv.TurningOff, pv.EffectiveAt = true, until
+ }
+ }
+ if !pv.On {
+ stableRoute := false
+ for _, rt := range routes {
+ if rung, ok := release.Rung(rt.Channel); ok && rung == release.RungStable {
+ stableRoute = true
+ break
+ }
+ }
+ pv.Nudge = stableRoute || len(prodWiredCanaryHosts(routes)) > 0
+ }
+ return pv
+}
+```
+
+In `assembleLadder` (ladder.go), the `entries[rung] = append(...)` literal for readable pointers gains `FastTrack: cv.FastTrack, BakedUntil: cv.BakedUntil,`.
+
+- [ ] **Step 3: Build and run existing console tests** — `go build ./... && go test ./internal/console/ -count=1` (env from Global Constraints). PASS — existing pages must render unchanged except: any existing test asserting `BakedUntil`/"bakes until" text for an app with NO production flag will now see it empty. `grep -rn "BakedUntil\|bakes until" internal/console/` — update any such assertion to seed the flag on first (`release.SetProduction(ctx, fx.rel, fx.priv, release.OpsAccount, app, true, "test")`) and note in the test why.
+
+- [ ] **Step 4: Commit** — `git add internal/console/view.go internal/console/ladder.go internal/console/*_test.go && git commit -m "console: production flag, fast-track and bake state plumbed into the app view"` (+ trailers).
+
+---
+
+### Task 7: console handlers — flag toggle, fast-track, promote checkbox (spec §7) — OPUS
+
+**Files:**
+- Modify: `internal/console/server.go` (route table ~line 231, handlePromote ~line 1305, new handlers after handleRollback ~line 1440)
+- Test: create `internal/console/bakecontrols_test.go`
+
+**Interfaces:**
+- Consumes: `s.accountApp`, `s.requireFreshAuth`, `s.audit`, `s.setFlash`, `RoleOwner`, `release.SetProduction`, `release.FastTrack`, `release.GetProduction`, `release.ValidChannel`; test harness: `newConsoleFixture`, `signInPaul`, `sessionToken`, `postForm`, `postGated`, `completeStepUp`, `get`, `fx.u`, `fx.rel`, `fx.priv`, `fx.st`, `fx.setUser`, `newBrowser`, `signUp`, `OpsAccountID`, `RoleMember` (see ladder_test.go for usage).
+- Produces: `POST /a/{sqid}/apps/{app}/production` (form: `csrf`, `production` = `"on"`/`"off"`), `POST /a/{sqid}/apps/{app}/fasttrack` (form: `csrf`, `channel`), and `enable_bake=1` honored on `POST .../promote`. Task 8's templates post to these.
+
+**Gate order is load-bearing and reviewed against CLAUDE.md:** session → CSRF → owner (byte-identical `http.NotFound`) → `requireFreshAuth` LAST. Turning ON and the promote checkbox take any member (the safe direction is cheap); turning OFF and fast-track take owner + passkey.
+
+- [ ] **Step 1: Write the failing tests** (`internal/console/bakecontrols_test.go`)
+
+```go
+package console
+
+// The bake controls (2026-08-04 console bake controls spec §7). Properties
+// in the order they'd hurt if lost: OFF cannot happen without owner +
+// passkey (and a member probing it gets the byte-identical 404); ON is any
+// member's, instantly; fast-track is owner + passkey and lands on the
+// pointer; the stable-promote checkbox flips the flag in the same confirmed
+// action.
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func productionFlag(t *testing.T, fx *consoleFixture, app string) *release.ProductionFlag {
+ t.Helper()
+ f, err := release.GetProduction(context.Background(), fx.rel, fx.priv.Public().(ed25519.PublicKey), release.OpsAccount, app)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return f
+}
+
+// NOTE for the implementer: if the fixture exposes the public key directly
+// (check consoleFixture's fields — many tests use fx.pub or similar), use
+// that instead of deriving from fx.priv; adjust the import of crypto/ed25519
+// accordingly.
+
+func TestProductionOnIsAnyMembers(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-bake")
+ csrf := sessionToken(t, fx, c)
+
+ status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"on"}})
+ if status != http.StatusSeeOther {
+ t.Fatalf("member turning bake ON = %d, want a plain redirect (no step-up)", status)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v, want production on", f)
+ }
+}
+
+func TestProductionOffIsOwnerPasskeyGated(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+ // Seed on, as any member could.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+
+ // Without the passkey the flag does not move.
+ status, loc, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"off"}})
+ if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) {
+ t.Fatalf("un-stepped-up off = %d -> %q, want a redirect into Keymail", status, loc)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatal("the flag flipped off with no passkey confirmation")
+ }
+ // With it, the held action resumes.
+ if status, _, body := completeStepUp(t, fx, c, loc); status != http.StatusSeeOther {
+ t.Fatalf("resumed off = %d:\n%s", status, body)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || f.Production {
+ t.Fatal("the flag did not flip off after confirmation")
+ }
+}
+
+// A MEMBER probing the off direction meets the byte-identical 404 — before
+// any step-up redirect could confirm the account exists.
+func TestProductionOffMemberGets404(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-bake2")
+ csrf := sessionToken(t, fx, c)
+ status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"off"}})
+ if status != http.StatusNotFound {
+ t.Fatalf("member turning bake OFF = %d, want 404", status)
+ }
+}
+
+func TestFastTrackIsOwnerPasskeyGatedAndLands(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+ // The fixture's stable already points at v1; waive it as the owner with
+ // the full passkey round trip.
+ postGated(t, fx, c, fx.u("/apps/kass/fasttrack"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}})
+ p := fx.channelVersion(t, "kass", "stable")
+ if !p.FastTrack {
+ t.Fatalf("pointer = %+v, want fast_track set", p)
+ }
+ if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
+ t.Fatalf("audit[0] = %+v, want the waiver recorded with its actor", p.Audit)
+ }
+}
+
+// The stable-promote prompt (spec §7): enable_bake rides the same
+// passkey-confirmed promote, and flips the flag on only when it is off.
+func TestPromoteEnableBakeFlipsTheFlag(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3")
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+
+ postGated(t, fx, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}, "version": {"v3"}, "enable_bake": {"1"}})
+ if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v3" {
+ t.Fatalf("stable = %s, want v3", p.Version)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v after enable_bake promote, want production on", f)
+ }
+}
+```
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/console/ -run 'TestProduction|TestFastTrackIs|TestPromoteEnableBake' -count=1`. Expected: 404s where routes don't exist yet (real failures, not compile errors, once imports are fixed against the fixture's actual field names — check `consoleFixture` in the harness file, e.g. `authcli_test.go` or wherever `newConsoleFixture` lives, and adjust `fx.fk.srv.URL` / pub-key access to match the fixture).
+
+- [ ] **Step 3: Register routes** (server.go route table, after the rollback line):
+
+```go
+ s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/production", s.handleProduction)
+ s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/fasttrack", s.handleFastTrack)
+```
+
+- [ ] **Step 4: Implement the handlers** (after `handleRollback`):
+
+```go
+// handleProduction flips the app's production flag (2026-08-04 console bake
+// controls spec §7). ON is any member's to do, instantly — the safe
+// direction stays cheap. OFF removes a supply-chain guard from every
+// subscriber of the app, so it is the owner's, behind the passkey, and the
+// fleet ratchets it in on each box's own clock. Gate order is the mandated
+// one: session → CSRF → owner (byte-identical 404) → requireFreshAuth LAST.
+func (s *Server) handleProduction(w http.ResponseWriter, r *http.Request) {
+ addr, raw, acct, role, app, ok := s.accountApp(w, r)
+ if !ok {
+ return
+ }
+ if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
+ s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
+ return
+ }
+ if s.cfg.Key == nil {
+ s.renderMessage(w, r, http.StatusServiceUnavailable, "Promotions disabled",
+ "No signing key on this host — the console is running read-only.")
+ return
+ }
+ back := func(kind, msg string) {
+ s.setFlash(w, kind, msg)
+ http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app), http.StatusSeeOther)
+ }
+ var on bool
+ switch r.FormValue("production") {
+ case "on":
+ on = true
+ case "off":
+ on = false
+ default:
+ back("err", "Invalid production value.")
+ return
+ }
+ if !on {
+ if role != RoleOwner {
+ http.NotFound(w, r)
+ return
+ }
+ if !s.requireFreshAuth(w, r, addr, raw) {
+ return
+ }
+ }
+ ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
+ defer cancel()
+ if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, on, addr); err != nil {
+ log.Printf("console: production %s -> %v: %v", app, on, err)
+ back("err", "Could not update the production flag: "+err.Error())
+ return
+ }
+ if on {
+ s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on)")
+ back("ok", "Supply-chain bake enabled — beta and stable now hold before hosts adopt.")
+ } else {
+ s.audit(acct, app, addr, "production", "disabled the supply-chain bake (production: off)")
+ back("ok", "Supply-chain bake turning off — each host honors it after the stable window (~72h) on its own clock.")
+ }
+}
+
+// handleFastTrack waives the bake window for one channel's CURRENT head —
+// owner + passkey, always: this is the loud instant path past the window
+// (spec §5, §9), and the pointer wears the badge forever.
+func (s *Server) handleFastTrack(w http.ResponseWriter, r *http.Request) {
+ addr, raw, acct, role, app, ok := s.accountApp(w, r)
+ if !ok {
+ return
+ }
+ if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
+ s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
+ return
+ }
+ if s.cfg.Key == nil {
+ s.renderMessage(w, r, http.StatusServiceUnavailable, "Promotions disabled",
+ "No signing key on this host — the console is running read-only.")
+ return
+ }
+ back := func(kind, msg string) {
+ s.setFlash(w, kind, msg)
+ http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app), http.StatusSeeOther)
+ }
+ channel := strings.TrimSpace(r.FormValue("channel"))
+ if !release.ValidChannel(channel) {
+ back("err", "Invalid channel name.")
+ return
+ }
+ if role != RoleOwner {
+ http.NotFound(w, r)
+ return
+ }
+ if !s.requireFreshAuth(w, r, addr, raw) {
+ return
+ }
+ ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
+ defer cancel()
+ ver, err := release.FastTrack(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, channel, addr)
+ if err != nil {
+ log.Printf("console: fast-track %s %s: %v", app, channel, err)
+ back("err", "Fast-track failed: "+err.Error())
+ return
+ }
+ s.audit(acct, app, addr, "fast-track", "waived the bake window for "+ver+" on "+channel)
+ back("ok", "Fast-tracked "+ver+" on "+channel+" — hosts adopt on their next pass.")
+}
+```
+
+- [ ] **Step 5: The promote checkbox.** In `handlePromote`, after the `s.audit(...)` line and the `msg := ...` construction, insert (before `back("ok", msg)`):
+
+```go
+ // The spec §7 prompt: promote-to-stable asked "is this app in production
+ // now?" and the human said yes — in the same passkey-confirmed action.
+ // Turning ON is any member's anyway, so no extra gate; flips only when
+ // the flag is actually off, so a stray field on a repeat is a no-op.
+ if r.FormValue("enable_bake") == "1" {
+ if flag, ferr := release.GetProduction(ctx, s.cfg.Releases, s.cfg.ReleasePub, acct.Sqid, app); ferr == nil && (flag == nil || !flag.Production) {
+ if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, true, addr); err != nil {
+ log.Printf("console: enable_bake %s: %v", app, err)
+ } else {
+ s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on, at stable promote)")
+ msg += " Supply-chain bake enabled."
+ }
+ }
+ }
+```
+
+- [ ] **Step 6: Run** — `go test ./internal/console/ -count=1`. PASS.
+
+- [ ] **Step 7: Commit** — `git add internal/console/server.go internal/console/bakecontrols_test.go && git commit -m "console: bake controls — on for members, off and fast-track behind owner + passkey"` (+ trailers).
+
+---
+
+### Task 8: templates — status line, controls, badges, checkbox, nudge (spec §7 UI)
+
+**Files:**
+- Modify: `internal/console/templates/ladder.html`
+- Test: `internal/console/ladder_test.go` (append)
+
+**Interfaces:**
+- Consumes: `AppView.Production` (`.Detail.Production`), `LadderChannel.FastTrack`/`.BakedUntil`, page fields `$.CanPromote` (signing key present), `$.IsOwner`, `$.CSRF`, `$.Base`, template helpers `fmtTime`, `icon` (vendored Lucide names only — check `internal/console/icons*.go` for which names exist; `shield`, `hourglass`, `triangle-alert`, `zap` are safe bets, verify before use and pick existing ones).
+- Produces: markup the Task 7 handlers receive.
+
+- [ ] **Step 1: Write the failing tests** (append to `ladder_test.go`)
+
+```go
+// The bake status line and its controls (spec §7): off-by-default says so;
+// an owner gets the turn-on/turn-off controls; a held channel row offers
+// the owner fast-track; the stable promote form carries the enable_bake
+// prompt while the flag is off.
+func TestLadderShowsBakeStateAndControls(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3")
+ c := signInPaul(t, fx)
+
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "Supply-chain bake") {
+ t.Fatalf("no bake status line:\n%s", body)
+ }
+ if !strings.Contains(body, `action="/a/bab/apps/kass/production"`) {
+ t.Fatal("owner is offered no production toggle")
+ }
+ // Off by default, and the stable promote form asks the question.
+ if !strings.Contains(body, `name="enable_bake"`) {
+ t.Fatal("the stable promote form does not carry the enable_bake prompt")
+ }
+
+ // Flip on; the held beta head now shows its window and the fast-track
+ // control appears on it.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body = get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Fatalf("no fast-track control on a held head:\n%s", rungBlock(t, body, "beta"))
+ }
+ if strings.Contains(body, `name="enable_bake"`) {
+ t.Fatal("the enable_bake prompt survived the flag turning on")
+ }
+
+ // Waive it; the badge is permanent and the control is gone.
+ if _, err := release.FastTrack(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", "beta", "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body = get(t, c, fx.u("/apps/kass"))
+ b := rungBlock(t, body, "beta")
+ if !strings.Contains(b, "fast-tracked") {
+ t.Errorf("no permanent fast-tracked badge:\n%s", b)
+ }
+ if strings.Contains(b, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Error("a waived head still offers the fast-track control")
+ }
+}
+
+// A member sees state, never the owner-only controls (off / fast-track) —
+// but IS offered turn-on, the safe direction.
+func TestLadderBakeControlsMemberView(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-baketpl")
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "Supply-chain bake") {
+ t.Fatal("member sees no bake status")
+ }
+ if !strings.Contains(body, `name="production" value="on"`) {
+ t.Error("member is not offered the turn-on control")
+ }
+ if strings.Contains(body, `name="production" value="off"`) {
+ t.Error("member is offered the owner-only turn-off control")
+ }
+ if strings.Contains(body, `/fasttrack"`) {
+ t.Error("member is offered the owner-only fast-track control")
+ }
+}
+
+// The prod-wiring nudge: a stable route with bake off gets the persistent
+// recommendation line.
+func TestLadderNudgesProdWiredBakeOff(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ // The fixture's kass.carlosapp.dev is on stable and the flag is off (the
+ // default): that IS the nudge condition.
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "adopts releases immediately") {
+ t.Fatalf("no prod-wiring nudge:\n%s", body)
+ }
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ if _, _, body := get(t, c, fx.u("/apps/kass")); strings.Contains(body, "adopts releases immediately") {
+ t.Fatal("the nudge survived the flag turning on")
+ }
+}
+```
+
+- [ ] **Step 2: Run, verify failure** — `go test ./internal/console/ -run TestLadder -count=1`. New tests FAIL; old ladder tests still PASS.
+
+- [ ] **Step 3: Edit `ladder.html`.** Insert after the intro `<p class="small">` (line 5), a bake block:
+
+```html
+{{with .Detail.Production}}
+<div class="bake">
+ {{if .Err}}
+ <p class="error">Production flag unreadable: {{.Err}} — hosts treat this app as in production and keep baking.</p>
+ {{else if .On}}
+ <p class="small">Supply-chain bake: <strong>on</strong> — beta holds 24h and stable 72h on each host before it adopts.
+ {{if and $.CanPromote $.IsOwner}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="off">
+ <button type="submit" class="linkish">Turn off</button>
+ </form>
+ <span class="small">— confirms with your passkey; each host honors it after the stable window (~72h) on its own clock</span>
+ {{end}}
+ </p>
+ {{else if .TurningOff}}
+ <p class="small">Supply-chain bake: <strong>turning off</strong> — hosts honor it from ~{{fmtTime .EffectiveAt}}, each on its own clock.
+ {{if $.CanPromote}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="on">
+ <button type="submit" class="linkish">Turn back on</button>
+ </form>
+ {{end}}
+ </p>
+ {{else}}
+ <p class="small">Supply-chain bake: <strong>off</strong> (default) — hosts adopt releases immediately.
+ {{if $.CanPromote}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="on">
+ <button type="submit" class="linkish">Turn on</button>
+ </form>
+ {{end}}
+ </p>
+ {{end}}
+ {{if .Nudge}}
+ <p class="small nudge">{{icon "triangle-alert"}} This app looks production-wired but adopts releases immediately — consider enabling the supply-chain bake.</p>
+ {{end}}
+</div>
+{{end}}
+```
+
+In the channel row (after the Hotfix span, line 19):
+
+```html
+ {{if .FastTrack}}· fast-tracked past the bake window{{end}}</span></p>
+ {{if and (not .BakedUntil.IsZero) $.CanPromote $.IsOwner}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/fasttrack" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="channel" value="{{.Channel}}">
+ <button type="submit" class="linkish">Fast-track {{.Version}}</button>
+ </form>
+ <span class="small">— in its bake window until ~{{fmtTime .BakedUntil}}; confirms with your passkey</span>
+ {{end}}
+```
+
+(the first line replaces the existing `{{if .Hotfix}}...{{end}}</span></p>` tail — keep the Hotfix clause, append the FastTrack clause inside the same `<span class="small">`.)
+
+In the promote step form (inside the `{{if and .Up.Version $.CanPromote $.IsOwner}}` form, before the button):
+
+```html
+ {{if and (eq .Up.To "stable") (not $.Detail.Production.On)}}
+ <label class="small"><input type="checkbox" name="enable_bake" value="1" checked>
+ This app is in production — enable the supply-chain bake (recommended)</label>
+ {{end}}
+```
+
+Also `grep -n "BakedUntil" internal/console/templates/*.html` — if `app.html`'s channels table renders `BakedUntil` or a bake note, add the same `fast-tracked` clause there for consistency; if it doesn't, touch nothing.
+
+- [ ] **Step 4: Run the console package** — `go test ./internal/console/ -count=1`. PASS.
+
+- [ ] **Step 5: Commit** — `git add internal/console/templates/ladder.html internal/console/ladder_test.go && git commit -m "console: bake status line, production toggle, fast-track control and the stable-promote prompt"` (+ trailers).
+
+---
+
+### Task 9: full verification, docs sync, review — OPUS
+
+**Files:**
+- Verify all; no planned source edits (fix what verification finds).
+
+- [ ] **Step 1: Full gate** — from the worktree root:
+
+```
+GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
+GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
+GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1
+```
+
+All 14+ packages PASS. Then `gofmt -l internal/ *.go` — none of the files this branch touched may appear (the three pre-existing unformatted files elsewhere are left alone).
+
+- [ ] **Step 2: Spec cross-check** — reread `docs/superpowers/specs/2026-08-04-console-bake-controls-design.md` section by section and point at the code satisfying each of §2–§8. Confirm specifically: (a) a box with `WasProduction` latched never downgrades on a missing object; (b) `verifyPointer` refuses grant-bearing `fast_track` wrappers; (c) the gate order in both new handlers is session → CSRF → owner 404 → step-up; (d) the canary fast loop pays no production-flag reads (only held-channel apps are fetched); (e) `laddercheck_test.go` untouched.
+
+- [ ] **Step 3: Commit anything the check fixed**, then run the repo's review skill (superpowers:requesting-code-review) over the branch diff with Opus as reviewer — this branch grants authority and touches production paths, which is exactly the CLAUDE.md case for an Opus review.
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..592b20e7b
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
@@ -0,0 +1,277 @@
+# 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 same additive rule applies to the box's own bake-clock
+file: an old binary that Saves it drops the production key it doesn't know
+about until that box is restarted onto the new binary; the loss self-heals
+in the closed direction (§4).
+
+**The flag object** is a new signed bucket object per app, in the same Ed25519
+envelope as channel pointers, root-key-signed:
+
+```json
+{"schema": 1, "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**: a dedicated `FastTrack` verb re-signs the
+channel's current pointer in place — same version, same history, the waiver
+set, one new audit entry (the `Rollback` precedent: a write that is not a
+promotion stays out of the ladder). Boxes then skip the hold for exactly
+that version on that channel.
+
+- An already-fast-tracked head is a no-op success, like a repeated promote.
+- **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". Detection recommends;
+ it never flips the flag (implicit gates are the thing this platform keeps
+ refusing). The fleet page renders the published status snapshot, not
+ console-assembled state, so the nudge lives on the app page; a fleet-page
+ line can follow whenever the snapshot shape next changes.
+
+## 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).
+
+The member-race asymmetry is accepted, not overlooked. Turning the flag ON is
+any member's and takes effect immediately; turning it OFF is owner + passkey
+and then a 72h ratchet on every box. So a member who wants the window kept can
+re-impose it the instant an owner removes it, and can keep doing so — each
+flip costing them one click and the owner a passkey confirmation plus another
+three days. That asymmetry is the same fail-safe direction the rest of the
+design leans on (the cheap, instant move is always the protective one), and
+the remedies are the ordinary ones: remove the member, and fast-track the
+individual releases that need to ship meanwhile — fast-track is per release
+and unaffected by the standing flag. Nothing here is quiet: every flip in
+either direction is audited with its actor and displayed on the app page, so a
+member re-imposing the window against an owner is visible in the trail rather
+than a mystery about why the fleet will not adopt.
+
+## 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.
diff --git a/e2e_test.go b/e2e_test.go
index d7dcc6277..4e992452a 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -1025,6 +1025,100 @@ func TestAccountsMigrateTakesTheLiveObjects(t *testing.T) {
}
}
+// The production flag does NOT travel. Its signed payload names the account,
+// so a byte copy is unverifiable at the destination forever — and an
+// unverifiable flag is exactly what every box reads as "keep baking", so the
+// migrated app's held heads would sit in a window with no expiry, silently
+// (verifyMigratedPointers checks channel pointers, not this). The verb skips
+// it and says so, loudly, because the alternative failure is invisible.
+func TestAccountsMigrateDoesNotCopyTheProductionFlag(t *testing.T) {
+ goBin, err := exec.LookPath("go")
+ if err != nil {
+ t.Skip("go toolchain not found")
+ }
+ tmp := t.TempDir()
+ carlosBin := filepath.Join(tmp, "carlos")
+ build := exec.Command(goBin, "build", "-o", carlosBin, ".")
+ build.Env = os.Environ()
+ if out, err := build.CombinedOutput(); err != nil {
+ t.Fatalf("build carlos: %v\n%s", err, out)
+ }
+ storeDir := filepath.Join(tmp, "store")
+ regPath := filepath.Join(tmp, "registry.db")
+ store := release.NewDirStore(storeDir)
+ ctx := context.Background()
+
+ seed, pubB64, err := release.GenerateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ priv, err := release.ParseSigningKey(seed, "e2e")
+ if err != nil {
+ t.Fatal(err)
+ }
+ pub, err := release.ParsePublicKey(pubB64, "e2e")
+ if err != nil {
+ t.Fatal(err)
+ }
+ env := append(os.Environ(),
+ "CARLOS_DEPLOYMENT_DIR="+storeDir,
+ "CARLOS_RELEASE_PUBKEY="+pubB64)
+ run := func(args ...string) (string, error) {
+ t.Helper()
+ cmd := exec.Command(carlosBin, args...)
+ cmd.Env = env
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+ }
+
+ m := release.Manifest{Schema: 1, App: "demo", Version: "v1", Kind: "binary",
+ Artifacts: map[string]release.Artifact{"linux-amd64": {File: "demo"}}}
+ if err := release.Ship(ctx, store, release.OpsAccount, m, map[string][]byte{"linux-amd64": []byte("v1 bytes")}); err != nil {
+ t.Fatal(err)
+ }
+ if err := release.Promote(ctx, store, priv, release.OpsAccount, "demo", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := release.SetProduction(ctx, store, priv, release.OpsAccount, "demo", true, "paul@keymail.dev"); err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := run("accounts", "create", "-registry", regPath, "-name", "Kass", "-owner", "paul@keymail.dev")
+ if err != nil {
+ t.Fatalf("accounts create: %v\n%s", err, out)
+ }
+ sqid := regexp.MustCompile(`(?m)^\s*sqid:\s*(\S+)`).FindStringSubmatch(out)[1]
+
+ out, err = run("accounts", "migrate", "-registry", regPath, "-app", "demo", "-to", sqid)
+ if err != nil {
+ t.Fatalf("migrate: %v\n%s", err, out)
+ }
+ // No flag object at the destination at all — a copied one would be worse
+ // than none: none defaults to "not production", a copy bakes forever.
+ if _, _, err := store.Get(ctx, release.ProductionKey(sqid, "demo")); err == nil {
+ t.Fatal("the migration copied production.json — the copy names the source account and can never verify")
+ }
+ if f, err := release.GetProduction(ctx, store, pub, sqid, "demo"); err != nil || f != nil {
+ t.Fatalf("destination flag = %+v, %v — want the clean default (no object, no error)", f, err)
+ }
+ // And the operator is TOLD, because nothing downstream will notice.
+ want := "demo: carried a production declaration — re-declare it at the destination"
+ if !strings.Contains(out, want) {
+ t.Fatalf("migrate output does not name the flag it left behind (%q):\n%s", want, out)
+ }
+ // The source is untouched, as with every other object this verb moves.
+ if f, err := release.GetProduction(ctx, store, pub, release.OpsAccount, "demo"); err != nil || f == nil || !f.Production {
+ t.Fatalf("source flag = %+v, %v after the move, want it untouched and still on", f, err)
+ }
+ // The rest of the app still came across.
+ if _, err := release.GetManifest(ctx, store, sqid, "demo", "v1"); err != nil {
+ t.Errorf("manifest missing after the move: %v", err)
+ }
+ if p, err := release.Channel(ctx, store, pub, sqid, "demo", "stable"); err != nil || p.Version != "v1" {
+ t.Errorf("migrated stable = %+v, %v, want v1", p, err)
+ }
+}
+
// A destination channel pointer that disagrees with the source must CONVERGE,
// not strand the operator. Channel pointers are the one mutable thing in the
// bucket — they move on every promote — so a stale or half-written one at the
diff --git a/internal/console/bakecontrols_test.go b/internal/console/bakecontrols_test.go
new file mode 100644
index 000000000..d62dc69e0
--- /dev/null
+++ b/internal/console/bakecontrols_test.go
@@ -0,0 +1,354 @@
+package console
+
+// The bake controls (2026-08-04 console bake controls spec §7). Properties
+// in the order they'd hurt if lost: OFF cannot happen without owner +
+// passkey (and a member probing it gets the byte-identical 404); ON is any
+// member's, instantly; fast-track is owner + passkey and lands on the
+// pointer; the stable-promote checkbox flips the flag in the same confirmed
+// action.
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// productionFlag reads the app's flag straight from the bucket, through the
+// same verification the adopt path uses. The fixture carries the release
+// public key, so there is nothing to derive.
+func productionFlag(t *testing.T, fx *consoleFixture, app string) *release.ProductionFlag {
+ t.Helper()
+ f, err := release.GetProduction(context.Background(), fx.rel, fx.pub, release.OpsAccount, app)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return f
+}
+
+// strangerRefusal is what someone with no membership at all gets from one of
+// this account's app routes: a signed-in browser belonging to nobody here.
+// It is the yardstick the member's refusal must match byte for byte.
+func strangerRefusal(t *testing.T, fx *consoleFixture, code, u string, form url.Values) string {
+ t.Helper()
+ fx.fk.mu.Lock()
+ prev := fx.fk.address
+ fx.fk.mu.Unlock()
+ fx.setUser("zoe@keymail.dev") // signs up into an account of her own
+ defer fx.setUser(prev)
+ c := newBrowser(t)
+ signUp(t, fx, c, code)
+ form.Set("csrf", sessionToken(t, fx, c))
+ status, _, body := postForm(t, c, u, form)
+ if status != http.StatusNotFound {
+ t.Fatalf("non-member posting %s = %d, want 404", u, status)
+ }
+ return body
+}
+
+// auditedBy asserts the console's OWN activity trail (store events, which is
+// what the app page renders) recorded this action for this actor. The
+// pointer's audit trail is a different record with a different purpose; a
+// console action that leaves no console event is an invisible one.
+func auditedBy(t *testing.T, fx *consoleFixture, app, action, actor, detailPart string) {
+ t.Helper()
+ events, err := fx.st.Events(OpsAccountID, app, 20)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range events {
+ if e.Action == action && e.Address == actor && strings.Contains(e.Detail, detailPart) {
+ return
+ }
+ }
+ t.Fatalf("events = %+v, want a %q row by %s mentioning %q", events, action, actor, detailPart)
+}
+
+func TestProductionOnIsAnyMembers(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-bake")
+ csrf := sessionToken(t, fx, c)
+
+ status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"on"}})
+ if status != http.StatusSeeOther {
+ t.Fatalf("member turning bake ON = %d, want a plain redirect (no step-up)", status)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v, want production on", f)
+ }
+ auditedBy(t, fx, "kass", "production", "sam@keymail.dev", "production: on")
+}
+
+func TestProductionOffIsOwnerPasskeyGated(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+ // Seed on, as any member could.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+
+ // Without the passkey the flag does not move.
+ status, loc, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"off"}})
+ if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) {
+ t.Fatalf("un-stepped-up off = %d -> %q, want a redirect into Keymail", status, loc)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatal("the flag flipped off with no passkey confirmation")
+ }
+ // With it, the held action resumes.
+ if status, _, body := completeStepUp(t, fx, c, loc); status != http.StatusSeeOther {
+ t.Fatalf("resumed off = %d:\n%s", status, body)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || f.Production {
+ t.Fatal("the flag did not flip off after confirmation")
+ }
+ auditedBy(t, fx, "kass", "production", "paul@keymail.dev", "production: off")
+}
+
+// A MEMBER probing the off direction meets the byte-identical 404 — before
+// any step-up redirect could confirm the account exists.
+func TestProductionOffMemberGets404(t *testing.T) {
+ fx := newConsoleFixture(t)
+ // Seeded ON, so "the flag stayed put" is an assertion about a value that
+ // could have moved rather than about an absent object.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-bake2")
+ csrf := sessionToken(t, fx, c)
+ form := url.Values{"csrf": {csrf}, "production": {"off"}}
+ status, _, memberBody := postForm(t, c, fx.u("/apps/kass/production"), form)
+ if status != http.StatusNotFound {
+ t.Fatalf("member turning bake OFF = %d, want 404", status)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v after a member's refused post, want it untouched", f)
+ }
+ // Byte-identical, not merely same-status: the difference between "not
+ // allowed" and "no such account" is not information this hands out.
+ strangerBody := strangerRefusal(t, fx, "code-zoe-bake2", fx.u("/apps/kass/production"),
+ url.Values{"production": {"off"}})
+ if memberBody != strangerBody {
+ t.Fatalf("the two refusals are not byte-identical:\n(member) %q\n(stranger) %q", memberBody, strangerBody)
+ }
+ // http.NotFound's own, well-known output — pinned so a future change to
+ // either path is caught even if it stays self-consistent.
+ if memberBody != "404 page not found\n" {
+ t.Fatalf("refusal body = %q, want the stdlib http.NotFound output", memberBody)
+ }
+}
+
+// Fast-track is owner + passkey, ALWAYS. The un-stepped-up post is what
+// proves the second half: postGated alone would stay green with the step-up
+// call deleted, because it only walks the round trip when one is demanded.
+func TestFastTrackIsOwnerPasskeyGatedAndLands(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+
+ // The fixture's stable already points at v1. Without the passkey the
+ // waiver does not happen.
+ status, loc, _ := postForm(t, c, fx.u("/apps/kass/fasttrack"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}})
+ if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) {
+ t.Fatalf("un-stepped-up fast-track = %d -> %q, want a redirect into Keymail", status, loc)
+ }
+ if p := fx.channelVersion(t, "kass", "stable"); p.FastTrack {
+ t.Fatal("the bake window was waived with no passkey confirmation")
+ }
+
+ // With it, the held action resumes and lands on the pointer.
+ if status, _, body := completeStepUp(t, fx, c, loc); status != http.StatusSeeOther {
+ t.Fatalf("resumed fast-track = %d:\n%s", status, body)
+ }
+ p := fx.channelVersion(t, "kass", "stable")
+ if !p.FastTrack {
+ t.Fatalf("pointer = %+v, want fast_track set", p)
+ }
+ if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
+ t.Fatalf("audit[0] = %+v, want the waiver recorded with its actor", p.Audit)
+ }
+ auditedBy(t, fx, "kass", "fast-track", "paul@keymail.dev", "waived the bake window for v1 on stable")
+}
+
+// Fast-tracking a channel that never bakes is refused. It looks inert —
+// adopt ignores the waiver on a zero-hold channel — but the re-sign stamps a
+// fresh PromotedAt, which is exactly what the canary TTL lease is measured
+// from: the post would quietly renew the lease and leave a permanent
+// "fast-tracked past the bake window" badge on a channel that has no window.
+func TestFastTrackRefusesAChannelWithNoBakeWindow(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "canary/x", "v3")
+ before := fx.channelVersion(t, "kass", "canary/x")
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+
+ status, _, _ := postGated(t, fx, c, fx.u("/apps/kass/fasttrack"),
+ url.Values{"csrf": {csrf}, "channel": {"canary/x"}})
+ if status != http.StatusSeeOther {
+ t.Fatalf("fast-track of a bake-free channel = %d, want the flash redirect", status)
+ }
+ if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body,
+ "Channel canary/x has no bake window — there is nothing to fast-track.") {
+ t.Fatalf("no refusal flash:\n%s", body)
+ }
+ after := fx.channelVersion(t, "kass", "canary/x")
+ if after.FastTrack {
+ t.Fatal("a bake-free channel wears the waiver badge")
+ }
+ if !after.PromotedAt.Equal(before.PromotedAt) {
+ t.Fatalf("PromotedAt moved %s -> %s — the refused post renewed the canary's TTL lease",
+ before.PromotedAt, after.PromotedAt)
+ }
+ if len(after.Audit) != len(before.Audit) {
+ t.Fatalf("audit trail grew from %d to %d on a refused fast-track", len(before.Audit), len(after.Audit))
+ }
+}
+
+// A MEMBER probing fast-track meets the byte-identical 404 — the loud
+// instant path past the window is the owner's alone.
+func TestFastTrackMemberGets404(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-sam-bake3")
+ csrf := sessionToken(t, fx, c)
+ status, _, memberBody := postForm(t, c, fx.u("/apps/kass/fasttrack"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}})
+ if status != http.StatusNotFound {
+ t.Fatalf("member fast-tracking = %d, want 404", status)
+ }
+ if p := fx.channelVersion(t, "kass", "stable"); p.FastTrack {
+ t.Fatal("a member's post waived the bake window")
+ }
+ // Byte-identical, not merely same-status: the difference between "not
+ // allowed" and "no such account" is not information this hands out.
+ strangerBody := strangerRefusal(t, fx, "code-zoe-bake3", fx.u("/apps/kass/fasttrack"),
+ url.Values{"channel": {"stable"}})
+ if memberBody != strangerBody {
+ t.Fatalf("the two refusals are not byte-identical:\n(member) %q\n(stranger) %q", memberBody, strangerBody)
+ }
+ // http.NotFound's own, well-known output — pinned so a future change to
+ // either path is caught even if it stays self-consistent.
+ if memberBody != "404 page not found\n" {
+ t.Fatalf("refusal body = %q, want the stdlib http.NotFound output", memberBody)
+ }
+}
+
+// The stable-promote prompt (spec §7): enable_bake rides the same
+// passkey-confirmed promote, and flips the flag on only when it is off.
+func TestPromoteEnableBakeFlipsTheFlag(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3")
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+
+ postGated(t, fx, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}, "version": {"v3"}, "enable_bake": {"1"}})
+ if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v3" {
+ t.Fatalf("stable = %s, want v3", p.Version)
+ }
+ if f := productionFlag(t, fx, "kass"); f == nil || !f.Production {
+ t.Fatalf("flag = %+v after enable_bake promote, want production on", f)
+ }
+ // The flash tells the truth about both halves of what just happened: the
+ // flag went on in this action, so the bake note is now accurate.
+ if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "Supply-chain bake enabled.") ||
+ !strings.Contains(body, "Adopts after the bake window") {
+ t.Fatalf("enable_bake flash missing its two halves:\n%s", body)
+ }
+}
+
+// A checkbox that could not be honored SAYS so. When the flag will not read,
+// the flip is never attempted — and the promote flash used to carry no trace
+// of that, leaving the human who ticked the box believing the app now bakes.
+func TestPromoteEnableBakeSaysWhenItCouldNotRead(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3")
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+ if _, err := fx.rel.Put(context.Background(),
+ release.ProductionKey(release.OpsAccount, "kass"), []byte("{not a wrapper"), ""); err != nil {
+ t.Fatal(err)
+ }
+
+ postGated(t, fx, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}, "version": {"v3"}, "enable_bake": {"1"}})
+ if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v3" {
+ t.Fatalf("stable = %s, want v3 — the promote itself must not depend on the flag", p.Version)
+ }
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "could not be read, so the bake was not enabled") {
+ t.Fatalf("the flash is silent about the unhonored checkbox:\n%s", body)
+ }
+ if strings.Contains(body, "Supply-chain bake enabled.") {
+ t.Fatal("the flash claims the bake was enabled over an unreadable flag")
+ }
+}
+
+// The bake note is a claim about this app, not about the channel: a
+// non-production app adopts a held channel immediately (spec §2), so
+// promising a window would be a lie.
+func TestPromoteBakeNoteFollowsTheFlag(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+
+ // No flag at all: beta holds, but this app does not bake. (edge first —
+ // the ladder promotes a build one rung at a time.)
+ seedChannel(t, fx, "kass", "edge", "v3")
+ // Checked, not fired and forgotten: a refused promote would make the
+ // absence assertion below true for the wrong reason.
+ if status, loc, body := postForm(t, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "version": {"v3"}, "channel": {"beta"}}); status != http.StatusSeeOther ||
+ loc != fx.path("/apps/kass") {
+ t.Fatalf("beta promote = %d -> %q:\n%s", status, loc, body)
+ }
+ if p := fx.channelVersion(t, "kass", "beta"); p.Version != "v3" {
+ t.Fatalf("beta = %+v, want v3", p)
+ }
+ if _, _, body := get(t, c, fx.u("/apps/kass")); strings.Contains(body, "Adopts after the bake window") {
+ t.Fatalf("a flagless app was promised a bake window:\n%s", body)
+ }
+
+ // Flag on: the same rung now says so.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ shipVersion(t, fx, "kass", "v4")
+ seedChannel(t, fx, "kass", "edge", "v4")
+ if status, _, body := postForm(t, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "version": {"v4"}, "channel": {"beta"}}); status != http.StatusSeeOther {
+ t.Fatalf("beta promote of v4 = %d:\n%s", status, body)
+ }
+ if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "Adopts after the bake window") {
+ t.Fatalf("a production app's held promotion did not mention the window:\n%s", body)
+ }
+}
diff --git a/internal/console/ladder.go b/internal/console/ladder.go
index ceadd0ab7..a96c45bb9 100644
--- a/internal/console/ladder.go
+++ b/internal/console/ladder.go
@@ -57,6 +57,10 @@ type LadderChannel struct {
Actor string
// Hotfix records that this promotion bypassed the ladder.
Hotfix bool
+ // FastTrack and BakedUntil carry the channel's waiver and its ~hold
+ // estimate into the rung row, where the fast-track control lives.
+ FastTrack bool
+ BakedUntil time.Time
// Err carries a pointer that would not read or verify, and a channel that
// hosts follow but that nothing has ever been published to. Both are one
// line of bad news on one rung, never a failed page.
@@ -146,6 +150,7 @@ func assembleLadder(channels []ChannelView, routes []RouteView, now time.Time) [
entries[rung] = append(entries[rung], LadderChannel{
Channel: cv.Name, Version: cv.Version, PromotedAt: cv.PromotedAt,
Actor: cv.Actor, Hotfix: cv.Hotfix, Err: cv.Err,
+ FastTrack: cv.FastTrack, BakedUntil: cv.BakedUntil,
Routes: byChannel[cv.Name],
})
delete(byChannel, cv.Name)
diff --git a/internal/console/ladder_test.go b/internal/console/ladder_test.go
index 2194c2768..62734d3e4 100644
--- a/internal/console/ladder_test.go
+++ b/internal/console/ladder_test.go
@@ -536,3 +536,209 @@ func TestLadderPromotesOntoAnExpiredCanary(t *testing.T) {
t.Fatalf("expired canary still holds %s — expiry has become a gate", p.Version)
}
}
+
+// The bake status line and its controls (spec §7): off-by-default says so;
+// an owner gets the turn-on/turn-off controls; a held channel row offers
+// the owner fast-track; the stable promote form carries the enable_bake
+// prompt while the flag is off.
+func TestLadderShowsBakeStateAndControls(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3")
+ c := signInPaul(t, fx)
+
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "Supply-chain bake") {
+ t.Fatalf("no bake status line:\n%s", body)
+ }
+ if !strings.Contains(body, `action="/a/bab/apps/kass/production"`) {
+ t.Fatal("owner is offered no production toggle")
+ }
+ // Off by default, and the stable promote form asks the question.
+ if !strings.Contains(body, `name="enable_bake"`) {
+ t.Fatal("the stable promote form does not carry the enable_bake prompt")
+ }
+
+ // Flip on; the held beta head now shows its window and the fast-track
+ // control appears on it.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body = get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Fatalf("no fast-track control on a held head:\n%s", rungBlock(t, body, "beta"))
+ }
+ if !strings.Contains(body, `name="production" value="off"`) {
+ t.Fatal("owner is offered no turn-off control once bake is on")
+ }
+ if strings.Contains(body, `name="enable_bake"`) {
+ t.Fatal("the enable_bake prompt survived the flag turning on")
+ }
+
+ // Waive it; the badge is permanent and the control is gone.
+ if _, err := release.FastTrack(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", "beta", "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body = get(t, c, fx.u("/apps/kass"))
+ b := rungBlock(t, body, "beta")
+ if !strings.Contains(b, "fast-tracked") {
+ t.Errorf("no permanent fast-tracked badge:\n%s", b)
+ }
+ if strings.Contains(b, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Error("a waived head still offers the fast-track control")
+ }
+}
+
+// A member sees state, never the owner-only controls (off / fast-track) —
+// but IS offered turn-on, the safe direction. Off-state alone would make
+// the turn-off/fast-track assertions tautological (neither control is
+// eligible to render for ANYONE — owner included — when the flag is off
+// and no channel is held): so this also flips the flag on and confirms
+// through the OWNER's own browser, first, that the controls DO render in
+// that state, before checking the member does not get them in the same
+// state. That ordering is what keeps the member-side absence checks from
+// passing vacuously.
+func TestLadderBakeControlsMemberView(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v9")
+ seedChannel(t, fx, "kass", "beta", "v9")
+ c := signInPaul(t, fx)
+
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ cm := newBrowser(t)
+ signUp(t, fx, cm, "code-sam-baketpl")
+
+ _, _, mbody := get(t, cm, fx.u("/apps/kass"))
+ if !strings.Contains(mbody, "Supply-chain bake") {
+ t.Fatal("member sees no bake status")
+ }
+ if !strings.Contains(mbody, `name="production" value="on"`) {
+ t.Error("member is not offered the turn-on control")
+ }
+ if strings.Contains(mbody, `name="production" value="off"`) {
+ t.Error("member is offered the owner-only turn-off control")
+ }
+ if strings.Contains(mbody, `/fasttrack"`) {
+ t.Error("member is offered the owner-only fast-track control")
+ }
+
+ // Put the page into the state where turn-off and fast-track are
+ // ELIGIBLE to render: flag on, with a held beta head. Confirm the
+ // OWNER gets both controls in this state...
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, obody := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(obody, `name="production" value="off"`) {
+ t.Fatal("owner is not offered the turn-off control once bake is eligible — the member-only checks below would be vacuous")
+ }
+ if !strings.Contains(obody, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Fatalf("owner is not offered the fast-track control on a held head — the member-only checks below would be vacuous:\n%s", rungBlock(t, obody, "beta"))
+ }
+
+ // ...then confirm the member does NOT, in that same eligible state.
+ _, _, mbody = get(t, cm, fx.u("/apps/kass"))
+ if strings.Contains(mbody, `name="production" value="off"`) {
+ t.Error("member is offered the owner-only turn-off control once bake is on")
+ }
+ if strings.Contains(mbody, `action="/a/bab/apps/kass/fasttrack"`) {
+ t.Error("member is offered the owner-only fast-track control on a held head")
+ }
+}
+
+// An UNREADABLE flag must not strand the app. The boxes fail toward baking
+// (spec §4), so an app whose flag object will not verify — a root key
+// rotation makes this every production app at once — bakes forever, and the
+// page's error text alone offers no way out. Re-declaring is the repair:
+// SetProduction CASes over whatever unverifiable bytes are there.
+func TestLadderUnreadableFlagOffersRepair(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ if _, err := fx.rel.Put(context.Background(),
+ release.ProductionKey(release.OpsAccount, "kass"), []byte("{not a wrapper"), ""); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "Production flag unreadable") {
+ t.Fatalf("no error line for an unverifiable flag:\n%s", body)
+ }
+ if !strings.Contains(body, `action="/a/bab/apps/kass/production"`) ||
+ !strings.Contains(body, `name="production" value="on"`) {
+ t.Fatalf("an unreadable flag offers no repair form — the app bakes forever:\n%s", body)
+ }
+ if !strings.Contains(body, "Re-declare production") {
+ t.Fatalf("the repair control is not named as a re-declaration:\n%s", body)
+ }
+ // The safe direction only: turning OFF over an unverifiable flag is still
+ // the owner's, through the ordinary control, and is not offered here.
+ if strings.Contains(body, `name="production" value="off"`) {
+ t.Error("the error branch offers turn-off; only the safe direction belongs here")
+ }
+ // And the repair actually lands: a member's plain POST replaces the
+ // garbage with a verifiable flag.
+ csrf := sessionToken(t, fx, c)
+ if status, _, b := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"on"}}); status != http.StatusSeeOther {
+ t.Fatalf("re-declare = %d, want 303:\n%s", status, b)
+ }
+ if f, err := release.GetProduction(context.Background(), fx.rel, fx.pub,
+ release.OpsAccount, "kass"); err != nil || f == nil || !f.Production {
+ t.Fatalf("flag = %+v, %v after the re-declaration, want a verifiable production: true", f, err)
+ }
+}
+
+// The turning-off branch, through the HTML: an off-flag younger than the
+// ratchet on an app with a held channel says hosts are still ratcheting, and
+// offers the instant way back on (any member's, the safe direction).
+func TestLadderTurningOffOffersTurnBackOn(t *testing.T) {
+ fx := newConsoleFixture(t)
+ shipVersion(t, fx, "kass", "v3")
+ seedChannel(t, fx, "kass", "beta", "v3") // a held channel: beta bakes
+ c := signInPaul(t, fx)
+ ctx := context.Background()
+ if err := release.SetProduction(ctx, fx.rel, fx.priv, release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ if err := release.SetProduction(ctx, fx.rel, fx.priv, release.OpsAccount, "kass", false, "test"); err != nil {
+ t.Fatal(err)
+ }
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "turning off") {
+ t.Fatalf("a fresh off-flag on a held app does not say hosts are still ratcheting:\n%s", body)
+ }
+ if !strings.Contains(body, `name="production" value="on"`) ||
+ !strings.Contains(body, "Turn back on") {
+ t.Fatalf("the turning-off state offers no way back on:\n%s", body)
+ }
+ // Not the default-off line: this app has an off-flag mid-ratchet, and
+ // telling the operator hosts adopt immediately would be false for ~72h.
+ if strings.Contains(body, "hosts adopt releases immediately") {
+ t.Error("mid-ratchet, the page claims hosts already adopt immediately")
+ }
+}
+
+// The prod-wiring nudge: a stable route with bake off gets the persistent
+// recommendation line.
+func TestLadderNudgesProdWiredBakeOff(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ // The fixture's kass.carlosapp.dev is on stable and the flag is off (the
+ // default): that IS the nudge condition.
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ if !strings.Contains(body, "adopts releases immediately") {
+ t.Fatalf("no prod-wiring nudge:\n%s", body)
+ }
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv,
+ release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ if _, _, body := get(t, c, fx.u("/apps/kass")); strings.Contains(body, "adopts releases immediately") {
+ t.Fatal("the nudge survived the flag turning on")
+ }
+}
diff --git a/internal/console/server.go b/internal/console/server.go
index 7f65c1a44..e0031150a 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -230,6 +230,8 @@ func NewServer(cfg Config, st *Store) (*Server, error) {
s.mux.HandleFunc("GET /a/{sqid}/apps/{app}", s.handleApp)
s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/promote", s.handlePromote)
s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/rollback", s.handleRollback)
+ s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/production", s.handleProduction)
+ s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/fasttrack", s.handleFastTrack)
s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/canary", s.handleNewCanary)
s.mux.HandleFunc("POST /a/{sqid}/apps/{app}/canary/retire", s.handleRetireCanary)
s.mux.HandleFunc("POST /a/{sqid}/invite", s.handleInvite)
@@ -1410,7 +1412,48 @@ func (s *Server) handlePromote(w http.ResponseWriter, r *http.Request) {
}
s.audit(acct, app, addr, "promote", "promoted "+version+" to "+channel)
msg := "Promoted " + version + " to " + channel + "."
- if release.HoldFor(release.DefaultHolds, channel) > 0 {
+ // Whether this app bakes at all is the production flag's business (bake
+ // controls spec §2): a non-production app's boxes adopt a held channel
+ // immediately, so the window note below would be a lie for them. Read the
+ // flag before the checkbox, which may still turn it on in this same
+ // request.
+ flag, ferr := release.GetProduction(ctx, s.cfg.Releases, s.cfg.ReleasePub, acct.Sqid, app)
+ if ferr != nil {
+ log.Printf("console: production flag %s: %v", app, ferr)
+ }
+ // An unreadable flag fails TOWARD baking, exactly as the adopt path does
+ // (spec §4) — so the note is still the truth about what the boxes will do.
+ baking := ferr != nil || (flag != nil && flag.Production)
+ // The spec §7 prompt: promote-to-stable asked "is this app in production
+ // now?" and the human said yes — in the same passkey-confirmed action.
+ // Turning ON is any member's anyway, so no extra gate; flips only when
+ // the flag is actually off, so a stray field on a repeat is a no-op.
+ // Both failure paths SAY so. The human ticked a box in a passkey-confirmed
+ // action; a log line they will never read is not an answer, and a silent
+ // skip leaves them believing the app bakes when it does not.
+ if r.FormValue("enable_bake") == "1" {
+ switch {
+ case ferr != nil:
+ // The read failed, so the flip was never attempted: writing over a
+ // flag nobody could read is the repair path's job (the app page's
+ // re-declare control), not a promote's side effect. baking is true
+ // here only because an unreadable flag fails TOWARD baking — which
+ // settles nothing about what this app has declared.
+ msg += " The production flag could not be read, so the bake was not enabled — use the toggle on the app page."
+ case baking:
+ // Already on: a stray checkbox on a repeat is a no-op, as before.
+ default:
+ if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, true, addr); err != nil {
+ log.Printf("console: enable_bake %s: %v", app, err)
+ msg += " Enabling the supply-chain bake failed — use the toggle on the app page."
+ } else {
+ s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on, at stable promote)")
+ msg += " Supply-chain bake enabled."
+ baking = true
+ }
+ }
+ }
+ if baking && release.HoldFor(release.DefaultHolds, channel) > 0 {
msg += " Adopts after the bake window on each host — rollback never bakes."
}
back("ok", msg)
@@ -1483,6 +1526,133 @@ func (s *Server) handleRollback(w http.ResponseWriter, r *http.Request) {
back("ok", "Rolled back "+channel+" to "+target+".")
}
+// handleProduction flips the app's production flag (2026-08-04 console bake
+// controls spec §7). ON is any member's to do, instantly — the safe
+// direction stays cheap. OFF removes a supply-chain guard from every
+// subscriber of the app, so it is the owner's, behind the passkey, and the
+// fleet ratchets it in on each box's own clock.
+//
+// Gate order is the mandated one, and the two owner-side gates apply to the
+// OFF direction ONLY: session/membership (accountApp) → CSRF → signing-key
+// availability → value parse → for OFF: owner (byte-identical 404) →
+// requireFreshAuth LAST. The refusals that cost nothing and reveal nothing
+// come first; the 404 comes before any step-up redirect, which would
+// otherwise confirm to a member-probing-as-stranger that the account exists.
+func (s *Server) handleProduction(w http.ResponseWriter, r *http.Request) {
+ addr, raw, acct, role, app, ok := s.accountApp(w, r)
+ if !ok {
+ return
+ }
+ if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
+ s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
+ return
+ }
+ if s.cfg.Key == nil {
+ s.renderMessage(w, r, http.StatusServiceUnavailable, "Promotions disabled",
+ "No signing key on this host — the console is running read-only.")
+ return
+ }
+ back := func(kind, msg string) {
+ s.setFlash(w, kind, msg)
+ http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app), http.StatusSeeOther)
+ }
+ var on bool
+ switch r.FormValue("production") {
+ case "on":
+ on = true
+ case "off":
+ on = false
+ default:
+ back("err", "Invalid production value.")
+ return
+ }
+ if !on {
+ if role != RoleOwner {
+ http.NotFound(w, r)
+ return
+ }
+ if !s.requireFreshAuth(w, r, addr, raw) {
+ return
+ }
+ }
+ ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
+ defer cancel()
+ if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, on, addr); err != nil {
+ log.Printf("console: production %s -> %v: %v", app, on, err)
+ back("err", "Could not update the production flag: "+err.Error())
+ return
+ }
+ if on {
+ s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on)")
+ back("ok", "Supply-chain bake enabled — beta and stable now hold before hosts adopt.")
+ } else {
+ s.audit(acct, app, addr, "production", "disabled the supply-chain bake (production: off)")
+ back("ok", "Supply-chain bake turning off — each host honors it after the stable window (~72h) on its own clock.")
+ }
+}
+
+// handleFastTrack waives the bake window for one channel's CURRENT head —
+// owner + passkey, always: this is the loud instant path past the window
+// (spec §5, §9), and the pointer wears the badge forever.
+func (s *Server) handleFastTrack(w http.ResponseWriter, r *http.Request) {
+ addr, raw, acct, role, app, ok := s.accountApp(w, r)
+ if !ok {
+ return
+ }
+ if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
+ s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
+ return
+ }
+ if s.cfg.Key == nil {
+ s.renderMessage(w, r, http.StatusServiceUnavailable, "Promotions disabled",
+ "No signing key on this host — the console is running read-only.")
+ return
+ }
+ back := func(kind, msg string) {
+ s.setFlash(w, kind, msg)
+ http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app), http.StatusSeeOther)
+ }
+ channel := strings.TrimSpace(r.FormValue("channel"))
+ if !release.ValidChannel(channel) {
+ back("err", "Invalid channel name.")
+ return
+ }
+ if role != RoleOwner {
+ http.NotFound(w, r)
+ return
+ }
+ if !s.requireFreshAuth(w, r, addr, raw) {
+ return
+ }
+ // A channel with no bake window has nothing to waive, and the waiver is
+ // not inert on it: FastTrack re-signs the pointer, which re-stamps
+ // PromotedAt — and PromotedAt is what CanaryExpired reads, so waiving a
+ // canary would silently renew its TTL lease and badge a channel that
+ // never bakes as "fast-tracked past the bake window", forever.
+ //
+ // Placed HERE, after both owner gates, and not with the cheap refusals
+ // above: HoldFor is pure vocabulary and leaks nothing on its own, but a
+ // refusal that fires before the owner check would make this endpoint's
+ // response vary with the channel name for a NON-OWNER, who must see the
+ // byte-identical 404 and nothing else. The mandated order (session → CSRF
+ // → owner 404 → requireFreshAuth LAST) then leaves only this spot. The
+ // cost is a passkey tap spent on a refusal; the alternative is an oracle.
+ if release.HoldFor(release.DefaultHolds, channel) == 0 {
+ back("err", "Channel "+channel+" has no bake window — there is nothing to fast-track.")
+ return
+ }
+ ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
+ defer cancel()
+ ver, err := release.FastTrack(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, channel, addr)
+ if err != nil {
+ log.Printf("console: fast-track %s %s: %v", app, channel, err)
+ back("err", "Fast-track failed: "+err.Error())
+ return
+ }
+ s.audit(acct, app, addr, "fast-track", "waived the bake window for "+ver+" on "+channel)
+ back("ok", "Fast-tracked "+ver+" on "+channel+" — hosts adopt on their next pass.")
+}
+
// ---- canaries ---------------------------------------------------------------
// canaryNameRe pins canary names to the same boring slugs as app names — the
diff --git a/internal/console/server_test.go b/internal/console/server_test.go
index a2d5efe5a..99a94cb4d 100644
--- a/internal/console/server_test.go
+++ b/internal/console/server_test.go
@@ -234,6 +234,14 @@ func TestConsoleE2E(t *testing.T) {
t.Fatal("welcome flash survived a second page view")
}
+ // Bake estimates only render for an app the production flag marks on
+ // (2026-08-04 console bake controls spec §7) — seed it so the "bakes
+ // until ~" assertion below still exercises the baking path rather than
+ // the new "nothing bakes without the flag" zeroing.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv, release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+
// Detail page: routes and channels, with the bake approximation labeled.
status, _, body := get(t, c, fx.u("/apps/kass"))
if status != 200 {
@@ -489,9 +497,11 @@ func TestPromoteFlow(t *testing.T) {
t.Fatalf("edge pointer = %+v, want v3", p)
}
_, _, body = get(t, c, fx.u("/apps/kass"))
- if !strings.Contains(body, "Promoted v3 to edge.") ||
- !strings.Contains(body, "bake window") || !strings.Contains(body, "rollback never bakes") {
- t.Fatalf("promote flash missing bake note:\n%s", body)
+ // edge holds zero — pipelines spec: merged to main is picked up as it lands.
+ // The flash for an edge promotion contains only "Promoted v3 to edge." without
+ // the "bake window" text that a held channel gets on a PRODUCTION app.
+ if !strings.Contains(body, `<p class="banner">Promoted v3 to edge.</p>`) {
+ t.Fatalf("edge promotion flash incorrect:\n%s", body)
}
// One rung at a time, up to stable — which is the rung that asks for a
@@ -501,8 +511,12 @@ func TestPromoteFlow(t *testing.T) {
if p := fx.channelVersion(t, "kass", "beta"); p.Version != "v3" {
t.Fatalf("beta pointer = %+v, want v3", p)
}
- if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "Promoted v3 to beta.") {
- t.Fatal("no beta promote flash")
+ // beta HOLDS, but this app has no production flag — the default is not
+ // production (bake controls spec §2), so its boxes adopt beta immediately
+ // and the flash must not promise a window. The exact banner is asserted,
+ // not a substring: "no bake note here" is the property under test.
+ if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, `<p class="banner">Promoted v3 to beta.</p>`) {
+ t.Fatalf("beta promotion flash incorrect (a flagless app must not be promised a bake window):\n%s", body)
}
postGated(t, fx, c, fx.u("/apps/kass/promote"),
url.Values{"csrf": {csrf}, "version": {"v3"}, "channel": {"stable"}})
@@ -868,6 +882,20 @@ func TestPromotionsDisabledWithoutKey(t *testing.T) {
url.Values{"csrf": {csrf}, "name": {"x"}}); status != http.StatusServiceUnavailable {
t.Fatalf("keyless canary retire = %d, want 503", status)
}
+ // The bake controls sign too — the flag is a root-signed object and
+ // fast-track re-signs a pointer, so both are mutations a keyless console
+ // must refuse rather than fail halfway through.
+ if status, _, _ := postForm(t, c, fx.u("/apps/kass/production"),
+ url.Values{"csrf": {csrf}, "production": {"on"}}); status != http.StatusServiceUnavailable {
+ t.Fatalf("keyless production flag = %d, want 503", status)
+ }
+ if status, _, _ := postForm(t, c, fx.u("/apps/kass/fasttrack"),
+ url.Values{"csrf": {csrf}, "channel": {"stable"}}); status != http.StatusServiceUnavailable {
+ t.Fatalf("keyless fast-track = %d, want 503", status)
+ }
+ if f, err := release.GetProduction(context.Background(), fx.rel, fx.pub, release.OpsAccount, "kass"); err != nil || f != nil {
+ t.Fatalf("keyless console wrote a production flag: %+v (%v)", f, err)
+ }
if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v1" {
t.Fatalf("keyless console moved a pointer to %s", p.Version)
}
diff --git a/internal/console/templates/app.html b/internal/console/templates/app.html
index d25b31297..8684587a9 100644
--- a/internal/console/templates/app.html
+++ b/internal/console/templates/app.html
@@ -95,7 +95,7 @@
{{else}}
<td><code>{{.Version}}</code></td>
<td>{{fmtTime .PromotedAt}}</td>
- <td>{{if .BakedUntil.IsZero}}live{{else}}bakes until ~{{fmtTime .BakedUntil}}{{end}}</td>
+ <td>{{if .BakedUntil.IsZero}}live{{else}}bakes until ~{{fmtTime .BakedUntil}}{{end}}{{if .FastTrack}} · fast-tracked{{end}}</td>
{{end}}
<td>{{if and $.CanPromote .HasHistory (not .Err)}}
<form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/rollback">
diff --git a/internal/console/templates/ladder.html b/internal/console/templates/ladder.html
index 72448ae20..f176e9c34 100644
--- a/internal/console/templates/ladder.html
+++ b/internal/console/templates/ladder.html
@@ -3,6 +3,57 @@
<p class="small">A channel is maturity, not a place: the same build climbs
canary → edge → beta → stable, and a promotion moves a signed pointer without
touching what any host is wired to.</p>
+{{with .Detail.Production}}
+<div class="bake">
+ {{if .Err}}
+ <p class="error">Production flag unreadable: {{.Err}} — hosts treat this app as in production and keep baking.
+ {{if $.CanPromote}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="on">
+ <button type="submit" class="linkish">Re-declare production</button>
+ </form>
+ <span class="small">— writes a fresh signed flag over the unreadable one. Without this the app
+ bakes forever; turning it off afterwards is the owner's, through the ordinary control.</span>
+ {{end}}
+ </p>
+ {{else if .On}}
+ <p class="small">Supply-chain bake: <strong>on</strong> — beta holds 24h and stable 72h on each host before it adopts.
+ {{if and $.CanPromote $.IsOwner}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="off">
+ <button type="submit" class="linkish">Turn off</button>
+ </form>
+ <span class="small">— confirms with your passkey; each host honors it after the stable window (~72h) on its own clock</span>
+ {{end}}
+ </p>
+ {{else if .TurningOff}}
+ <p class="small">Supply-chain bake: <strong>turning off</strong> — hosts honor it from ~{{fmtTime .EffectiveAt}}, each on its own clock.
+ {{if $.CanPromote}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="on">
+ <button type="submit" class="linkish">Turn back on</button>
+ </form>
+ {{end}}
+ </p>
+ {{else}}
+ <p class="small">Supply-chain bake: <strong>off</strong> (default) — hosts adopt releases immediately.
+ {{if $.CanPromote}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="production" value="on">
+ <button type="submit" class="linkish">Turn on</button>
+ </form>
+ {{end}}
+ </p>
+ {{end}}
+ {{if .Nudge}}
+ <p class="small nudge">{{icon "triangle-alert"}} This app looks production-wired but adopts releases immediately — consider enabling the supply-chain bake.</p>
+ {{end}}
+</div>
+{{end}}
<ol class="ladder">
{{range .Detail.Ladder}}
<li class="rung" id="rung-{{.Rung}}">
@@ -16,7 +67,16 @@ touching what any host is wired to.</p>
<p><code>{{.Channel}}</code> holds <code>{{.Version}}</code>
<span class="small">promoted {{fmtTime .PromotedAt}}
{{if .Actor}}by {{.Actor}}{{else}}— by whom is not recorded, on a pointer written before the trail existed{{end}}
- {{if .Hotfix}}· ladder bypassed (hotfix){{end}}</span></p>
+ {{if .Hotfix}}· ladder bypassed (hotfix){{end}}
+ {{if .FastTrack}}· fast-tracked past the bake window{{end}}</span></p>
+ {{if and (not .BakedUntil.IsZero) $.CanPromote $.IsOwner}}
+ <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/fasttrack" class="inline">
+ <input type="hidden" name="csrf" value="{{$.CSRF}}">
+ <input type="hidden" name="channel" value="{{.Channel}}">
+ <button type="submit" class="linkish">Fast-track {{.Version}}</button>
+ </form>
+ <span class="small">— in its bake window until ~{{fmtTime .BakedUntil}}; confirms with your passkey</span>
+ {{end}}
{{end}}
{{if .Routes}}
<ul class="routes">
@@ -46,6 +106,10 @@ touching what any host is wired to.</p>
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input type="hidden" name="channel" value="{{.Up.To}}">
<input type="hidden" name="version" value="{{.Up.Version}}">
+ {{if and (eq .Up.To "stable") (not $.Detail.Production.On)}}
+ <label class="small"><input type="checkbox" name="enable_bake" value="1" checked>
+ This app is in production — enable the supply-chain bake (recommended)</label>
+ {{end}}
<button type="submit" class="linkish">Promote {{.Up.Version}} to {{.Up.To}}</button>
</form>
{{if .Up.StepUp}}<span class="small">— confirms with your passkey</span>{{end}}
diff --git a/internal/console/view.go b/internal/console/view.go
index 0e9c7054c..2f34e51ed 100644
--- a/internal/console/view.go
+++ b/internal/console/view.go
@@ -31,6 +31,9 @@ type AppView struct {
// channels and routes as the fields above, arranged as the pipeline they
// actually are rather than as two alphabetical lists.
Ladder []LadderRung
+ // Production is the app's production-flag state (release.ProductionFlag)
+ // as the console shows it — see ProductionView.
+ Production ProductionView
// RouteAccount is the account the app's ROUTES say it belongs to, when
// that disagrees with the account whose page this is; "" when they agree
// or there are no routes. It is the half-migrated state — see
@@ -142,11 +145,36 @@ type ChannelView struct {
// missing information, not an error, and the page says so.
Actor string
Hotfix bool
+ // FastTrack is the pointer's bake waiver (release.ChannelPointer) — the
+ // badge is permanent, like the hotfix badge.
+ FastTrack bool
// Err degrades a missing or invalid channel pointer to one row's error
// text instead of a failed page.
Err string
}
+// ProductionView is the app's production-flag state as the console shows it
+// (2026-08-04 console bake controls spec §7).
+type ProductionView struct {
+ // On is the flag as the bucket has it; the default (no object) is off.
+ On bool
+ // TurningOff marks an off-flag younger than the stable window while the
+ // app still has held channels: hosts may still be ratcheting.
+ // EffectiveAt is the ~estimate shown (set_at + the stable hold at the
+ // MAXIMUM jitter, 1.25x) — each box's own clock and jitter govern,
+ // exactly as BakedUntil is only ever an approximation, and this one errs
+ // late on purpose (see assembleProduction).
+ TurningOff bool
+ EffectiveAt time.Time
+ SetAt time.Time
+ // Nudge: the app looks production-wired while bake is off. Detection
+ // recommends; it never flips the flag.
+ Nudge bool
+ // Err is a flag object that exists but will not verify — shown, and
+ // boxes treat it as production (they keep baking).
+ Err string
+}
+
// VersionView is one shipped release on the app page.
type VersionView struct {
Version, Kind, BuiltAt string
@@ -244,6 +272,19 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, acct strin
if ra := routeAccount[app]; ra != "" && ra != acct {
mismatch = ra
}
+ pv := assembleProduction(ctx, s, pub, acct, app, channels, byApp[app], now)
+ if pv.Err == "" && !pv.On {
+ // No production flag (or off): nothing bakes, so no estimate. An
+ // Err keeps the estimate — boxes fail toward baking.
+ for i := range channels {
+ channels[i].BakedUntil = time.Time{}
+ }
+ }
+ for i := range channels {
+ if channels[i].FastTrack {
+ channels[i].BakedUntil = time.Time{} // waived — nothing to wait out
+ }
+ }
out = append(out, AppView{
App: app,
Routes: byApp[app], // List orders by host already
@@ -251,6 +292,7 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, acct strin
Channels: channels,
Canaries: assembleCanaries(channels, byApp[app]),
Ladder: assembleLadder(channels, byApp[app], now),
+ Production: pv,
RouteAccount: mismatch,
})
}
@@ -312,6 +354,7 @@ func assembleChannels(ctx context.Context, s release.Store, pub ed25519.PublicKe
cv := ChannelView{
Name: name, Version: p.Version, PromotedAt: p.PromotedAt,
HasHistory: len(p.History) > 0,
+ FastTrack: p.FastTrack,
}
// The trail is newest first, but a rollback carries it forward
// without appending: the entry that explains the CURRENT version is
@@ -334,6 +377,54 @@ func assembleChannels(ctx context.Context, s release.Store, pub ed25519.PublicKe
return out
}
+// assembleProduction reads the app's flag and derives the console's view of
+// it: on/off, the turning-off ratchet estimate, and the prod-wiring nudge.
+func assembleProduction(ctx context.Context, s release.Store, pub ed25519.PublicKey, acct, app string, channels []ChannelView, routes []RouteView, now time.Time) ProductionView {
+ pv := ProductionView{}
+ flag, err := release.GetProduction(ctx, s, pub, acct, app)
+ switch {
+ case err != nil:
+ pv.Err = err.Error()
+ pv.On = true // what the boxes will do: fail toward baking
+ return pv
+ case flag != nil:
+ pv.On = flag.Production
+ pv.SetAt = flag.SetAt
+ }
+ held := false
+ for _, cv := range channels {
+ if cv.Err == "" && release.HoldFor(release.DefaultHolds, cv.Name) > 0 {
+ held = true
+ break
+ }
+ }
+ if flag != nil && !flag.Production && held {
+ // The estimate errs LATE, deliberately. A box ratchets from its OWN
+ // first sighting of the off-flag, not from set_at, and it waits
+ // hold*(1+jitter) with jitter in [0, 0.25) — so set_at + the bare hold
+ // is the earliest any box could possibly honor it, and the console
+ // would go on to claim "off — hosts adopt immediately" for up to
+ // another 19 hours while the slowest boxes were still baking. Quoting
+ // the maximum (1.25x) means the line is late rather than wrong; each
+ // box's own clock still governs, as the wording says.
+ hold := release.HoldFor(release.DefaultHolds, release.RungStable)
+ if until := flag.SetAt.Add(time.Duration(float64(hold) * 1.25)); now.Before(until) {
+ pv.TurningOff, pv.EffectiveAt = true, until
+ }
+ }
+ if !pv.On {
+ stableRoute := false
+ for _, rt := range routes {
+ if rung, ok := release.Rung(rt.Channel); ok && rung == release.RungStable {
+ stableRoute = true
+ break
+ }
+ }
+ pv.Nudge = stableRoute || len(prodWiredCanaryHosts(routes)) > 0
+ }
+ return pv
+}
+
// FleetView reads status/fleet.json off the console's release.Store handle
// and decodes it into status.Fleet — the exact shape internal/status's
// public page renders, so the operator view and the public page never
diff --git a/internal/console/view_test.go b/internal/console/view_test.go
index 4fea7b3b4..c93b651b2 100644
--- a/internal/console/view_test.go
+++ b/internal/console/view_test.go
@@ -107,7 +107,14 @@ func findChannel(t *testing.T, chans []ChannelView, name string) ChannelView {
}
func TestAssemble(t *testing.T) {
- regPath, store, _, pub := buildFixtures(t)
+ regPath, store, priv, pub := buildFixtures(t)
+ // Bake estimates only render for an app the flag marks as production
+ // (2026-08-04 console bake controls spec §7) — seed it so the
+ // BakedUntil assertions below still exercise the "still baking" path
+ // rather than the new "nothing bakes without the flag" zeroing.
+ if err := release.SetProduction(context.Background(), store, priv, release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
views, err := Assemble(regPath, store, pub, release.OpsAccount, []string{"kass"}, nil)
if err != nil {
t.Fatal(err)
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index c654bb1da..8816459ba 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -199,6 +199,45 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
pointers[k] = ptr
}
+ // Resolve each app's production flag once per pass — but ONLY for apps
+ // with a HELD head resolved this pass (spec §6). That filter is a cost
+ // property, not a nicety: the canary fast loop (PassFilter with a
+ // canary/*-only extra, every 2-3 SECONDS) resolves nothing but channels
+ // whose HoldFor is 0, so it never pays a production-flag bucket read at
+ // all; the hourly pass pays exactly one read per (account, app) with a
+ // held head, not one per channel and not one per route.
+ type prodState struct {
+ flag *ProductionFlag
+ err error // non-nil: the object exists but would not read or verify
+ }
+ bakeable := map[acctApp]bool{}
+ for k, ptr := range pointers {
+ if ptr != nil && HoldFor(DefaultHolds, k.channel) > 0 {
+ bakeable[acctApp{k.acct, k.app}] = true
+ }
+ }
+ prod := map[acctApp]prodState{}
+ for ak := range bakeable {
+ // A missing flag is (nil, nil) — the default, not production. Any
+ // OTHER failure is kept as an error and fails toward baking in
+ // ProductionBake: an attacker who can corrupt but not sign must not
+ // be able to shorten a window by breaking the object.
+ f, err := GetProduction(ctx, s, pub, ak.acct, ak.app)
+ prod[ak] = prodState{flag: f, err: err}
+ if err != nil {
+ // SAY SO. This failure re-imposes the full window on every held
+ // head of the app, and the most likely cause is not tampering at
+ // all: on a GetObject-without-ListBucket fleet (the same IAM shape
+ // the missing-pointer branch above already handles) a GET of an
+ // ABSENT key answers 403, not 404 — so the spec's default-OFF
+ // silently inverts to production for every app with no flag. A
+ // 72h hold nobody can see the reason for is the worst version of
+ // this feature; log it like every other per-app problem here and
+ // carry on.
+ fmt.Printf(" %s/%s: production flag unreadable (%v) — treating as production, holds apply\n", ak.acct, ak.app, err)
+ }
+ }
+
// Tick the bake clock: record every channel head this box hasn't seen at
// that head before, so the supply-chain window is measured from the box's
// own first sighting, never a feed timestamp (bake.go). The clock only
@@ -219,7 +258,51 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
changed = true
}
}
- if changed {
+ // ORDER IS LOAD-BEARING: fold this pass's verified flag reads into the
+ // box's production memory HERE, before any bake verdict below is taken.
+ // That is what makes prodMemory's "a mixed-version rollout costs at most
+ // one pass of this memory" true — a binary that wiped the latch re-latches
+ // it from the bucket and honors it in the SAME pass, never a pass later.
+ //
+ // The err == nil guard is the second lock on a door ObserveProduction's
+ // own nil-check already holds (GetProduction returns a nil flag on every
+ // error path, and a nil flag is a no-op there). It stays because the rule
+ // it states — the memory only ever ticks for VERIFIED documents — is
+ // worth being true at the call site too, not only at the callee.
+ for ak, ps := range prod {
+ if ps.err == nil && bake.ObserveProduction(ak.acct, ak.app, ps.flag, now) {
+ changed = true
+ }
+ }
+ // bake.tainted forces a Save for a different reason than migrated: the
+ // clock file existed but did not read or parse, so every held head on
+ // this box is failing toward baking until that file is replaced
+ // (ProductionBake). Left alone, the corrupt file would survive every
+ // changed-gated pass and the box would bake forever.
+ //
+ // But ONLY a full pass may perform that repair. A NARROWED pass
+ // (extra != nil — the canary fast loop, every 2-3 seconds) resolves only
+ // channels with no hold, so it fetched no production flags at all: its
+ // in-memory Production map is EMPTY, not repaired. Letting it write would
+ // overwrite the corrupt file with that emptiness AND clear the taint that
+ // is currently failing closed, all before anything re-observed the flags
+ // — and if the bucket flag had also been deleted, the latch would be gone
+ // for good. So a tainted clock on a narrowed pass skips the Save entirely,
+ // forced or ordinary: a pass that fetched no flags cannot repair what it
+ // would overwrite. Nothing is lost — the canary observations it would
+ // have recorded re-occur within seconds, and the hourly pass does the
+ // repair with the flags actually in hand.
+ //
+ // Residual, accepted: even the full pass only re-observes flags for apps
+ // with a currently-resolved HELD head, so an app whose channel pointer is
+ // simultaneously unreadable keeps its latch out of the repaired file
+ // until that pointer heals. That is triple-tamper territory (corrupt
+ // clock file AND unreadable pointer AND deleted flag) and sits inside the
+ // box-file trust boundary LoadFirstSeen already documents as accepted.
+ switch {
+ case bake.tainted && extra != nil:
+ // skip: see above.
+ case changed || bake.tainted:
if err := bake.Save(); err != nil {
return fmt.Errorf("record bake clock: %v", err)
}
@@ -229,6 +312,18 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
// this pass — one bad build shouldn't bounce every host in turn.
poisoned := map[string]bool{}
+ // One definition of "does the window apply here", shared by the awake and
+ // hibernating loops below — the same question must not be able to be
+ // answered two ways for the same app depending on whether it sleeps.
+ //
+ // RouteAccount(rt), never the raw rt.Account column: the group and
+ // bakeable keys were built from RouteAccount output, and an unmigrated
+ // row can still carry an account NAME there, which resolves to ops.
+ bakeIgnore := func(rt registry.Route) bool {
+ ak := acctApp{RouteAccount(rt), rt.App}
+ return !bake.ProductionBake(ak.acct, ak.app, prod[ak].err, bakeable[ak], DefaultHolds, now)
+ }
+
// Awake hosts first, hibernating second: repoints have no health gate, so
// let an awake host discover (and poison) a broken build before any
// sleeper is moved onto it.
@@ -242,12 +337,12 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
continue
}
ptr := pointers[groupKey{RouteAccount(rt), rt.App, routeChannel(rt)}]
- adoptHost(ctx, reg, s, optDir, bake, poisoned, rt, ptr, restart, now, cfg.nag)
+ adoptHost(ctx, reg, s, optDir, bake, poisoned, rt, ptr, restart, now, cfg.nag, bakeIgnore(rt))
}
for _, rt := range hibernating {
ptr := pointers[groupKey{RouteAccount(rt), rt.App, routeChannel(rt)}]
// restart=nil: hibernating hosts get the pure repoint — never woken.
- adoptHost(ctx, reg, s, optDir, bake, poisoned, rt, ptr, nil, now, cfg.nag)
+ adoptHost(ctx, reg, s, optDir, bake, poisoned, rt, ptr, nil, now, cfg.nag, bakeIgnore(rt))
}
return nil
}
@@ -300,7 +395,13 @@ func RouteAccount(rt registry.Route) string {
// nag is passConfig.nag from the call's PassOptions — nil (the ordinary
// case) means the canary-TTL nag below never fires, never even builds its
// string; see NagExpiredCanaries for who sets it and why.
-func adoptHost(ctx context.Context, reg *registry.Registry, s Store, optDir string, bake *FirstSeen, poisoned map[string]bool, rt registry.Route, ptr *ChannelPointer, restart func(host string) error, now time.Time, nag io.Writer) {
+//
+// ignore is the app-level bake verdict from ProductionBake — the production
+// flag's doing, never a publisher field. True means the supply-chain window
+// does not CURRENTLY apply to this app on this box (not declared production,
+// or declared off and past the ratchet); the caller computes it once per
+// route from the flag it read this pass.
+func adoptHost(ctx context.Context, reg *registry.Registry, s Store, optDir string, bake *FirstSeen, poisoned map[string]bool, rt registry.Route, ptr *ChannelPointer, restart func(host string) error, now time.Time, nag io.Writer, ignore bool) {
if ptr == nil {
return
}
@@ -336,7 +437,10 @@ func adoptHost(ctx context.Context, reg *registry.Registry, s Store, optDir stri
if cur == ver {
return // up to date — stay quiet on the periodic tick
}
- if hold := Hold(bake, RouteAccount(rt), rt.App, channel, ver, DefaultHolds, false, now); hold != "" {
+ // Two independent waivers, both root-signed in origin: ignore is the
+ // app's production verdict (§2/§4), ptr.FastTrack is this ONE pointer's
+ // per-promotion waiver (§5). Either one alone releases the head.
+ if hold := Hold(bake, RouteAccount(rt), rt.App, channel, ver, DefaultHolds, ignore || ptr.FastTrack, now); hold != "" {
fmt.Printf(" %s: %s (%s) is %s\n", rt.Host, ver, channel, hold)
return
}
diff --git a/internal/release/adopt_test.go b/internal/release/adopt_test.go
index f7ffb98e1..962ab16f3 100644
--- a/internal/release/adopt_test.go
+++ b/internal/release/adopt_test.go
@@ -195,6 +195,11 @@ func TestPassStableHeldByBakeWindow(t *testing.T) {
if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
t.Fatalf("Promote: %v", err)
}
+ // The app is DECLARED production (2026-08-04 spec §2): the window is no
+ // longer unconditional — it is what the flag buys.
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatalf("SetProduction: %v", err)
+ }
addRoute(t, reg, "h.example.com", "myapp", "", false) // "" = stable
if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
@@ -689,3 +694,318 @@ func withCapturedStdout(t *testing.T, buf *bytes.Buffer, fn func()) {
w.Close()
<-done
}
+
+// ---- the production flag on the adopt path (2026-08-04 spec §6) -------------
+
+// The default: no flag, no window (2026-08-04 spec §2). The whole fleet
+// runs on canary/* today, so nothing live loses protection — and a
+// rapid-iteration app's stable is just another channel.
+func TestPassNoProductionFlagInstallsImmediately(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
+ t.Fatalf("live = %q, want v1 — no production flag means no window", v)
+ }
+}
+
+// Fast-track: a production app's held head installs the moment its pointer
+// carries the root-signed waiver (spec §5).
+func TestPassFastTrackSkipsTheWindow(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want held (production, no waiver yet)", v)
+ }
+ if _, err := FastTrack(ctx, s, priv, "bab", "myapp", "stable", "test"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "v1" {
+ t.Fatalf("live = %q, want v1 — the waiver did not skip the window", v)
+ }
+}
+
+// A production app whose flag OBJECT rots (exists, won't verify) keeps
+// baking — an attacker who can corrupt but not sign shortens nothing.
+func TestPassCorruptProductionFlagFailsTowardBaking(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "myapp"), []byte("not a wrapper"), ""); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want held — a corrupt flag must read as production", v)
+ }
+}
+
+// ORDERING, pinned: ObserveProduction runs BEFORE this pass's bake verdicts
+// are taken, so the bucket flag re-latches a wiped memory within ONE pass —
+// the whole basis of prodMemory's "a mixed-version rollout costs at most one
+// pass of this memory" comment. Simulates the old binary that re-marshals a
+// struct with no Production field and thereby deletes the latch.
+func TestPassReLatchesProductionMemoryInTheSamePass(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ st := LoadFirstSeen(FirstSeenPath(opt))
+ if len(st.Production) == 0 {
+ t.Fatalf("first pass did not latch the production memory")
+ }
+
+ // The old binary's write: every channel observation survives, the
+ // production memory (and with it the replay floor) is gone.
+ st.Production = map[string]*prodMemory{}
+ if err := st.Save(); err != nil {
+ t.Fatalf("Save wiped clock: %v", err)
+ }
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want still held — the bucket flag must re-latch BEFORE this pass's bake verdict, not after it", v)
+ }
+ if st := LoadFirstSeen(FirstSeenPath(opt)); len(st.Production) == 0 {
+ t.Fatalf("the re-latch was not persisted")
+ }
+}
+
+// A clock file that exists but does not parse is rewritten within ONE pass,
+// even when the pass observes nothing new — the taint-forced Save. Without
+// it a corrupt file would sit there forever, and every held head on the box
+// would fail toward baking pass after pass (ProductionBake's st.tainted).
+func TestPassRewritesATaintedClockInOnePass(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ _, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t) // no routes: nothing new is observed this pass
+
+ if err := os.WriteFile(FirstSeenPath(opt), []byte("{not json"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if st := LoadFirstSeen(FirstSeenPath(opt)); !st.tainted {
+ t.Fatalf("setup: the corrupt clock did not read as tainted")
+ }
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if st := LoadFirstSeen(FirstSeenPath(opt)); st.tainted {
+ t.Fatalf("the pass left the clock file corrupt — a tainted clock must force its own Save")
+ }
+}
+
+// prodReadStore counts Get calls for one exact key.
+type prodReadStore struct {
+ Store
+ key string
+ n int
+}
+
+func (p *prodReadStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if key == p.key {
+ p.n++
+ }
+ return p.Store.Get(ctx, key)
+}
+
+// The 2-3 SECOND canary fast loop must never pay a production-flag bucket
+// read: canary/* has no window (DefaultHolds), so no canary-only pass ever
+// has a HELD head to ask about. The hourly pass pays exactly one read per
+// (account, app) with a held head — not one per channel, not one per route.
+func TestCanaryFastLoopPaysNoProductionRead(t *testing.T) {
+ dir := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, dir, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ for _, ch := range []string{"canary/x", "canary/y", "stable"} {
+ if err := Promote(ctx, dir, priv, "bab", "myapp", ch, "v1"); err != nil {
+ t.Fatalf("Promote %s: %v", ch, err)
+ }
+ }
+ if err := SetProduction(ctx, dir, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "cx.example.com", "myapp", "canary/x", false)
+ addRoute(t, reg, "cy.example.com", "myapp", "canary/y", false)
+ addRoute(t, reg, "s.example.com", "myapp", "stable", false)
+
+ s := &prodReadStore{Store: dir, key: ProductionKey("bab", "myapp")}
+ if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil {
+ t.Fatalf("PassFilter: %v", err)
+ }
+ if s.n != 0 {
+ t.Fatalf("canary-only pass read the production flag %d times, want 0 — the fast loop must not pay bucket reads for a window canary/* does not have", s.n)
+ }
+
+ // Not vacuous: the full pass, which DOES resolve a held head, reads it —
+ // once, for two canary channels, a stable one and three routes.
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if s.n != 1 {
+ t.Fatalf("full pass read the production flag %d times, want exactly 1 per (account, app)", s.n)
+ }
+}
+
+// A NARROWED pass (the canary fast loop) must never repair a tainted clock:
+// it fetched no production flags, so its Production map is empty, and writing
+// it would replace the corrupt file with that emptiness AND clear the taint
+// that is currently failing closed — before anything re-observed the flags.
+// The full pass, which holds the flags, does the repair.
+func TestNarrowedPassNeverRepairsATaintedClock(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ for _, ch := range []string{"canary/x", "stable"} {
+ if err := Promote(ctx, s, priv, "bab", "myapp", ch, "v1"); err != nil {
+ t.Fatalf("Promote %s: %v", ch, err)
+ }
+ }
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "cx.example.com", "myapp", "canary/x", false)
+ addRoute(t, reg, "s.example.com", "myapp", "stable", false)
+
+ if err := os.WriteFile(FirstSeenPath(opt), []byte("{not json"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil {
+ t.Fatalf("PassFilter: %v", err)
+ }
+ if raw, err := os.ReadFile(FirstSeenPath(opt)); err != nil {
+ t.Fatal(err)
+ } else if json.Valid(raw) {
+ t.Fatalf("the canary fast loop overwrote a tainted clock — a pass that fetched no flags cannot repair what it would overwrite; file is now %q", raw)
+ }
+
+ // The hourly pass has the flags in hand, so it repairs — and the app's
+ // production state survives, re-latched from the bucket in that same pass.
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ st := LoadFirstSeen(FirstSeenPath(opt))
+ if st.tainted {
+ t.Fatalf("the full pass did not repair the tainted clock")
+ }
+ if mem := st.Production[prodKey("bab", "myapp")]; mem == nil || !mem.WasProduction {
+ t.Fatalf("production memory = %+v, want the latch re-established by the repairing pass", mem)
+ }
+}
+
+// The off-ratchet, end to end on the adopt path: flipping production OFF does
+// not release a held head immediately — the off is honored only after the
+// stable window has elapsed on this box's own sighting of it (spec §4). This
+// is the dangerous direction, so it is pinned here and not only in bake_test:
+// it is also what fails if ProductionBake's hasBakeableHead carve-out is ever
+// passed a constant false from this call site.
+func TestPassProductionOffStillRatchets(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, "h.example.com", "myapp", "", false)
+
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want held (declared production)", v)
+ }
+
+ // Flip it off. An operator who can flip the flag must not thereby have a
+ // same-minute bypass of the window their held head is sitting in.
+ if err := SetProduction(ctx, s, priv, "bab", "myapp", false, "test"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := LiveVersion(opt, "h.example.com"); v != "" {
+ t.Fatalf("live = %q, want STILL held — production: false must ratchet through the stable window, never release a held head on sight", v)
+ }
+ // And the box recorded the off-sighting, which is what the ratchet is
+ // measured from — its own clock, never the flag's timestamp.
+ mem := LoadFirstSeen(FirstSeenPath(opt)).Production[prodKey("bab", "myapp")]
+ if mem == nil || mem.OffFirstSeen.IsZero() || !mem.WasProduction {
+ t.Fatalf("production memory = %+v, want a latched app with an off-sighting recorded", mem)
+ }
+}
diff --git a/internal/release/bake.go b/internal/release/bake.go
index ed87fbc43..8b8d0c311 100644
--- a/internal/release/bake.go
+++ b/internal/release/bake.go
@@ -13,7 +13,8 @@
// promote`). beta's 24h absorbs the caught-within-a-day mass of real
// supply-chain incidents; stable's 72h additionally spans a weekend, so a
// Friday-night malicious publish is seen by a human on Monday with margin to
-// pull it back.
+// pull it back. edge has no window either — merged to main is picked up as it
+// lands (pipelines spec §3).
//
// There is deliberately NO publisher-side bypass ("critical fix, skip the
// hold") — that flag is the first thing an attacker sets. The bypass is
@@ -23,6 +24,7 @@ package release
import (
"encoding/json"
+ "errors"
"fmt"
"math/rand"
"os"
@@ -35,6 +37,7 @@ import (
// overrides come from host config as a replacement map passed by the caller.
var DefaultHolds = map[string]time.Duration{
"canary/*": 0,
+ "edge": 0,
"beta": 24 * time.Hour,
"stable": 72 * time.Hour,
}
@@ -84,6 +87,27 @@ type FirstSeen struct {
// channelClockKey.
Channels map[string]map[string]time.Time `json:"channels"`
+ // Production is the per-app production-flag memory (2026-08-04 console
+ // bake controls spec §4), keyed with clockSep — the same SEPARATOR
+ // argument as channelClockKey (a channel segment may contain "/"; an
+ // account or app name never contains clockSep). prodKey folds "" and
+ // OpsAccount to the same key — there is no legacy bare shape to
+ // preserve here, this memory is new, so callers can pass whatever
+ // RouteAccount(rt) (adopt.go) returns and land on the right memory
+ // either way.
+ //
+ // ADDITIVE, but only one pass deep: an OLD binary that Saves this file
+ // re-marshals the struct IT knows about, which has no Production field —
+ // its write DELETES every app's production memory from the shared file.
+ // That is not silently unsafe: the latch re-latches from the bucket flag
+ // the very next pass a new binary reads it (true stays true), and a lost
+ // ratchet clock simply restarts from that pass's sighting (an
+ // in-progress off-ratchet gets pushed out, never skipped) — both are the
+ // closed direction on their own. A mixed-version rollout only ever costs
+ // up to one pass of this memory; it never lets an off-flag through
+ // early, and never lets a deleted true downgrade anything either.
+ Production map[string]*prodMemory `json:"production,omitempty"`
+
path string // where Save persists; set by LoadFirstSeen
// migrated marks a clock LoadFirstSeenMigrating built from its
@@ -92,6 +116,12 @@ type FirstSeen struct {
// change this pass — see LoadFirstSeenMigrating's doc comment for why
// a changed-gated Save alone isn't enough to make the migration durable.
migrated bool
+
+ // tainted marks a clock whose file read succeeded but did NOT parse as
+ // JSON — distinct from a missing file, which is just a fresh, empty
+ // clock. Never serialized (unexported); see LoadFirstSeen and
+ // ProductionBake for why the two failure modes must be told apart.
+ tainted bool
}
// channelClockKey identifies one channel head's clock. The ACCOUNT is part of
@@ -126,17 +156,45 @@ func channelClockKey(acct, app, channel string) string {
// name (appRe) or a channel segment (channelSegRe).
const clockSep = "|"
-// LoadFirstSeen reads the bake clock from path. A missing or corrupt file is
-// an empty clock — every head then bakes from "now", which fails closed (a
-// wiped clock delays updates; it never lets one through early).
+// LoadFirstSeen reads the bake clock from path. For the CHANNEL clock, a
+// missing OR corrupt file is simply an empty clock — every head then bakes
+// from "now", which fails closed (a wiped clock delays updates; it never
+// lets one through early either way).
+//
+// For the PRODUCTION memory (§4), those two failure modes must be told
+// apart. A file that reads but does not PARSE marks the clock tainted, and
+// ProductionBake fails toward baking for every held head on a tainted
+// clock — the same closed direction, reached differently, because an
+// unmarshal error can leave Production nil or half-written rather than
+// usably empty. A file that is simply ABSENT, though, is indistinguishable
+// from a fresh box: the same read that lets the channel clock safely
+// restart "from now" also silently loses the production latch and the
+// off-ratchet's replay floor, because there is nothing on disk to say a
+// latch ever existed. That gap is accepted: the latch defends against
+// BUCKET-side tampering (someone with bucket write but no key deleting the
+// flag object) — a box's own /data is a different trust boundary. Whatever
+// already has write access there already holds adopt authority, i.e.
+// already controls what this box installs; deleting its bake-clock file
+// buys it nothing a corrupt bucket object would.
func LoadFirstSeen(path string) *FirstSeen {
st := &FirstSeen{path: path}
if raw, err := os.ReadFile(path); err == nil {
- _ = json.Unmarshal(raw, st)
+ if err := json.Unmarshal(raw, st); err != nil {
+ st.tainted = true
+ }
+ } else if !errors.Is(err, os.ErrNotExist) {
+ // The file EXISTS but this process could not read it (EACCES,
+ // EISDIR, an I/O error) — a different failure than "no file", and
+ // one this box cannot tell apart from a bucket object it can't
+ // trust either. Same taint, same fail-toward-baking direction.
+ st.tainted = true
}
if st.Channels == nil {
st.Channels = map[string]map[string]time.Time{}
}
+ if st.Production == nil {
+ st.Production = map[string]*prodMemory{}
+ }
if st.Jitter < 0 || st.Jitter >= 0.25 {
st.Jitter = 0
}
@@ -302,3 +360,118 @@ func Hold(st *FirstSeen, acct, app, channel, version string, holds map[string]ti
}
return ""
}
+
+// ---- production memory ------------------------------------------------------
+
+// prodMemory is one app's production-flag memory on this box. It exists to
+// hold two properties no bucket read can: DELETION IS NOT A DOWNGRADE (once
+// a verified production: true is seen, absence never clears it — anyone
+// with bucket write but no key can delete an object), and A STALE FLAG
+// CANNOT BE REPLAYED (an off from a past era, re-written by someone with
+// bucket access alone, must not roll back a newer on).
+type prodMemory struct {
+ // WasProduction latches on the first verified production: true.
+ WasProduction bool `json:"was_production,omitempty"`
+ // NewestSetAt is the replay floor: the newest set_at this box has
+ // honored. Anything at or below it is old news.
+ NewestSetAt time.Time `json:"newest_set_at,omitzero"`
+ // OffSetAt is the discriminator: non-zero means the newest verified word
+ // for this app is off (only ever read via IsZero — its VALUE isn't
+ // otherwise consulted; NewestSetAt already carries the replay floor).
+ // OffFirstSeen is when THIS BOX first saw that off-flag edition — the
+ // ratchet clock, the box's own observation, never the flag's own
+ // timestamp (same argument as the bake clock itself).
+ OffSetAt time.Time `json:"off_set_at,omitzero"`
+ OffFirstSeen time.Time `json:"off_first_seen,omitzero"`
+}
+
+// prodKey identifies one app's production memory. Joined with clockSep for
+// the same reason channelClockKey is (an app or account name can never
+// contain it, unlike a channel segment), and it folds "" and OpsAccount to
+// the same key — unlike channelClockKey there's no legacy bare shape to
+// keep separate, this memory is new, so RouteAccount(rt)'s either form
+// always resolves to the one record.
+func prodKey(acct, app string) string {
+ if acct == "" || acct == OpsAccount {
+ acct = OpsAccount
+ }
+ return acct + clockSep + app
+}
+
+// ObserveProduction folds one VERIFIED flag read into this box's memory.
+// nil (no object in the bucket) changes nothing — absence is not evidence.
+// Returns whether anything changed; the caller persists via Save. Never
+// call it with a flag that failed verification: like Observe, the memory
+// only ever ticks for signed documents.
+func (st *FirstSeen) ObserveProduction(acct, app string, flag *ProductionFlag, now time.Time) bool {
+ if flag == nil {
+ return false
+ }
+ key := prodKey(acct, app)
+ mem := st.Production[key]
+ if mem == nil {
+ mem = &prodMemory{}
+ }
+ // The replay floor. Equal set_at is the object already recorded — the
+ // every-pass re-read — so nothing changes; older is a replay and is
+ // ignored outright.
+ if !flag.SetAt.After(mem.NewestSetAt) {
+ return false
+ }
+ mem.NewestSetAt = flag.SetAt
+ if flag.Production {
+ mem.WasProduction = true
+ mem.OffSetAt, mem.OffFirstSeen = time.Time{}, time.Time{}
+ } else {
+ mem.OffSetAt = flag.SetAt
+ mem.OffFirstSeen = now.UTC().Truncate(time.Second)
+ }
+ st.Production[key] = mem
+ return true
+}
+
+// ProductionBake is the adopt pass's one question: does the supply-chain
+// window apply to this app on this box right now? Call ObserveProduction
+// first — this reads memory, plus flagErr, which is non-nil when a flag
+// object EXISTS but would not read or verify: that fails toward baking (an
+// attacker who can corrupt but not sign must not shorten anything).
+// hasBakeableHead is the spec §4 carve-out: with no held channel head
+// resolved this pass, an off-flag needs no ratchet — there is nothing at a
+// baked head to protect.
+func (st *FirstSeen) ProductionBake(acct, app string, flagErr error, hasBakeableHead bool, holds map[string]time.Duration, now time.Time) bool {
+ if st.tainted {
+ // This box's own clock file existed but couldn't be read or didn't
+ // parse — untrustworthy in a way absence never is. Fail toward
+ // baking for every held head, same direction as flagErr, for the
+ // lifetime of THIS load: tainted is set once, in LoadFirstSeen, and
+ // nothing clears it in memory afterward. Save rewrites the file
+ // from this struct's current (now-valid, in-memory) content, so the
+ // NEXT LoadFirstSeen of that path starts clean.
+ return true
+ }
+ if flagErr != nil {
+ return true
+ }
+ mem := st.Production[prodKey(acct, app)]
+ if mem == nil {
+ return false // never declared production — the default
+ }
+ if mem.OffSetAt.IsZero() {
+ // The newest verified word is production: true — or the flag has
+ // vanished since, which never downgrades (WasProduction latches).
+ return mem.WasProduction
+ }
+ // The newest verified word is production: false.
+ if !mem.WasProduction {
+ return false // off, and never was on: nothing to ratchet
+ }
+ if !hasBakeableHead {
+ return false // the carve-out
+ }
+ // The ratchet: off is honored only after the stable window has elapsed
+ // on this box's own sighting of the off-flag, jittered like any hold —
+ // the flag defends itself with the mechanism it controls.
+ d := HoldFor(holds, RungStable)
+ eligible := mem.OffFirstSeen.Add(time.Duration(float64(d) * (1 + st.Jitter)))
+ return now.Before(eligible)
+}
diff --git a/internal/release/bake_test.go b/internal/release/bake_test.go
index 90077ebf6..7465509cb 100644
--- a/internal/release/bake_test.go
+++ b/internal/release/bake_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "fmt"
"os"
"path/filepath"
"testing"
@@ -356,3 +357,253 @@ func TestFirstSeenSaveReplacesFileItCannotWriteInPlace(t *testing.T) {
t.Fatal("observation missing after Save")
}
}
+
+// The pipelines spec defines edge as "picked up as it lands — no human in
+// the loop, by definition". Without its own entry, HoldFor fail-closes edge
+// to 72h — longer than beta, and a spec violation (2026-08-04 console bake
+// controls spec, §1/§8).
+func TestHoldForEdgeIsZero(t *testing.T) {
+ if d := HoldFor(DefaultHolds, "edge"); d != 0 {
+ t.Fatalf("edge hold = %v, want 0", d)
+ }
+}
+
+// The box-side production memory (2026-08-04 console bake controls spec §4).
+// Scenario table, because the security content IS the state machine: the
+// default is off; on is instant; off ratchets on the box's own clock; a
+// DELETED flag never downgrades a box that saw true; a stale replayed off
+// cannot roll back a newer on.
+func TestProductionMemoryRatchet(t *testing.T) {
+ base := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
+ flag := func(prod bool, setAt time.Time) *ProductionFlag {
+ return &ProductionFlag{Schema: 1, Account: "bab", App: "kass",
+ Production: prod, SetAt: setAt}
+ }
+ newClock := func() *FirstSeen {
+ st := LoadFirstSeen(filepath.Join(t.TempDir(), "clock.json"))
+ st.Jitter = 0 // determinism: eligibility is exactly firstSeen + hold
+ return st
+ }
+
+ t.Run("default is off", func(t *testing.T) {
+ st := newClock()
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("an app with no flag ever seen bakes")
+ }
+ })
+
+ t.Run("unverifiable flag fails toward baking", func(t *testing.T) {
+ st := newClock()
+ if !st.ProductionBake("bab", "kass", fmt.Errorf("sig"), true, DefaultHolds, base) {
+ t.Fatal("a corrupt flag read as the default instead of production")
+ }
+ })
+
+ t.Run("on is instant, deletion is not a downgrade", func(t *testing.T) {
+ st := newClock()
+ if !st.ObserveProduction("bab", "kass", flag(true, base), base) {
+ t.Fatal("first observation did not change the clock")
+ }
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("production: true did not apply immediately")
+ }
+ // The object vanishes from the bucket (flag == nil every later pass):
+ // the box holds its verdict.
+ st.ObserveProduction("bab", "kass", nil, base.Add(time.Hour))
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(time.Hour)) {
+ t.Fatal("deleting the flag object downgraded a box that saw true")
+ }
+ })
+
+ t.Run("off ratchets for the stable window", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ off := flag(false, base.Add(time.Hour))
+ st.ObserveProduction("bab", "kass", off, base.Add(2*time.Hour)) // box sees it an hour late
+ hold := HoldFor(DefaultHolds, RungStable)
+ // Still baking just inside the window, measured from the box's OWN
+ // sighting (base+2h), not the flag's set_at (base+1h).
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold-time.Minute)) {
+ t.Fatal("off was honored before the ratchet elapsed")
+ }
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold+time.Minute)) {
+ t.Fatal("off was not honored after the ratchet elapsed")
+ }
+ })
+
+ t.Run("no bakeable head skips the ratchet", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ st.ObserveProduction("bab", "kass", flag(false, base.Add(time.Hour)), base.Add(time.Hour))
+ if st.ProductionBake("bab", "kass", nil, false, DefaultHolds, base.Add(time.Hour)) {
+ t.Fatal("carve-out: nothing at a held head, yet off still ratcheted")
+ }
+ })
+
+ t.Run("a stale off cannot be replayed over a newer on", func(t *testing.T) {
+ st := newClock()
+ staleOff := flag(false, base)
+ st.ObserveProduction("bab", "kass", staleOff, base)
+ st.ObserveProduction("bab", "kass", flag(true, base.Add(time.Hour)), base.Add(time.Hour))
+ // Attacker re-writes the old signed off object; the box refuses to
+ // regress below the newest set_at it has honored.
+ if st.ObserveProduction("bab", "kass", staleOff, base.Add(2*time.Hour)) {
+ t.Fatal("a replayed stale flag changed the clock")
+ }
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(100*time.Hour)) {
+ t.Fatal("a replayed stale off eventually turned the bake off")
+ }
+ })
+
+ t.Run("memory survives a save/load round trip", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ if err := st.Save(); err != nil {
+ t.Fatal(err)
+ }
+ st2 := LoadFirstSeen(st.path)
+ if !st2.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("production memory did not survive persistence")
+ }
+ })
+
+ // Task-4-review fix 2: prodKey must fold the ops account the same way
+ // channelClockKey does, or an app addressed as "" (the pre-accounts
+ // bare shape some callers still use) and the same app addressed as
+ // OpsAccount silently keep SEPARATE production memories.
+ t.Run("prodKey folds the ops account like channelClockKey", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction(OpsAccount, "kass", flag(true, base), base)
+ if !st.ProductionBake("", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal(`observed under OpsAccount, not found evaluating under ""`)
+ }
+
+ st2 := newClock()
+ st2.ObserveProduction("", "kass", flag(true, base), base)
+ if !st2.ProductionBake(OpsAccount, "kass", nil, true, DefaultHolds, base) {
+ t.Fatal(`observed under "", not found evaluating under OpsAccount`)
+ }
+ // No free ride for an unrelated account: "bab"/"kass" (the ops
+ // account) must not satisfy "zzz"/"kass".
+ if st2.ProductionBake("zzz", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("an unrelated account satisfied the ops account's production memory")
+ }
+ })
+
+ // Task-4-review fix 3a: re-reading the SAME off object (the every-pass
+ // re-read, not a fresh sighting) must not change anything — in
+ // particular must not push the ratchet clock forward from the box's
+ // FIRST sighting to whatever pass happens to re-read it.
+ t.Run("equal set_at re-read does not reset the ratchet clock", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ off := flag(false, base.Add(time.Hour))
+ st.ObserveProduction("bab", "kass", off, base.Add(2*time.Hour)) // first sighting
+ if st.ObserveProduction("bab", "kass", off, base.Add(3*time.Hour)) {
+ t.Fatal("re-reading the identical off object reported a change")
+ }
+ hold := HoldFor(DefaultHolds, RungStable)
+ // Still measured from the FIRST sighting (base+2h), not the re-read
+ // (base+3h) — a re-read one hour later must not buy the flag one
+ // more hour of ratchet.
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold-time.Minute)) {
+ t.Fatal("off honored before the ratchet from its first sighting elapsed")
+ }
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(2*time.Hour).Add(hold+time.Minute)) {
+ t.Fatal("re-reading the same object pushed the ratchet clock forward")
+ }
+ })
+
+ // Task-4-review fix 3b: an in-progress off-ratchet (not just a bare
+ // "on" latch) must itself survive Save/LoadFirstSeen.
+ t.Run("off-state ratchet survives a save/load round trip", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ st.ObserveProduction("bab", "kass", flag(false, base.Add(time.Hour)), base.Add(time.Hour))
+ if err := st.Save(); err != nil {
+ t.Fatal(err)
+ }
+ st2 := LoadFirstSeen(st.path)
+ hold := HoldFor(DefaultHolds, RungStable)
+ if !st2.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(time.Hour).Add(hold-time.Minute)) {
+ t.Fatal("off-state ratchet lost on reload: should still be baking mid-window")
+ }
+ if st2.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(time.Hour).Add(hold+time.Minute)) {
+ t.Fatal("off-state ratchet lost on reload: should be off past the window")
+ }
+ })
+
+ // Task-4-review fix 3c: the ratchet is jittered exactly like any other
+ // hold (HoldFor's formula), not a bare, un-jittered 72h.
+ t.Run("the ratchet is jittered like any other hold", func(t *testing.T) {
+ st := LoadFirstSeen(filepath.Join(t.TempDir(), "clock.json"))
+ st.Jitter = 0.2
+ st.ObserveProduction("bab", "kass", flag(true, base), base)
+ st.ObserveProduction("bab", "kass", flag(false, base.Add(time.Hour)), base.Add(time.Hour))
+ hold := HoldFor(DefaultHolds, RungStable) // 72h, unjittered
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(time.Hour).Add(hold).Add(time.Minute)) {
+ t.Fatal("jitter not applied: off honored right at the un-jittered window")
+ }
+ jittered := time.Duration(float64(hold) * 1.2)
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(time.Hour).Add(jittered).Add(time.Minute)) {
+ t.Fatal("jitter not applied: off still honored past the jittered window")
+ }
+ })
+
+ // Task-4-review fix 3d: an app declared off and NEVER on has nothing to
+ // ratchet — it must never bake, immediately and forever.
+ t.Run("an app declared off and never on never bakes", func(t *testing.T) {
+ st := newClock()
+ st.ObserveProduction("bab", "kass", flag(false, base), base)
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("an app declared off, never on, baked immediately")
+ }
+ if st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base.Add(1000*time.Hour)) {
+ t.Fatal("an app declared off, never on, baked eventually")
+ }
+ })
+}
+
+// Task-4-review fix 1 (Critical): LoadFirstSeen must distinguish a file that
+// read but did not PARSE from a file that is simply absent. Before this fix
+// both collapsed to the same "empty clock" result, which for the CHANNEL
+// clock is fine (fails closed either way — everything re-bakes from now),
+// but for the PRODUCTION memory silently failed OPEN: a corrupt clock file
+// (mem == nil for every app) made ProductionBake return false, instantly
+// un-baking every production app on the box. A corrupt file must instead
+// fail TOWARD baking (st.tainted), while an absent file — a genuinely fresh
+// box — still gets the ordinary false default.
+func TestCorruptClockFailsTowardBakingNotAwayFromIt(t *testing.T) {
+ base := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
+
+ path := filepath.Join(t.TempDir(), "clock.json")
+ if err := os.WriteFile(path, []byte("{ this is not json"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ st := LoadFirstSeen(path)
+ if !st.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("a corrupt clock file did not fail toward baking")
+ }
+
+ // A genuinely fresh box (no file at all) is a different case entirely —
+ // still the ordinary false default, not tainted.
+ fresh := LoadFirstSeen(filepath.Join(t.TempDir(), "absent.json"))
+ if fresh.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("a fresh box with no clock file baked by default")
+ }
+
+ // A file that EXISTS but can't be read at all (not "did not parse") is
+ // a different os.ReadFile failure mode than os.ErrNotExist — a
+ // directory sitting at the clock path is the easiest way to reproduce
+ // it portably (EISDIR), and review found the first cut of this fix only
+ // tainted the "read OK, parse failed" branch, leaving this one to
+ // silently fail open.
+ dirPath := filepath.Join(t.TempDir(), "clock.json")
+ if err := os.Mkdir(dirPath, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ unreadable := LoadFirstSeen(dirPath)
+ if !unreadable.ProductionBake("bab", "kass", nil, true, DefaultHolds, base) {
+ t.Fatal("an unreadable (directory-at-path) clock file did not fail toward baking")
+ }
+}
diff --git a/internal/release/production.go b/internal/release/production.go
new file mode 100644
index 000000000..29383b989
--- /dev/null
+++ b/internal/release/production.go
@@ -0,0 +1,177 @@
+// The per-app production flag — the console bake controls spec
+// (docs/superpowers/specs/2026-08-04-console-bake-controls-design.md, §2–§3).
+//
+// production: true means the supply-chain bake window (bake.go) applies to
+// this app's held channels; false — and MISSING, the default — means its
+// boxes adopt immediately. The object is root-signed in the pointer's own
+// signedWrapper envelope, and root-ONLY: a wrapper carrying a key grant is
+// refused before its signature is even checked, because a scoped key that
+// could flip the flag could skip the very window it bakes under.
+//
+// What verification here does NOT decide: whether a missing or unreadable
+// flag downgrades a box that once saw production: true. It never does —
+// that memory, the ratchet and the replay floor are the box's own
+// (bake.go's production memory), because anyone with bucket write but no
+// key can delete an object, and deletion must not be a downgrade (spec §4).
+package release
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+)
+
+// ProductionFlag is the signed payload: one app's declaration that it is (or
+// is not) in production, and therefore whether its releases bake.
+type ProductionFlag struct {
+ Schema int `json:"schema"` // 1
+ Account string `json:"account"`
+ App string `json:"app"`
+ Production bool `json:"production"`
+ // Actor is who flipped it — a console member's address. Recorded for the
+ // same reason the pointer's audit trail exists: an invisible bypass is a
+ // broken gate.
+ Actor string `json:"actor,omitempty"`
+ SetAt time.Time `json:"set_at"`
+}
+
+// ProductionKey is where an app's flag lives: beside channels/ and
+// releases/, deliberately NEVER under ChannelsPrefix — those listings are
+// parsed as channel pointers by the console and the CLI.
+//
+// WARNING to anything that copies objects under an app prefix (`carlos
+// accounts migrate` is the one such thing today): this object must be
+// RE-DECLARED at a new account, never copied. Its signed payload names the
+// account, so a byte-identical copy fails validateProductionFlag at the
+// destination forever — which every box reads as "unreadable, keep baking",
+// holding the app's held heads with no expiry and no error anyone sees.
+func ProductionKey(acct, app string) string {
+ return appPrefix(acct, app) + "production.json"
+}
+
+// SetProduction writes the flag, root key only, CAS over whatever is there
+// (a concurrent flip surfaces as ErrCASConflict — re-ask, never overwrite
+// blind). SetAt is stamped here; boxes use their OWN sighting for the
+// ratchet and this stamp only as a replay floor.
+func SetProduction(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app string, production bool, actor string) error {
+ if len(priv) != ed25519.PrivateKeySize {
+ return fmt.Errorf("set production: no usable release signing key")
+ }
+ if !ValidAccount(acct) {
+ return fmt.Errorf("set production: invalid account %q", acct)
+ }
+ if !ValidApp(app) {
+ return fmt.Errorf("set production: invalid app %q", app)
+ }
+ key := ProductionKey(acct, app)
+ raw, etag, err := s.Get(ctx, key)
+ if errors.Is(err, os.ErrNotExist) {
+ etag = "" // first write: create-only
+ } else if err != nil {
+ return fmt.Errorf("set production: read %s: %w", key, err)
+ }
+ setAt := time.Now().UTC().Truncate(time.Second)
+ if err == nil {
+ // SetAt is truncated to the second; two flips inside one wall-clock
+ // second would otherwise share a value, and the box-side replay
+ // floor (bake.go's NewestSetAt) treats an equal set_at as nothing
+ // new — silently swallowing the second flip on every box. Bump
+ // strictly past whatever this key's own last signed write recorded.
+ if existing, verr := verifyProductionFlag(priv.Public().(ed25519.PublicKey), raw, acct, app); verr == nil && !existing.SetAt.Before(setAt) {
+ setAt = existing.SetAt.Add(time.Second)
+ }
+ }
+ f := &ProductionFlag{
+ Schema: 1, Account: acct, App: app, Production: production,
+ Actor: actor, SetAt: setAt,
+ }
+ if err := validateProductionFlag(f, acct, app); err != nil {
+ return fmt.Errorf("set production: %w", err)
+ }
+ payload, err := json.MarshalIndent(f, "", " ")
+ if err != nil {
+ return err
+ }
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ wrapper, err := json.MarshalIndent(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(priv, []byte(b64))),
+ }, "", " ")
+ if err != nil {
+ return err
+ }
+ _, err = s.Put(ctx, key, wrapper, etag)
+ return err
+}
+
+// GetProduction resolves an app's flag. A MISSING object is (nil, nil): the
+// default, not production — but never let a nil collapse the distinction the
+// spec draws: any OTHER failure (unreadable, unverifiable, misnamed) is the
+// error, and callers on the adopt path treat it as production=true, failing
+// toward baking.
+func GetProduction(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) (*ProductionFlag, error) {
+ raw, _, err := s.Get(ctx, ProductionKey(acct, app))
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return verifyProductionFlag(pub, raw, acct, app)
+}
+
+// verifyProductionFlag is verifyPointer's discipline for the flag: refuse a
+// grant outright, check the signature against the ROOT key alone, decode
+// after, validate the payload names the account/app whose key it sits at.
+func verifyProductionFlag(pub ed25519.PublicKey, wrapper []byte, wantAcct, wantApp string) (*ProductionFlag, error) {
+ var sw signedWrapper
+ if err := json.Unmarshal(wrapper, &sw); err != nil {
+ return nil, fmt.Errorf("production flag wrapper: %v", err)
+ }
+ if sw.Grant != nil {
+ return nil, fmt.Errorf("production flag %s/%s: signed by a scoped key — the flag takes the root key, because a delegate that could flip it could skip the window it bakes under", wantAcct, wantApp)
+ }
+ if len(pub) != ed25519.PublicKeySize {
+ return nil, fmt.Errorf("production flag: no usable release public key")
+ }
+ sig, err := base64.StdEncoding.DecodeString(sw.Sig)
+ if err != nil {
+ return nil, fmt.Errorf("production flag sig: %v", err)
+ }
+ if !ed25519.Verify(pub, []byte(sw.Payload), sig) {
+ return nil, fmt.Errorf("production flag signature verification failed")
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ return nil, fmt.Errorf("production flag payload: %v", err)
+ }
+ var f ProductionFlag
+ if err := json.Unmarshal(payload, &f); err != nil {
+ return nil, fmt.Errorf("production flag payload: %v", err)
+ }
+ if err := validateProductionFlag(&f, wantAcct, wantApp); err != nil {
+ return nil, err
+ }
+ return &f, nil
+}
+
+func validateProductionFlag(f *ProductionFlag, wantAcct, wantApp string) error {
+ if f.Schema != 1 {
+ return fmt.Errorf("unsupported production flag schema %d", f.Schema)
+ }
+ if f.Account != wantAcct || f.App != wantApp {
+ return fmt.Errorf("production flag names %s/%s, expected %s/%s", f.Account, f.App, wantAcct, wantApp)
+ }
+ if !ValidAccount(f.Account) || !ValidApp(f.App) {
+ return fmt.Errorf("production flag has invalid account/app %s/%s", f.Account, f.App)
+ }
+ if f.SetAt.IsZero() {
+ return fmt.Errorf("production flag has no set_at")
+ }
+ return nil
+}
diff --git a/internal/release/production_test.go b/internal/release/production_test.go
new file mode 100644
index 000000000..2ae0866fc
--- /dev/null
+++ b/internal/release/production_test.go
@@ -0,0 +1,240 @@
+package release
+
+// The production flag (2026-08-04 console bake controls spec §2–§3): a
+// root-signed per-app object deciding whether the supply-chain bake applies.
+// The properties tested here, in the order they'd hurt if lost: missing is
+// the default (nil, nil); a set flag round-trips; a scoped key's wrapper is
+// refused even with a valid root grant; a tampered payload is refused; the
+// key sits OUTSIDE channels/ so pointer listings never try to parse it.
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestProductionFlagDefaultIsMissing(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ _, pub := testKeys(t)
+ f, err := GetProduction(context.Background(), s, pub, "bab", "kass")
+ if err != nil || f != nil {
+ t.Fatalf("GetProduction on empty store = %v, %v; want nil, nil (the default: not production)", f, err)
+ }
+}
+
+func TestProductionFlagRoundTrips(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, "paul@keymail.dev"); err != nil {
+ t.Fatalf("SetProduction: %v", err)
+ }
+ f, err := GetProduction(ctx, s, pub, "bab", "kass")
+ if err != nil {
+ t.Fatalf("GetProduction: %v", err)
+ }
+ if f == nil || !f.Production || f.Actor != "paul@keymail.dev" || f.SetAt.IsZero() {
+ t.Fatalf("flag = %+v, want production on, actor recorded, set_at stamped", f)
+ }
+ // Flip it off over the existing object — the CAS path.
+ if err := SetProduction(ctx, s, priv, "bab", "kass", false, "paul@keymail.dev"); err != nil {
+ t.Fatalf("SetProduction off: %v", err)
+ }
+ if f, _ = GetProduction(ctx, s, pub, "bab", "kass"); f == nil || f.Production {
+ t.Fatalf("flag = %+v after flipping off, want production false", f)
+ }
+}
+
+// The flag key lives beside channels/, never inside it: ChannelsPrefix
+// listings are parsed as pointers by the console and CLI.
+func TestProductionKeyIsOutsideChannels(t *testing.T) {
+ if k := ProductionKey("bab", "kass"); k != "apps/bab/kass/production.json" {
+ t.Fatalf("ProductionKey = %q", k)
+ }
+ if strings.HasPrefix(ProductionKey("bab", "kass"), ChannelsPrefix("bab", "kass")) {
+ t.Fatal("production flag key sits under channels/ — pointer listings would try to parse it")
+ }
+}
+
+// A scoped key must not be able to mint the flag: a wrapper carrying ANY
+// grant is refused before signature checking, and a wrapper signed by a key
+// other than the root fails signature checking. Together: root only.
+func TestProductionFlagRefusesScopedAndForeignKeys(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+
+ // Build a validly-shaped flag payload signed by a DIFFERENT key.
+ otherPub, otherPriv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _ = otherPub
+ payload, _ := json.Marshal(ProductionFlag{Schema: 1, Account: "bab", App: "kass",
+ Production: false, SetAt: timeNowTestHelper()})
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ wrapper, _ := json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(otherPriv, []byte(b64))),
+ })
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "kass"); err == nil {
+ t.Fatal("a flag signed by a non-root key verified")
+ }
+
+ // A wrapper WITH a grant is refused outright — even a grant the root
+ // really signed. Mint a real scoped key and have it sign the flag.
+ seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ scopedKey, err := ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sg, err := parseGrantBlob(grantB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ wrapper, _ = json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(scopedKey, []byte(b64))),
+ Grant: sg,
+ })
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, etagOfKey(t, s, "bab", "kass")); err != nil {
+ t.Fatal(err)
+ }
+ _, err = GetProduction(ctx, s, pub, "bab", "kass")
+ if err == nil || !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a grant-bearing flag wrapper verified (err=%v), want a root-only refusal", err)
+ }
+}
+
+// etagOfKey reads the current etag of an app's flag so a test can CAS over it.
+func etagOfKey(t *testing.T, s Store, acct, app string) string {
+ t.Helper()
+ _, etag, err := s.Get(context.Background(), ProductionKey(acct, app))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return etag
+}
+
+func TestProductionFlagRefusesWrongApp(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, ""); err != nil {
+ t.Fatal(err)
+ }
+ // Copy kass's flag to keymail's key — a cross-app replay must be refused
+ // because the SIGNED payload names the app.
+ raw, _, err := s.Get(ctx, ProductionKey("bab", "kass"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "keymail"), raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "keymail"); err == nil {
+ t.Fatal("another app's flag verified under keymail's key")
+ }
+}
+
+// The signature binds the exact base64 payload STRING, not the decoded JSON
+// meaning — so a payload that is decoded, edited, and re-encoded (even
+// leaving the original Sig in place) must fail verification. A verifier that
+// decoded first and checked a signature over re-marshalled bytes would let
+// this through if the re-marshalling happened to reproduce the tampered
+// bytes; this test pins the "no canonicalisation" wire rule directly against
+// the field an attacker would actually want to flip.
+func TestProductionFlagRefusesTamperedPayload(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, "paul@keymail.dev"); err != nil {
+ t.Fatal(err)
+ }
+ raw, etag, err := s.Get(ctx, ProductionKey("bab", "kass"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var sw signedWrapper
+ if err := json.Unmarshal(raw, &sw); err != nil {
+ t.Fatal(err)
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var f ProductionFlag
+ if err := json.Unmarshal(payload, &f); err != nil {
+ t.Fatal(err)
+ }
+ if !f.Production {
+ t.Fatal("expected production=true before tampering")
+ }
+ f.Production = false // flip the bit an attacker would want to flip
+ tampered, err := json.Marshal(&f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Keep the ORIGINAL Sig — it covers the ORIGINAL payload string. A
+ // re-encoded payload, even with identical decoded meaning, must fail
+ // verification without a fresh signature.
+ sw.Payload = base64.StdEncoding.EncodeToString(tampered)
+ wrapper, err := json.Marshal(sw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, etag); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "kass"); err == nil {
+ t.Fatal("a tampered payload (re-encoded, original sig) verified")
+ }
+}
+
+// timeNowTestHelper is a stamped, truncated now — what signProductionFlag
+// itself writes — for tests that hand-build payloads.
+func timeNowTestHelper() time.Time { return time.Now().UTC().Truncate(time.Second) }
+
+// Task-4-review fix 5 (Minor, promoted), tightened by re-review: SetAt is
+// truncated to the second, so two flips within the same wall-clock second
+// would otherwise share a set_at — and the box-side replay floor (bake.go's
+// NewestSetAt) treats an equal set_at as nothing new, silently swallowing
+// the second flip on every box that already saw the first. A single
+// before/after pair is vacuous if the two calls happen to straddle a
+// second boundary (it would pass with or without the bump). Looping several
+// rapid flips and requiring EVERY consecutive pair to be strictly
+// increasing removes that escape: the bump guarantees strict monotonicity
+// regardless of whether any given pair lands in the same truncated second
+// or crosses one, so this holds deterministically with the fix in place.
+func TestSetProductionMonotonicSetAtOnRapidFlip(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+
+ var prev time.Time
+ for i := 0; i < 3; i++ {
+ if err := SetProduction(ctx, s, priv, "bab", "kass", i%2 == 0, "paul@keymail.dev"); err != nil {
+ t.Fatalf("SetProduction flip %d: %v", i, err)
+ }
+ f, err := GetProduction(ctx, s, pub, "bab", "kass")
+ if err != nil || f == nil {
+ t.Fatalf("GetProduction after flip %d: %v, %v", i, f, err)
+ }
+ if i > 0 && !f.SetAt.After(prev) {
+ t.Fatalf("flip %d's set_at %v not strictly after flip %d's %v (same-second collision not bumped)",
+ i, f.SetAt, i-1, prev)
+ }
+ prev = f.SetAt
+ }
+}
diff --git a/internal/release/release.go b/internal/release/release.go
index e67ac5ab9..5fec54c66 100644
--- a/internal/release/release.go
+++ b/internal/release/release.go
@@ -81,6 +81,13 @@ type ChannelPointer struct {
// pointer written before the trail existed has no "audit" key, parses
// with none, and is not an error; that is most of the live fleet.
Audit []Promotion `json:"audit,omitempty"`
+ // FastTrack waives the supply-chain bake window for THIS version on THIS
+ // channel (2026-08-04 console bake controls spec §5). Root-key-only at
+ // mint AND at verification — a scoped key must never skip the window it
+ // bakes under. Set per write and never inherited: the next promotion
+ // starts clean. ADDITIVE: old pointers have no key; old binaries drop the
+ // field and keep baking, the safe direction.
+ FastTrack bool `json:"fast_track,omitempty"`
}
// Promotion is one entry in a channel pointer's audit trail. It rides inside
@@ -99,8 +106,11 @@ type Promotion struct {
Actor string `json:"actor,omitempty"`
// Hotfix records that the ladder was bypassed. This is the whole reason
// the field exists: a bypass nobody can see is a broken gate.
- Hotfix bool `json:"hotfix,omitempty"`
- At time.Time `json:"at"`
+ Hotfix bool `json:"hotfix,omitempty"`
+ // FastTrack records that this write waived the bake window — same
+ // reasoning as Hotfix: a bypass nobody can see is a broken gate.
+ FastTrack bool `json:"fast_track,omitempty"`
+ At time.Time `json:"at"`
}
// historyMax bounds a pointer's rollback history; auditMax bounds its
@@ -272,6 +282,9 @@ func signPointer(sg Signer, p *ChannelPointer) ([]byte, error) {
if err := sg.allows(p.Channel, p.PromotedAt); err != nil {
return nil, err
}
+ if p.FastTrack && sg.scope != nil {
+ return nil, fmt.Errorf("fast-track is root-key-only — key grant %q cannot waive the bake window", sg.scope.Name)
+ }
payload, err := json.MarshalIndent(p, "", " ")
if err != nil {
return nil, err
@@ -344,6 +357,9 @@ func verifyPointer(pub ed25519.PublicKey, wrapper []byte, wantApp, wantChannel s
if err := validatePointer(&p, wantApp, wantChannel); err != nil {
return nil, err
}
+ if p.FastTrack && sw.Grant != nil {
+ return nil, fmt.Errorf("channel pointer %s/%s: fast_track is root-key-only — a scoped key cannot waive the bake window", p.App, p.Channel)
+ }
if scope != nil {
// promoted_at, not the wall clock: adoption re-verifies every pass, so
// comparing expiry against now would make an expired grant take a
@@ -854,6 +870,57 @@ func (sg Signer) Rollback(ctx context.Context, s Store, acct, app, channel strin
return err
}
+// FastTrack re-signs a channel's current pointer with the bake-window
+// waiver set — same version, same history, one new audit entry (the
+// Rollback precedent: a write that is not a promotion stays out of the
+// ladder). Root key only: the window defends subscribers against exactly
+// the keys that are not the root, so this takes priv directly and never a
+// Signer. A head already waived is a no-op success. Returns the version
+// waived either way.
+//
+// signPointer re-stamps PromotedAt, which is correct here: the bake clock
+// keys on the box's own first sighting of the VERSION (bake.go), which this
+// write does not change, and the console suppresses its bakes-until
+// estimate for a waived head.
+func FastTrack(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app, channel, actor string) (string, error) {
+ sg := RootSigner(priv)
+ if !ValidAccount(acct) {
+ return "", fmt.Errorf("fast-track: invalid account %q", acct)
+ }
+ if !ValidApp(app) || !ValidChannel(channel) {
+ return "", fmt.Errorf("fast-track: invalid app/channel %s/%s", app, channel)
+ }
+ key := ChannelKey(acct, app, channel)
+ cur, etag, err := GetQualified(ctx, s, key)
+ if err != nil {
+ return "", fmt.Errorf("fast-track: %w", err)
+ }
+ old, err := verifyPointer(sg.verifyPub(), cur, app, channel)
+ if err != nil {
+ return "", fmt.Errorf("fast-track: existing pointer at %s: %w", key, err)
+ }
+ if old.FastTrack {
+ return old.Version, nil
+ }
+ rung, _ := Rung(channel)
+ next := &ChannelPointer{
+ App: app, Channel: channel, Version: old.Version,
+ History: old.History, FastTrack: true,
+ Audit: prependCapped(Promotion{
+ Version: old.Version, From: rung, To: rung, Actor: actor,
+ FastTrack: true, At: time.Now().UTC().Truncate(time.Second),
+ }, old.Audit, auditMax),
+ }
+ wrapper, err := signPointer(sg, next)
+ if err != nil {
+ return "", err
+ }
+ if _, err := s.Put(ctx, key, wrapper, etag); err != nil {
+ return "", err
+ }
+ return old.Version, nil
+}
+
// prependCapped puts v at the head of list, dropping the tail beyond max —
// the newest-first, bounded shape both the rollback history and the audit
// trail have.
diff --git a/internal/release/release_test.go b/internal/release/release_test.go
index a4385b872..8c0101cbe 100644
--- a/internal/release/release_test.go
+++ b/internal/release/release_test.go
@@ -537,3 +537,146 @@ func TestPointerWrittenBeforeAuditStillParses(t *testing.T) {
t.Fatalf("stable audit = %+v", sp.Audit)
}
}
+
+// Fast-track (2026-08-04 console bake controls spec §5): re-sign the head
+// with the bake waiver — same version, same history, one new audit entry —
+// and never from a scoped key, at mint or at verification.
+func TestFastTrackWaivesTheHeadInPlace(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ shipRelease(t, s, "bab", "kass", "v2", "linux-amd64", []byte("b2"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v2"); err != nil {
+ t.Fatal(err)
+ }
+
+ ver, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev")
+ if err != nil {
+ t.Fatalf("FastTrack: %v", err)
+ }
+ if ver != "v2" {
+ t.Fatalf("FastTrack waived %q, want the head v2", ver)
+ }
+ p, err := Channel(ctx, s, pub, "bab", "kass", "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !p.FastTrack || p.Version != "v2" {
+ t.Fatalf("pointer = version %s fast_track %v, want v2 waived", p.Version, p.FastTrack)
+ }
+ // History survives — a fast-tracked head must stay rollbackable.
+ if len(p.History) == 0 || p.History[0] != "v1" {
+ t.Fatalf("history = %v, want v1 preserved", p.History)
+ }
+ // The waiver is recorded on the trail it waives.
+ if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
+ t.Fatalf("audit[0] = %+v, want a fast_track entry naming the actor", p.Audit)
+ }
+ // Idempotent: a second fast-track is a no-op success.
+ if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev"); err != nil {
+ t.Fatalf("repeated FastTrack: %v", err)
+ }
+ // The NEXT promotion starts clean — fast_track is per write, never
+ // inherited (spec §5).
+ shipRelease(t, s, "bab", "kass", "v3", "linux-amd64", []byte("b3"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v3"); err != nil {
+ t.Fatal(err)
+ }
+ if p, _ := Channel(ctx, s, pub, "bab", "kass", "stable"); p.FastTrack {
+ t.Fatal("fast_track survived onto the next promotion")
+ }
+}
+
+// Root only, both directions (mint AND verification, like the grant cap):
+// a scoped signer cannot SIGN a fast-tracked pointer, and a hand-built
+// grant-bearing wrapper whose payload carries fast_track does not VERIFY.
+func TestFastTrackIsRootOnly(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "edge", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ scoped, err := ScopedSigner(key, grantB64, pub)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Mint side: the scoped signer refuses the payload.
+ if _, err := signPointer(scoped, &ChannelPointer{
+ App: "kass", Channel: "edge", Version: "v1", FastTrack: true,
+ }); err == nil || !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a scoped signer signed a fast-tracked pointer (err=%v)", err)
+ }
+ // Verify side: build the wrapper by hand, exactly as a patched-out mint
+ // check would, and watch verification refuse it.
+ payload, _ := json.Marshal(ChannelPointer{Schema: 1, App: "kass", Channel: "edge",
+ Version: "v1", FastTrack: true, PromotedAt: timeNowTestHelper()})
+ b64 := base64.StdEncoding.EncodeToString(payload)
+ sg, err := parseGrantBlob(grantB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ wrapper, _ := json.Marshal(signedWrapper{
+ Payload: b64,
+ Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(key, []byte(b64))),
+ Grant: sg,
+ })
+ if _, err := verifyPointer(pub, wrapper, "kass", "edge"); err == nil ||
+ !strings.Contains(err.Error(), "root") {
+ t.Fatalf("a scoped-signed fast_track pointer verified (err=%v)", err)
+ }
+}
+
+// The wire change is additive both ways: a pre-change pointer still parses
+// (no fast_track key at all), and a fast-tracked pointer parses in a struct
+// that predates the field — the old binary simply keeps baking.
+func TestFastTrackIsAdditiveOnTheWire(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, _ := testKeys(t)
+ shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
+ if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", ""); err != nil {
+ t.Fatal(err)
+ }
+ raw, _, err := GetQualified(ctx, s, ChannelKey("bab", "kass", "stable"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var sw signedWrapper
+ if err := json.Unmarshal(raw, &sw); err != nil {
+ t.Fatal(err)
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The old binary's shape: every pre-fast_track field, nothing else.
+ var old struct {
+ Schema int `json:"schema"`
+ App string `json:"app"`
+ Channel string `json:"channel"`
+ Version string `json:"version"`
+ }
+ if err := json.Unmarshal(payload, &old); err != nil {
+ t.Fatalf("an old struct shape failed to parse a fast-tracked payload: %v", err)
+ }
+ if old.Version != "v1" {
+ t.Fatalf("old shape read version %q", old.Version)
+ }
+}