carlos / platform Public

bake: the ratchet memory, not the production co-write, decides the verdict

An app whose pipeline read ERRORED, or whose pipeline was DELETED and is
mid-ratchet, had its WINDOWS floored by the box's own ratchet memory but
its VERDICT taken from the legacy production flag — so a flagless app's
held heads adopted immediately the instant one GET of pipeline.json
failed, or the object was deleted, defeating the very floor the pass had
just computed. Enforcement rode on the console's production: true
co-write, a bucket write the box can never confirm happened (an app
pipelined before the co-write existed, an app whose flag was deleted, a
direct-bucket pipeline write all lack it).

bakeIgnore now never ignores holds for any app this pass built a window
map for: the memory IS the evidence the app was pipelined. The flag path
is left to apps with no pipeline signal at all — every legacy app,
byte-identically — and to a once-pipelined app after its deletion
ratchet has landed every remembered channel back at its legacy default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 66740c28075efc48a012d33a574fb97edd2f9807 parent a347034
2 files changed, +163 −13
  • internal/release/adopt.go +42 −13
  • internal/release/adopt_test.go +121 −0
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 718c6cee2..0e9b7ecd0 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -348,9 +348,11 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
// name like edge its ZERO legacy window back). A channel with no memory
// keeps the fail-closed DefaultHolds resolution — the old-binary
// posture (spec §3's old-binary edge), whose production: true co-write
- // remains the mitigation for a MEMORY-LESS box only. The production-
- // flag machinery still applies to an errored app exactly as to any
- // legacy app. The error is NEVER folded into the ratchet as a nil: nil
+ // remains the mitigation for a MEMORY-LESS box only. Where memory DOES
+ // exist, the floor is enforcement in full: the bake VERDICT rides on
+ // the same evidence (bakeIgnore below never ignores a floored app), so
+ // nothing here depends on the console having managed the co-write.
+ // The error is NEVER folded into the ratchet as a nil: nil
// means verified-absent only (ObservePipeline's contract), and a
// corrupt object must not masquerade as a deletion — a deletion RATCHETS
// each remembered channel down to its legacy default over one old-window
@@ -379,7 +381,7 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
// non-tampering trigger too: on a GetObject-without-ListBucket
// fleet a GET of an ABSENT key answers 403, not 404, and every
// app would land here every pass.
- fmt.Printf(" %s/%s: pipeline unreadable (%v) — legacy holds and the production flag apply\n", ak.acct, ak.app, perr)
+ fmt.Printf(" %s/%s: pipeline unreadable (%v) — remembered windows still apply; anything unremembered falls to legacy holds\n", ak.acct, ak.app, perr)
}
}
@@ -459,6 +461,11 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
// path. An app with no memory at all — every legacy app, every
// pass — takes that bare path byte-identically, and the len
// check keeps the fleet-wide no-pipeline case allocation-free.
+ //
+ // Whether a map lands in appHolds is therefore also the pass's
+ // answer to "is this app pipelined as far as this box knows" —
+ // bakeIgnore reads exactly that, so the floor and the verdict
+ // can never be derived from two different pieces of evidence.
if len(bake.Holds) == 0 {
break
}
@@ -513,7 +520,8 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
// backoff window at worst, not one per tick. A PIPELINED app's held
// head pays the read as well even though its verdict never consults the
// flag (bakeIgnore below): the fold keeps the legacy latch fresh for
- // the day the pipeline is deleted and the app falls back to it.
+ // the day the pipeline is deleted AND its deletion ratchet has landed,
+ // which is when the app falls back to the flag for real.
type prodState struct {
flag *ProductionFlag
err error // non-nil: the object exists but would not read or verify
@@ -627,11 +635,30 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
ak := acctApp{RouteAccount(rt), rt.App}
// A PIPELINED app never consults the flag: its bake policy is the
// pipeline's alone (spec §3) — never ignore, and a zero-window
- // channel holds nothing anyway. The flag stays meaningful for
- // legacy apps, and for THIS app on a pass whose pipeline read
- // errored, which takes exactly that legacy verdict path (its
- // WINDOWS are memory-floored above; the verdict is unchanged).
- if ps := pipes[ak]; ps.err == nil && ps.p != nil {
+ // channel holds nothing anyway.
+ //
+ // "Pipelined" here is EVERY app this pass built a window map for
+ // (appHolds), not only the apps whose read verified this pass. The
+ // memory floor is built exactly when this box's own ratchet
+ // remembers a window above legacy for the app, and that memory IS
+ // the evidence the app was pipelined — the errored-read and the
+ // verified-absent-mid-ratchet cases both land here. Riding the
+ // verdict on the console's production: true CO-WRITE instead would
+ // make enforcement depend on a bucket write this box can never
+ // confirm happened: an app pipelined before the co-write existed, an
+ // app whose flag was deleted, a direct-bucket pipeline write — each
+ // leaves a flagless app whose held heads would adopt the instant one
+ // GET of pipeline.json errored, or the instant the object was
+ // deleted, defeating the very floor computed above. The co-write
+ // keeps its own job, which was always the MEMORY-LESS box (spec §3's
+ // old-binary edge — any box that never saw the pipeline).
+ //
+ // The flag path is left to apps with no pipeline signal at all —
+ // every legacy app, byte-identically — and to a once-pipelined app
+ // after its deletion ratchet has landed every remembered channel
+ // back at its legacy default: no floor map is built then, and the
+ // app is legacy again in full.
+ if _, floored := appHolds[ak]; floored {
return false
}
return !bake.ProductionBake(ak.acct, ak.app, prod[ak].err, bakeable[ak], DefaultHolds, now)
@@ -764,12 +791,14 @@ func RouteAccount(rt registry.Route) string {
// holds is the enforced per-channel window map the caller resolved for this
// route's app: a verified pipeline's ratcheted HoldsForApp map (canary/*
// stitched back in at zero), DefaultHolds floored by the ratchet memory for
-// an app whose pipeline read errored, else DefaultHolds — never consulted
-// app-blind.
+// an app whose pipeline read errored or is verified-absent mid-ratchet, else
+// DefaultHolds — never consulted app-blind.
//
// ignore is the app-level bake verdict from ProductionBake — the LEGACY
// production flag's doing, never a publisher field, and constant false for
-// a pipelined app, whose windows come from holds alone (spec §3). True
+// any app this box carries pipeline evidence for: a verified pipeline, or a
+// ratchet memory standing above legacy for it (see bakeIgnore). Such an
+// app's windows come from holds alone (spec §3). 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.
diff --git a/internal/release/adopt_test.go b/internal/release/adopt_test.go
index 0268cd28b..5498550a6 100644
--- a/internal/release/adopt_test.go
+++ b/internal/release/adopt_test.go
@@ -3095,6 +3095,127 @@ func TestPassDeletedPipelineRatchetsDown(t *testing.T) {
}
}
+// TestPassErroredPipelineHoldsWithoutProductionFlag: the memory floor is only
+// half a defence if the app's held head is then ADOPTED anyway. The bake
+// VERDICT ("does the window apply at all?") must ride on the same evidence
+// the WINDOWS do — the box's own ratchet memory — not on the production flag
+// the console co-writes. A co-write is a bucket write the box cannot verify
+// happened: an app pipelined before that co-write existed, an app whose flag
+// was deleted, a direct-bucket pipeline write — all leave a flagless app with
+// a live ratchet memory, and a legacy verdict would adopt its held head the
+// moment one GET of pipeline.json errored. NO production flag is set here.
+func TestPassErroredPipelineHoldsWithoutProductionFlag(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
+ {Name: "edge", Bake: "24h"},
+ }, "test"); err != nil {
+ t.Fatalf("SetPipeline: %v", err)
+ }
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
+ t.Fatalf("Promote: %v", err)
+ }
+ addRoute(t, reg, "e.example.com", "myapp", "edge", false)
+
+ // Pass 1, pipeline verified: edge's zero baseline tightens to 24h
+ // immediately (a tighten is never pending) — held, flag or no flag.
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
+ t.Fatalf("live = %q, want held — a pipelined app's window applies with no production flag at all", v)
+ }
+
+ // Corrupt the object in place: the next read ERRORS.
+ _, etag, err := s.Get(ctx, PipelineKey("bab", "myapp"))
+ if err != nil {
+ t.Fatalf("read pipeline etag: %v", err)
+ }
+ if _, err := s.Put(ctx, PipelineKey("bab", "myapp"), []byte("not a wrapper"), etag); err != nil {
+ t.Fatalf("corrupt the pipeline object: %v", err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
+ t.Fatalf("live = %q, want STILL held — the ratchet memory IS the evidence this app is pipelined; the verdict must not fall back to the production flag", v)
+ }
+ if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[channelClockKey("bab", "myapp", "edge")]; mem == nil || mem.Window != 24*time.Hour {
+ t.Fatalf("hold memory = %+v, want the 24h floor still remembered — otherwise this test proves nothing", mem)
+ }
+}
+
+// TestPassDeletedPipelineHoldsWithoutProductionFlag: same law on the
+// verified-absent path. Deleting pipeline.json is a plain bucket write; the
+// deletion ratchet keeps the 24h window enforced for one old-window cycle,
+// and the VERDICT must honour it without any production flag — otherwise
+// deleting one object adopts every held head of a flagless pipelined app in
+// the very next pass, which is precisely the collapse the ratchet exists to
+// prevent.
+func TestPassDeletedPipelineHoldsWithoutProductionFlag(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+
+ if _, err := SetPipeline(ctx, s, priv, "bab", "myapp", []PipelineChannel{
+ {Name: "edge", Bake: "24h"},
+ }, "test"); err != nil {
+ t.Fatalf("SetPipeline: %v", err)
+ }
+ shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("bin"))
+ if err := Promote(ctx, s, priv, "bab", "myapp", "edge", "v1"); err != nil {
+ t.Fatalf("Promote: %v", err)
+ }
+ addRoute(t, reg, "e.example.com", "myapp", "edge", 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, "e.example.com"); v != "" {
+ t.Fatalf("live = %q, want held — the verified pipeline tightened edge to 24h", v)
+ }
+
+ if err := s.Delete(ctx, PipelineKey("bab", "myapp")); err != nil {
+ t.Fatalf("delete pipeline: %v", err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "e.example.com"); v != "" {
+ t.Fatalf("live = %q, want STILL held — deletion ratchets the window down over a cycle, and the verdict must ride on that memory, not on a production flag nobody set", v)
+ }
+ key := channelClockKey("bab", "myapp", "edge")
+ if mem := LoadFirstSeen(FirstSeenPath(opt)).Holds[key]; mem == nil || mem.Window != 24*time.Hour || mem.PendingAt.IsZero() {
+ t.Fatalf("hold memory = %+v, want 24h enforced with the legacy 0 pending", mem)
+ }
+
+ // And the ratchet still LANDS: once the old window has elapsed on the
+ // box's own clock the app is legacy again — memory at the legacy default,
+ // no floor map, and the bare-DefaultHolds verdict adopts. The fix must not
+ // pin a once-pipelined app to a hold forever.
+ st := LoadFirstSeen(FirstSeenPath(opt))
+ st.Holds[key].PendingAt = st.Holds[key].PendingAt.Add(-31 * time.Hour)
+ if seen, ok := st.Channels[key]["v1"]; ok {
+ st.Channels[key]["v1"] = seen.Add(-31 * time.Hour)
+ }
+ if err := st.Save(); err != nil {
+ t.Fatalf("Save backdated clock: %v", err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, "e.example.com"); v != "v1" {
+ t.Fatalf("live = %q, want v1 — the old window elapsed, so legacy holds (edge 0) resume and the legacy verdict adopts", v)
+ }
+}
+
// TestPassNoPipelineNoMemoryStaysLegacy pins the other half of the deletion
// ratchet's contract: an app with NO pipeline and NO ratchet memory — every
// legacy app on every pass — keeps the bare DefaultHolds path byte-identically.