// 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 claimRawSetProduction(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) } // A trashed app cannot be declared production, in EITHER direction: §3 // already refuses ship/promote to one, and a production flag appearing // on an app in the trash would contradict the flag's own promise // (production = never deletable). There is nothing legitimate to do to // a trashed app's flag; restore first. Unreadable refuses too — the // usual direction: a caller about to declare production must never act // as though a marker it cannot verify simply isn't there. tomb, err := ReadAppTombstone(ctx, s, acct, app) if err != nil { return fmt.Errorf("set production: cannot check the trash marker for %s/%s: %w", acct, app, err) } if tomb != nil { return fmt.Errorf("set production: %s/%s is in the trash — restore it before flagging it production", acct, 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 } func SetProduction(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app string, production bool, actor string) (claimResult0 error) { claimResult0 = WithAppMutation(ctx, s, acct, app, "release.SetProduction", func(claimContext context.Context) error { claimResult0 = claimRawSetProduction(claimContext, s, priv, acct, app, production, actor) return claimResult0 }) return }