control

package
v0.9.0 Latest Latest
Warning

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

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

Documentation

Overview

OIDC login for the control plane: GET /auth/oidc/start redirects to the configured provider's hosted login; GET /auth/oidc/callback exchanges the authorization code and mints the same scs session password login mints, so everything downstream of login is identical for both paths.

These are raw chi handlers, not huma operations: they speak browser redirects and cookies, not JSON.

The provider is generic OIDC (issuer discovery via sdk/oauth). Per-dance state + PKCE verifier travel in a short-lived HttpOnly SameSite=Lax cookie rather than the session: the callback arrives as a cross-site top-level navigation, which a SameSite=Strict session cookie does not accompany.

GET /catalog/graph — the whole catalog as a minimal graph for the admin model picker.

Returns providers, hosts, and models (with their host bindings) in one call so the UI can render and cross-link them without three separate list fetches + client-side flattening. Only the fields the picker and the policy model view actually read are included — identity (id/name/ displayName), the enabled flag, host icon path, the featured flag (from the catalog's `labels.featured`), per-model deprecation, capabilities, context window, the pointer (the upstream wire name the bare model name resolves to at request time), and the (hostId, adapter, enabled) bindings. Heavy spec (full snapshot list, pricing refs, modalities, …) is deliberately omitted.

Reads the in-memory snapshot via the same index as /catalog/resolve, so it returns exactly what the data plane can route to right now: disabled providers/hosts/models are already absent, and disabled bindings are pruned here. The picker only offers routable targets; full-list / detail views use the CRUD APIs (PG), which show disabled rows.

GET /config.json — the public, unauthenticated runtime config the embedded admin UI fetches once at boot, same-origin (it is served from the control listener, which is also where the SPA is served from). Modelled on the OIDC `/.well-known/openid-configuration` discovery pattern.

Why runtime, not build-time: relay-ui is built once into a generic tarball and go:embed'd into the binary, so VITE_* build-time env can't encode a specific deployment's URLs/flags. The UI learns them here at boot instead.

WORLD-READABLE — public values only. Never add secrets (keys, DSNs to private services, internal hostnames). The body is rendered once at registration from static process env, so it is effectively a constant for the process lifetime.

Package control is the admin-plane HTTP API: /auth/*, CRUD across the eight catalog kinds, /version, /master-key/*, /reload.

Mount(r, deps) wires huma+chi onto an existing chi router and returns the huma.API. main.go constructs Deps and calls Mount.

GET /debug/snapshot — render the current data-plane snapshot for operator inspection. The snapshot is what the hot path actually sees; diffing it against PG (via the regular admin list endpoints) is the fastest way to spot what sanitize dropped.

detail=counts (default): per-kind row counts + per-policy reverse-join

sizes. Tiny payload, scans nothing.

detail=full: every entity in stable slug order, sanitized

spec included. Bigger; admin-only, no SLO.

list_filter.go wires the pure pkg/filter engine into the generic CRUD list handler. huma binds typed structs, but the filter allowlist is per-resource dynamic, so a small middleware stashes the raw url.Values on the request context and the list resolver reads them back to feed filterSchema.Parse. See registerKind in crud.go and the per-resource schemas in list_schemas.go.

list_schemas.go declares the per-resource filter allowlists consumed by the generic list handler (registerKind). Each schema is the single source of truth for that resource's filterable/sortable params: the accessors are typed closures, so renaming an underlying spec field is a compile error here. The query-param name, the allowlist entry, and the match logic all derive from one Field literal.

Covers all eight catalog kinds. Time filters (created/updated, release/ deprecation dates) read Metadata timestamps + spec date strings; ?label= k=v works on every kind via the Labels hook. The host-key circuit-breaker state filter (?health=) is intentionally absent here — it needs a snapshot+kv join the pure store-slice engine can't express.

Logs read-side endpoints: the full per-request lifecycle view that drives the frontend Logs page. A "log" is what happened on a request (routing, status, timing, tokens, errors, identity) — i.e. the full usage.Event — with the captured request/response bodies attached when payload logging was opted in for that request ("(not logged)" otherwise).

GET /logs                full lifecycle records, newest first, filterable
GET /logs/{request_id}   one record + its captured bodies (if any)

Distinction from /usage: /usage is the narrow metrics projection (billing); /logs is the full record + optional body. Both read the same log event stream via usagelog.Reader; the body joins by request_id via payload.Reader.

overlay.go binds the catalog-overlay subresource:

GET    /models/by-id/{id}/overlay   patch + template + effective + quarantine state
PUT    /models/by-id/{id}/overlay   set/replace the user's sparse patch
DELETE /models/by-id/{id}/overlay   factory reset (template resumes verbatim)

The overlay is an explicitly user-managed patch document — PUT replaces the whole patch. Writes hit PG only; the table's NOTIFY trigger fans the merge out to every pod's snapshot (~1s), same as generic CRUD.

Policy model-grant normalization + enabled-resolution guard.

At policy create/update the catalog-ref strings in Spec.Models (and each RLBinding's Models) are:

  1. slugified to canonical form — operators may paste real-world names ("openai/GPT-4o", "anthropic/claude-3.5") and they're rewritten to the stored slug form ("openai/gpt-4o", "anthropic/claude-3-5"). This keeps PG, the data-plane snapshot, and the picker agreeing on one string.
  2. resolved against the catalog and required to match at least one *enabled* binding (model enabled, host enabled, binding enabled). Host-only "@host" refs only require the host to exist + be enabled — they grant every present and future binding on that host.

Refs that resolve to nothing enabled are rejected with 400. This is the cross-entity check the per-row policy.Validate() (grammar only, no catalog access) can't perform. Host-key / relay-key existence is deliberately NOT checked here — the inference path handles those at request time.

Attach / detach RelayKey ↔ Policy from the policy side. The authoritative field is RelayKey.Spec.PolicyID (1:N — one policy per key, many keys per policy); these endpoints mutate it so the policy form can manage its key roster without round-tripping through the relay-key form.

POST   /policies/by-id/{id}/relay-keys/{relayKeyId}   — attach
DELETE /policies/by-id/{id}/relay-keys/{relayKeyId}   — detach

Attach overwrites any existing PolicyID on the relay key (no confirmation): moving a key from policy A to policy B is the common case. Detach succeeds when the key currently points at this policy; mismatched detach returns 409 to surface the drift instead of silently no-op'ing.

GET /{kind}/by-id/{id}/references — generic reverse-ref lookup.

Returns every PG row that references the target entity. Used by the admin UI for blast-radius confirmation dialogs ("deleting this rate limit will affect N policies") and for inline "in use by" lists.

Walks the relevant stores per kind; admin-only, no SLO. Indices come later if scan time matters. Reading PG (not the catalog snapshot) so disabled / soft-dropped refs are still visible — they exist in PG even when the data plane has filtered them out.

Custom POST /relay-keys: generates the bearer plaintext server-side via relaykey.Generate, persists only the hash + prefix, and returns the plaintext exactly once on the create response. The generic CRUD POST in registerKind is skipped for this kind (skipCreate=true) so callers can't sneak a precomputed keyHash through.

POST /relay-keys/by-id/{id}/rotate — mint a fresh bearer plaintext for an existing relay-key, replacing KeyHash + Prefix in place. All other fields (policy binding, flags, slug) survive, so customers swap the secret without re-wiring anything. The old plaintext stops authenticating as soon as the snapshot picks up the NOTIFY (~1s fleet-wide). Like create, the new plaintext is returned exactly once.

subresources.go binds the catalogview read projections to chi/huma as resource-navigation endpoints — "API UX":

GET /models/{ref}/hosts     hosts serving this model (+ binding + pricing)
GET /models/{ref}/pricing   pricing per host for this model
GET /models/{ref}/policies  policies granting this model (+ per-model limits)
GET /hosts/{ref}/models      models this host serves (+ binding + pricing)
GET /hosts/{ref}/keys        upstream credentials this host owns (secret-free)
GET /hosts/{ref}/policies    host-tier serving policies this host owns
GET /policies/{ref}/models       models this policy grants (+ per-model limits)
GET /policies/{ref}/hosts        hosts this policy reaches (+ host-keys)
GET /policies/{ref}/rate-limits  rate-limit rule sets this policy references

All composition lives in app/catalogview (PG-backed, full state incl. disabled rows). These handlers are thin: build the Service from the stores, call the projection, return its rows. {ref} is a slug or UUID id.

Usage read-side endpoints: surface the JSONL stream the post-flight observer writes via filtered + aggregated queries. Backed by a usagelog.Reader, so the store can swap (file today, ClickHouse later) without touching this layer.

GET /usage/events    raw events, newest first, filterable
GET /usage/summary   per-group aggregates over the filtered set

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AdminTokenMiddleware

func AdminTokenMiddleware(adminToken string) func(http.Handler) http.Handler

AdminTokenMiddleware accepts the request iff it carries a valid bearer admin token in the Authorization header. On success it injects an Actor with AdminToken=true into context so downstream handlers + authz see a valid caller. On absent/mismatched token it does nothing — the next middleware in line (session) may still authenticate.

adminToken is the cleartext token from RELAY_ADMIN_TOKEN. Empty disables the bypass entirely.

func CORS

func CORS(allowedOrigins ...string) func(http.Handler) http.Handler

CORS returns a middleware that handles preflight (OPTIONS) and adds the headers needed for a credentialed browser fetch from one of the allowed origins. Required because the control API is consumed by a frontend on a separate hostname (e.g. https://relay.wyolet.dev).

allowedOrigins is matched exactly against the Origin header — wildcard origins are not allowed when Access-Control-Allow-Credentials is true, per the CORS spec.

func ConfigJSONHandler added in v0.4.1

func ConfigJSONHandler(rc RuntimeConfig, settingsSrc settings.Reader) http.HandlerFunc

ConfigJSONHandler serves GET /config.json at the control listener ROOT. The UI fetches ${origin}/config.json at boot — before it knows the /api prefix — so this must stay at root even though the rest of the control API mounts under /api. Static fields are marshalled per request because feature flags (auth:oidc) are settings-driven and may flip at runtime. Mirrors registerConfigJSON's body shaping.

func Mount

func Mount(r chi.Router, d Deps) huma.API

Mount installs the control-plane huma API on r and registers all operations. Returns the huma.API so the caller can attach test-only ops.

Middleware order on r (caller is responsible for wiring these onto the chi router before any operations are matched):

  1. session.Manager.Middleware — loads cookie session, sets Actor in ctx
  2. AdminTokenMiddleware — sets Actor in ctx when bearer matches

Per-operation Middlewares attached by Mount enforce RequireActor on every protected route. Public ops (/auth/login, /version) omit it.

func RequireActor

func RequireActor(next http.Handler) http.Handler

RequireActor blocks any request that has no Actor in context. Stacked after AdminTokenMiddleware and the session middleware so either auth path can satisfy it.

Types

type Deps

type Deps struct {
	// Identity is the YAML-backed user store used by /auth/login.
	Identity *identity.Store

	// Users is the DB-backed user store. Login consults it before the YAML
	// fallback; the OIDC callback provisions into it. nil disables both
	// (YAML-only deployments).
	Users *user.Store

	// Sessions is the cookie-backed session manager. Login/Logout write to
	// it; the session middleware (installed by Mount) reads from it.
	Sessions *session.Manager

	// AdminToken is the cleartext break-glass bearer. Empty disables the
	// bypass. Validated by AdminTokenMiddleware; not used directly by
	// handlers.
	AdminToken string

	// Authz is the policy-decision interface. Handlers call
	// d.Authz.Authorize before mutations; today's impl is permissive for
	// any authenticated caller.
	Authz authz.Authorizer

	// Catalog is the in-memory snapshot used for slug→id resolution on
	// reads. Writes go through Stores.
	Catalog *appcatalog.Catalog

	// Stores is the bundle of eight typed stores used by CRUD writes.
	Stores *appcatalog.Stores

	// CookieSecure controls the Secure attribute on the session cookie.
	// Surfaced here so the OpenAPI doc can reflect deployment posture.
	CookieSecure bool

	// UsageReader serves /usage/* read-side endpoints. nil disables
	// them — useful for deployments where usage events are consumed
	// from a separate store.
	UsageReader usagelog.Reader

	// PayloadReader serves /payloads/* read-side endpoints (the Logs view).
	// nil disables them — e.g. minimal builds or deployments where captured
	// bodies are consumed from a separate store.
	PayloadReader payloadlog.Reader

	// Selector is the keypool circuit-breaker owner, shared with the data
	// plane. The host-key health endpoint reads per-key breaker state through
	// it. nil disables that endpoint.
	Selector *keypool.Selector

	// HostHealth reads per-host runtime reachability (observed state), shared
	// with the data plane. Overlays host.Status on host reads. nil disables
	// the overlay (Status stays absent → UI shows "unknown").
	HostHealth HostHealthReader

	// RuntimeConfig is the public config served at GET /config.json for the
	// embedded admin UI. Zero value is fine (UI falls back to origin defaults).
	RuntimeConfig RuntimeConfig
}

Deps is the typed dependency bundle for the admin plane.

type HostHealthReader added in v0.2.0

type HostHealthReader interface {
	Read(ctx context.Context, hostID string) (host.Status, bool)
}

HostHealthReader reads observed host reachability. Implemented by app/hosthealth.Recorder.

type RuntimeConfig added in v0.2.0

type RuntimeConfig struct {
	ControlAPIURL   string          `json:"controlApiUrl,omitempty"`
	InferenceAPIURL string          `json:"inferenceApiUrl,omitempty"`
	Mode            string          `json:"mode,omitempty"` // "oss" | "cloud"
	Version         string          `json:"version,omitempty"`
	Features        map[string]bool `json:"features,omitempty"`
	Telemetry       *Telemetry      `json:"telemetry,omitempty"`
	DocsURL         string          `json:"docsUrl,omitempty"`
	SupportURL      string          `json:"supportUrl,omitempty"`
}

RuntimeConfig is the JSON shape the UI reads. Every field is optional and omitempty: an empty controlApiUrl tells the UI to use its own origin; an empty inferenceApiUrl has no safe origin default (UI/data-plane may differ), so the UI prompts rather than guessing. The object is additive — the UI reads keys it knows and ignores the rest.

type Telemetry added in v0.2.0

type Telemetry struct {
	SentryDSN   string `json:"sentryDsn,omitempty"`
	Environment string `json:"environment,omitempty"`
}

Telemetry carries PUBLIC client-side telemetry config only.

type UsageFilterInput

type UsageFilterInput struct {
	Since        string   `query:"since" doc:"Relative window (e.g. \"1h\", \"24h\", \"7d\"). Default \"1h\". Ignored when from is set."`
	From         string   `query:"from" doc:"Absolute lower bound (RFC3339). Overrides since."`
	To           string   `query:"to" doc:"Absolute upper bound (RFC3339)."`
	RequestID    string   `query:"request_id" doc:"Exact match on a single request id (deep-link one event)."`
	RelayKeyHash []string `query:"relay_key_hash" doc:"Match any of the given sha256 hashes of the inbound bearer."`
	PolicyID     []string `query:"policy_id" doc:"Match any of the given Policy.metadata.id values."`
	ModelID      []string `query:"model_id" doc:"Match any of the given Model.metadata.id values."`
	HostID       []string `query:"host_id" doc:"Match any of the given Host.metadata.id values."`
	Source       []string `query:"source" doc:"Match any of \"pipeline\" | \"proxy\" | \"ws\" | \"batch\"."`
	FinishReason []string `query:"finish_reason" doc:"Match any of \"stop\" | \"length\" | \"tool_calls\" | \"content_filter\" | \"refusal\"."`
	ErrorKind    []string `query:"error_kind" doc:"Match any of the given error_kind values."`
	StatusMin    int      `query:"status_min" doc:"Minimum HTTP status to include."`
	StatusMax    int      `query:"status_max" doc:"Maximum HTTP status to include."`

	HostKeyID      []string `query:"host_key_id" doc:"Match any of the given HostKey.metadata.id values."`
	RequestedModel []string `query:"requested_model" doc:"Match any of the model strings as the caller sent them."`
	Model          []string `query:"model" doc:"Match any of the given model slugs (event-time metadata.name, denormalized)."`
	Host           []string `query:"host" doc:"Match any of the given host slugs (event-time metadata.name, denormalized)."`
	Policy         []string `query:"policy" doc:"Match any of the given policy slugs (event-time metadata.name, denormalized)."`
	Provider       []string `query:"provider" doc:"Match any of the given provider slugs (event-time, denormalized)."`
	Status         []int    `query:"status" doc:"Match any of these exact HTTP status codes."`
	StatusClass    string   `query:"status_class" doc:"Convenience status band: \"2xx\" | \"4xx\" | \"5xx\". Sets status_min/max."`
	Streamed       string   `query:"streamed" enum:"true,false" doc:"true = only streamed responses, false = only non-streamed."`
	Error          string   `query:"error" enum:"true,false" doc:"true = only errors (status>=400 or error_kind set), false = only successes."`
	AttemptsMin    int      `query:"attempts_min" doc:"Minimum upstream try count (failover) — finds retried requests."`
	DurationMsMin  int64    `query:"duration_ms_min" doc:"Minimum total duration in ms (slow-request filter)."`
	DurationMsMax  int64    `query:"duration_ms_max" doc:"Maximum total duration in ms."`
	TTFTMsMin      int64    `query:"ttft_ms_min" doc:"Minimum upstream time-to-first-byte (ms); excludes requests with no upstream timing."`
	TTFTMsMax      int64    `query:"ttft_ms_max" doc:"Maximum upstream time-to-first-byte (ms)."`
	Q              string   `query:"q" doc:"Free-text substring across request_id, model_id, requested_model, source."`
	Tag            []string `` /* 142-byte string literal not displayed */
}

Jump to

Keyboard shortcuts

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