featuregate

package
v1.801.37 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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

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 Mount

func Mount(app *zip.App, deps cloud.Deps) error

Mount wires the feature-gate surface onto app per HIP-0106.

func NormalizeHost

func NormalizeHost(host string) string

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.

func Shutdown

func Shutdown() error

Shutdown closes the featuregate store. Idempotent.

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:

  1. global admin (c.IsAdmin()) → approved (admins are never gated)
  2. validated header X-User-Approved → its bit (forward-perfect, no lookup)
  3. IAM get-account (caller's creds) → approved unless approvalStatus=="pending" — cached per user for ttl; FAIL-OPEN on any IAM error.

func NewApprovals

func NewApprovals(iamBase string, ttl time.Duration) *Approvals

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).

func (*Approvals) Approved

func (a *Approvals) Approved(c *zip.Ctx) bool

Approved reports whether the caller is off the waitlist.

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) Close

func (s *Store) Close() error

Close closes the underlying database. Idempotent-safe via sql.DB.

func (*Store) Get

func (s *Store) Get(ctx context.Context, service string) (Service, error)

Get returns one service by slug, or errNotFound.

func (*Store) List

func (s *Store) List(ctx context.Context) ([]Service, error)

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

func (s *Store) Seed(ctx context.Context, rows []SeedService, now int64) (int, error)

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

func (s *Store) Upsert(ctx context.Context, in Service, by string, now int64) (Service, error)

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL