carlos / platform Public

console: bake controls — on for members, off and fast-track behind owner + passkey

Three doors for the bake window (2026-08-04 console bake controls spec §7),
with the gate order the repo has paid for: session → CSRF → owner as a
byte-identical 404 → requireFreshAuth LAST.

- POST .../production flips the per-app flag. ON is any member's, instantly:
  the safe direction stays cheap. OFF takes an account owner behind the
  passkey, because it removes a supply-chain guard from every subscriber —
  and a member probing it meets the 404 before any step-up redirect could
  confirm the account exists.
- POST .../fasttrack waives the window for a channel's current head. Owner +
  passkey always; the pointer wears the badge and the actor forever.
- enable_bake=1 on promote turns the flag on inside the same
  passkey-confirmed stable promotion, and only when it is off.

The promote flash's bake note now follows the flag as well as the hold: with
production defaulting off, "adopts after the bake window" was a promise a
non-production app's boxes do not keep. An unreadable flag still shows the
note — the adopt path fails toward baking, so that is the truth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
Paul Campbell 0e361098b85551e6f8d60bafadb0713875e320b2 parent aa9e3ac
3 files changed, +331 −4
  • internal/console/bakecontrols_test.go +192 −0
  • internal/console/server.go +132 −1
  • internal/console/server_test.go +7 −3
diff --git a/internal/console/bakecontrols_test.go b/internal/console/bakecontrols_test.go
new file mode 100644
index 000000000..c252ef2af
--- /dev/null
+++ b/internal/console/bakecontrols_test.go
@@ -0,0 +1,192 @@
+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
+}
+
+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)
+ }
+}
+
+// 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, _, _ := 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")
+ }
+}
+
+// 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)
+ }
+}
+
+// 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")
+ postForm(t, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "version": {"v3"}, "channel": {"beta"}})
+ 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")
+ postForm(t, c, fx.u("/apps/kass/promote"),
+ url.Values{"csrf": {csrf}, "version": {"v4"}, "channel": {"beta"}})
+ 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/server.go b/internal/console/server.go
index 5e44aa521..eae1c5167 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)
@@ -1365,7 +1367,32 @@ 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.
+ if r.FormValue("enable_bake") == "1" && ferr == nil && !baking {
+ 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."
+ 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)
@@ -1438,6 +1465,110 @@ 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: 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.")
+}
+
// ---- 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 3f078f8ae..c959921af 100644
--- a/internal/console/server_test.go
+++ b/internal/console/server_test.go
@@ -497,7 +497,7 @@ func TestPromoteFlow(t *testing.T) {
_, _, body = get(t, c, fx.u("/apps/kass"))
// 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 appears for channels with a hold (beta, stable).
+ // 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)
}
@@ -509,8 +509,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"}})