flags: move the waitlist mode read /v1/featuregate/mode -> /v1/flags/waitlist (one namespace)

The guard's public waitlist-mode read now lives under /v1/flags (the flags engine
owns it) — there is NO /v1/featuregate HTTP endpoint. The route, the Enforce
exempt prefix, and the doc/prose comments move; the featuregate Go PACKAGE (native
Enforce middleware) is NOT renamed, and the /v1/admin/services board is unchanged.

- clients/flags/routes.go            GET /v1/featuregate/mode -> GET /v1/flags/waitlist
- clients/flags/waitlist.go          doc comments repointed
- clients/featuregate/middleware.go  defaultExemptPrefixes /v1/featuregate/ -> /v1/flags/waitlist
- clients/featuregate/middleware_test.go  exempt-path assertion updated
- apps/apps.go                       stale prose comment repointed

Verified green: go build ./clients/flags/... ./clients/featuregate/... ./apps/...,
go vet, and CGO_ENABLED=0 go test ./clients/featuregate/...
This commit is contained in:
zeekay
2026-07-17 10:57:25 -07:00
parent 2c77dd781e
commit 7317925fb7
5 changed files with 13 additions and 13 deletions
+1 -1
View File
@@ -339,7 +339,7 @@ func Wire() []cloud.MountSpec {
{Name: "admin", Mount: admin.Mount},
// Launch-control (per-service waitlist mode) folded into the flags engine: the
// mode IS the switch waitlist.<svc>, the board is the /v1/admin/services lens,
// and /v1/featuregate/mode is served by flags. featuregate is no longer a mounted
// and /v1/flags/waitlist is served by flags. featuregate is no longer a mounted
// subsystem — it exposes only the native Enforce middleware (wired in serve.go),
// a consumer of flags.WaitlistModeForHost.
// Tasks: the durable workflow/UI surface AND platform cron (durable schedules
+4 -4
View File
@@ -17,7 +17,7 @@
// from IAM). It is a CONSUMER of the ONE policy engine — the per-service waitlist MODE
// and the host→service registry live in clients/flags (a service's mode IS the
// switch waitlist.<svc>, evaluated through the native engine); the admin board is the
// /v1/admin/services lens and the guard's runtime mode read is /v1/featuregate/mode,
// /v1/admin/services lens and the guard's runtime mode read is /v1/flags/waitlist,
// both served there. This package owns only enforcement, decomplected into two axes:
//
// - PER-SERVICE waitlist mode on|off — the flags switch waitlist.<svc>,
@@ -112,9 +112,9 @@ type EnforceConfig struct {
// health, the auth/OIDC handshake, and the waitlist join API itself (so a gated
// user can still submit the waitlist form).
var defaultExemptPrefixes = []string{
"/v1/featuregate/", // the mode read + the health route
"/v1/iam/", // auth / OIDC / approval-status / get-account handshake
"/v1/waitlist", // the waitlist join API (a gated user must reach it)
"/v1/flags/waitlist", // the guard's public mode read (flags engine)
"/v1/iam/", // auth / OIDC / approval-status / get-account handshake
"/v1/waitlist", // the waitlist join API (a gated user must reach it)
"/health",
"/healthz",
"/__guard/", // the @file guard's own callback surface (defense in depth)
+1 -1
View File
@@ -186,7 +186,7 @@ func TestRule_UngovernedHost_PassesThrough(t *testing.T) {
func TestRule_ExemptPaths_NeverGated(t *testing.T) {
app := gateApp(t, "pending")
for _, p := range []string{"/health", "/v1/iam/get-account", "/v1/waitlist/join", "/v1/featuregate/mode"} {
for _, p := range []string{"/health", "/v1/iam/get-account", "/v1/waitlist/join", "/v1/flags/waitlist"} {
code, _ := drive(t, app, greq{host: "hanzo.chat", path: p, user: "u", org: "acme", accept: html})
if code != 200 {
t.Fatalf("exempt path %q = %d, want 200 (never gated)", p, code)
+4 -4
View File
@@ -25,10 +25,10 @@ func routes(app *zip.App, s *cloud.Service[state]) {
app.Put("/v1/flags/defs/:key", cloud.Handle(s, putDef))
app.Delete("/v1/flags/defs/:key", cloud.Handle(s, deleteDef))
app.Get("/v1/flags/activity", cloud.Handle(s, listActivity))
// Repointed from the former featuregate: the guard's public runtime mode read,
// now answered by the engine decide (host→service→waitlist.<svc>). Exempt from the
// Enforce gate (/v1/featuregate/ prefix), so a gated user can still resolve mode.
app.Get("/v1/featuregate/mode", cloud.Handle(s, waitlistModeRoute))
// The guard's public runtime mode read, answered by the engine decide
// (host→service→waitlist.<svc>). One namespace — it lives under /v1/flags. Exempt
// from the Enforce gate (/v1/flags/waitlist), so a gated user can still resolve mode.
app.Get("/v1/flags/waitlist", cloud.Handle(s, waitlistModeRoute))
}
// tenant resolves the org — the tenant-isolation KEY — from the validated
+3 -3
View File
@@ -11,7 +11,7 @@ package flags
//
// The decide is WaitlistModeForHost(host) → (mode, service, known): resolve host→svc,
// then read waitlist.<svc>. featuregate.Enforce is now a CONSUMER of this decide, and
// /v1/featuregate/mode + the /v1/admin/services board read it too. Per-user approval
// /v1/flags/waitlist + the /v1/admin/services board read it too. Per-user approval
// (pending|approved) stays IAM's (featuregate/approval.go) — the second, orthogonal
// axis, unchanged.
@@ -101,7 +101,7 @@ func requireRegistry() (*waitlistStore, error) {
return c.registry.For(platformOrg, platformProject)
}
// WaitlistModeForHost is THE decide the Enforce consumer, /v1/featuregate/mode, and
// WaitlistModeForHost is THE decide the Enforce consumer, /v1/flags/waitlist, and
// the admin board call: resolve host→service, then read the waitlist.<svc> switch
// through the engine. FAIL-OPEN by construction — an unmounted registry, a store
// error, or an un-governed host all return known=false, so a request is NEVER gated
@@ -224,7 +224,7 @@ func mountWaitlist(c *Client, brand string, log luxlog.Logger) {
}
}
// waitlistModeRoute answers GET /v1/featuregate/mode?host=<h> — the runtime lookup the
// waitlistModeRoute answers GET /v1/flags/waitlist?host=<h> — the runtime lookup the
// @file waitlist-guard caches. Public (in-cluster) read: it returns ONLY the boolean
// mode for the ONE queried host, never an enumeration. Same wire shape as the former
// featuregate route, so the interim guard ports 1:1.