Documentation
¶
Overview ¶
Package featuregate is the launch-control plane for Hanzo's hosted services: the ONE source of truth for whether each service (studio.hanzo.ai, hanzo.chat, console.hanzo.ai, hanzo.app, api.hanzo.ai, hanzo.team, …) is in WAITLIST MODE. It owns a small global SQLite registry {service, hosts, waitlistMode} and serves the control-plane admin.hanzo.ai calls to list services and flip a service's mode — the "remove the waitlist one service at a time" toggle.
TWO knobs, ONE rule (decomplected):
- PER-SERVICE waitlistMode on|off — this registry (a row per hosted service). OFF = open to any signed-up user. This is the admin's launch lever.
- PER-USER approvalStatus pending|approved — owned by IAM (iam#104: get-pending-users / approve-user / reject-user). REUSED, not rebuilt.
THE RULE, applied at ONE native enforcement point (Enforce, middleware.go) and mirrored by the interim @file waitlist-guard (which reads the SAME registry over GET /v1/featuregate/mode so an admin toggle governs it WITHOUT an ingress edit):
if waitlistMode[host] AND NOT user.approved → bounce to the waitlist if approved OR mode=off → allow unauthenticated → login first
Surface (all /v1/, never /api/):
GET /v1/admin/services (global-admin) list every service + mode — the board
POST /v1/admin/services/:service/mode (global-admin) flip waitlistMode {waitlistMode:bool}
GET /v1/featuregate/mode?host=<h> (public read) the guard's runtime mode lookup
The Pending-Users QUEUE (approve/reject/pending) is NOT re-served here — it is IAM's iam#104, reached by admin.hanzo.ai through its existing /admin/iam gated proxy. One approval API, one registry, one enforcement rule — no per-app copy.
serve.go auto-registers GET /v1/featuregate/health.
Index ¶
- func Enforce(cfg EnforceConfig) zip.Handler
- func Mount(app *zip.App, deps cloud.Deps) error
- func NormalizeHost(host string) string
- func Shutdown() error
- type Approvals
- type EnforceConfig
- type SeedService
- type Service
- type Store
- func (s *Store) Close() error
- func (s *Store) Get(ctx context.Context, service string) (Service, error)
- func (s *Store) List(ctx context.Context) ([]Service, error)
- func (s *Store) ModeForHost(ctx context.Context, host string) (mode bool, service string, known bool, err error)
- func (s *Store) Seed(ctx context.Context, rows []SeedService, now int64) (int, error)
- func (s *Store) SetMode(ctx context.Context, service string, mode bool, by string, now int64) (Service, error)
- func (s *Store) Upsert(ctx context.Context, in Service, by string, now int64) (Service, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Enforce ¶
func Enforce(cfg EnforceConfig) zip.Handler
Enforce builds the native enforcement middleware. It is a no-op passthrough when no registry store is resolved yet (moduleStore() nil, i.e. Mount hasn't run) — so a request before boot completes is never wrongly gated.
func NormalizeHost ¶
NormalizeHost reduces a request Host to the registry key: lowercased, trimmed, port stripped. ONE canonicalization for the seed, the toggle, and every lookup, so "Hanzo.Chat:443" and "hanzo.chat" resolve to the same service.
Types ¶
type Approvals ¶
type Approvals struct {
// contains filtered or unexported fields
}
Approvals resolves whether the current caller is off the waitlist. It is the ONE approval predicate the native middleware uses, DRY with the @file waitlist-guard (both read properties.approvalStatus == "pending"). Resolution order:
- global admin (c.IsAdmin()) → approved (admins are never gated)
- validated header X-User-Approved → its bit (forward-perfect, no lookup)
- IAM get-account (caller's creds) → approved unless approvalStatus=="pending" — cached per user for ttl; FAIL-OPEN on any IAM error.
func NewApprovals ¶
NewApprovals builds a resolver. iamBase is the in-cluster IAM base (e.g. http://iam.hanzo.svc.cluster.local:8000); ttl bounds the per-user cache. A zero iamBase yields a resolver whose lookup always fails-open (approved) — safe for a deployment where approval is enforced elsewhere (the guard).
type EnforceConfig ¶
type EnforceConfig struct {
// WaitlistURL is where an unapproved / unauthenticated browser is bounced
// (per-brand, e.g. https://waitlist.hanzo.ai). Empty → API-style 403/401 for
// everyone (no redirect target), so enforcement still holds.
WaitlistURL string
// Approvals resolves whether the caller is off the waitlist. When nil, Enforce
// builds one from IAMBase.
Approvals *Approvals
// IAMBase is the in-cluster IAM base used to build Approvals when it is nil.
IAMBase string
// ExemptPrefixes are request-path prefixes never gated (health/metrics/auth).
// A sensible default set is used when empty.
ExemptPrefixes []string
}
Enforce is the NATIVE, forward-perfect enforcement point for the waitlist — the single in-binary middleware that reads the registry (in-process, no HTTP hop) and the caller's approval, and applies THE RULE for every request whose Host is a governed service. As product hosts fold into the one-binary cloud, this is the ONE enforcement point (the @file waitlist-guard is the interim gate for hosts not yet cloud-fronted; both read the SAME registry so a toggle governs both).
THE RULE (per request, on a governed host in waitlist mode):
carries a Hanzo API key (hk-/sk-/…) → allow (paid inference; possession-gated) exempt path (health/iam/waitlist) → allow unauthenticated → 302 waitlist (browser) / 401 (API) waitlist mode OFF (or host un-governed) → allow (c.Next) waitlist mode ON AND approved → allow (c.Next) waitlist mode ON AND NOT approved → 302 waitlist (browser) / 403 (API)
INTEGRATION POINT — wire in serve.go RIGHT AFTER SanitizeIdentity:
app.Use(IdentityMiddleware(cfg)) // establishes the validated principal
app.Use(featuregate.Enforce(featuregate.EnforceConfig{ WaitlistURL: … })) // ← here
It reads the sanitized X-User-Id / X-User-IsAdmin / X-User-Approved that IdentityMiddleware minted, so it MUST run after it and (like BillingGate) before the subsystem handlers. It is deliberately NOT wired here — the unified-binary agent owns serve.go's boot chain; this package exposes Enforce + the store so the one-line app.Use lands without a merge collision. The store is resolved lazily (moduleStore()) so Enforce can be constructed before Mount runs.
WHY NATIVE IS CANONICAL (in-cluster-bypass). The @file edge guard only gates traffic arriving THROUGH the ingress — a pod reaching another service's pod directly in-cluster bypasses it (a cluster-wide baseline NetworkPolicy + Cilium broad-allow union means additive netpols can't seal that). For a waitlist (threat model = external users) edge-only is acceptable, but this native middleware is FORWARD-PERFECT: when the app IS cloud, the gate is IN the request path, so reaching the pod directly STILL hits it — there is no edge to go around. That is the reason the native middleware is the canonical enforcement and the @file guard is purely interim. It is also STATELESS (it reads the sanitized X-User-* headers, sets no cookie), so the multi-apex cookie-domain concern the @file guard must handle does not exist here at all.
Paths that must NEVER be gated (health, the waitlist page's own API, auth callbacks) are skipped via ExemptPrefixes so enforcement can't lock the platform out of its own recovery/observability surface.
type SeedService ¶
type SeedService struct {
Service string
DisplayName string
Hosts []string
Description string
WaitlistMode bool // the launch default for a freshly-seeded service
}
SeedService is one row of the initial registry (the live hosted services). Seed is idempotent (INSERT OR IGNORE), so a restart NEVER overwrites an admin's live toggle — the seed only ever CREATES a missing row.
type Service ¶
type Service struct {
Service string `json:"service"`
DisplayName string `json:"displayName"`
Hosts []string `json:"hosts"`
WaitlistMode bool `json:"waitlistMode"`
Description string `json:"description"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
UpdatedBy string `json:"updatedBy"`
}
Service is one hosted Hanzo service in the launch-control registry — the ONE source of truth for whether that service is in waitlist mode. Hosts are the public hostnames the service answers on (the key the guard / native middleware look a request up by). WaitlistMode ON = gated (only APPROVED users past the waitlist); OFF = open to any signed-up user. UpdatedBy records the admin who last flipped the mode (audit trail on the toggle itself).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the feature-gate registry. ONE global SQLite file holds every hosted service's waitlist mode + its hostnames. It is a PLATFORM-WIDE config store (not per-tenant): the waitlist mode of hanzo.chat is one global value, toggled from admin.hanzo.ai, read by every enforcement point. Two tables, normalized:
feature_services(service PK, display_name, waitlist_mode, description, …) feature_hosts(host PK, service FK) -- host → service, the hot lookup index
func (*Store) List ¶
List returns every registered service (with its hosts), sorted by slug. This is the admin Services board.
func (*Store) ModeForHost ¶
func (s *Store) ModeForHost(ctx context.Context, host string) (mode bool, service string, known bool, err error)
ModeForHost is the HOT lookup the enforcement points call once per request: it resolves a request host to its service and that service's waitlist mode. `known` is false when the host is not in the registry (an UN-GOVERNED host — the native middleware passes it through; the guard, attached only to gated hosts, fails safe to gated). host is normalized here so the caller passes the raw Host.
func (*Store) Seed ¶
Seed inserts the initial registry idempotently (INSERT OR IGNORE on both tables), so a boot never clobbers a live admin toggle. Returns the number of services created (0 on a warm store).
func (*Store) SetMode ¶
func (s *Store) SetMode(ctx context.Context, service string, mode bool, by string, now int64) (Service, error)
SetMode flips one service's waitlist mode and stamps who/when. errNotFound if the slug is unknown. Returns the updated service.
func (*Store) Upsert ¶
Upsert creates or updates a service (display name, description, hosts, initial mode) so a new hosted service can be onboarded from admin.<brand> WITHOUT a redeploy. On an existing service it updates the metadata + REPLACES the host set but PRESERVES the live waitlist_mode (a re-register never silently re-gates an opened service); on a NEW service it sets the given mode. Hosts already claimed by ANOTHER service are skipped (first-claim wins — a host maps to one service).