release: production memory on the bake clock — off ratchets, deletion never downgrades, replays refused
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
2 files changed,
+201
−0
internal/release/bake.go+100 −0internal/release/bake_test.go+101 −0
diff --git a/internal/release/bake.go b/internal/release/bake.go| index 85b2cac26..dfb5c8a9e 100644 |
| --- a/internal/release/bake.go |
| +++ b/internal/release/bake.go |
| @@ -86,6 +86,12 @@ 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 "<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"` |
| + |
| path string // where Save persists; set by LoadFirstSeen |
| // migrated marks a clock LoadFirstSeenMigrating built from its |
| @@ -139,6 +145,9 @@ func LoadFirstSeen(path string) *FirstSeen { |
| 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 |
| } |
| @@ -304,3 +313,94 @@ 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,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) |
| +} |
diff --git a/internal/release/bake_test.go b/internal/release/bake_test.go| index 4b66e0004..a4ca340c1 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" |
| @@ -366,3 +367,103 @@ func TestHoldForEdgeIsZero(t *testing.T) { |
| 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") |
| + } |
| + }) |
| +} |