release: fast_track waives the bake window — root-only at mint and at verification
ChannelPointer.FastTrack and Promotion.FastTrack (both omitempty, additive on the wire) plus the FastTrack verb: re-signs a channel's head in place with the waiver set, same version and history, one new audit entry, and never inherited onto the next promotion. Root-key-only in both directions — signPointer refuses a scoped key's grant, verifyPointer refuses a grant-bearing wrapper whose payload carries fast_track — since the window defends subscribers against exactly the keys that are not the root.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
2 files changed,
+212
−2
internal/release/release.go+69 −2internal/release/release_test.go+143 −0
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) |
| + } |
| +} |