governance

package
v1.15.0 Latest Latest
Warning

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

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

Documentation

Overview

Package governance is Harbor's policy middleware between the runtime and the LLM-edge chain. It owns identity-scoped enforcement of cost ceilings, per-call MaxTokens, and rate limits. The package composes OUTSIDE the retry wrapper — for the full chain order:

governance(retry(downgrade(corrections(safety(driver)))))

Governance ships LATENT at V1 (per the V1 scoping): the interface + math + events + persistence all ship and wire, but every enforcement path is operator-opt-in. With zero `Governance.IdentityTiers` configured (the loader's default), every `PreCall` permits and every `PostCall` is an accumulator update only — no ceilings fire, no events emit beyond the accumulator's own bookkeeping.

Concurrent reuse: one Subsystem instance is safe to share across N concurrent goroutines. Per-key state lives in a `sync.Map` of `*identityState`; each `identityState` uses atomic primitives for its counters (lock-free CAS for monetary float64 sums; atomic for integer counts). Per-run scope flows via `ctx`; no mutable state on the Subsystem struct itself crosses run boundaries beyond the keyed per-identity caches.

Package governance — registry.

Governance composes OUTSIDE the rest of the LLM-edge chain via the `llm.RegisterGovernanceWrapper` hook. The wrapper is INSTALLED here (from `init()`), but only FIRES when a registered factory has been supplied via `SetFactory`. The factory takes a `llm.ConfigSnapshot` + `llm.Deps` and returns the per-`llm.Open` Subsystem (or nil to leave governance latent for this caller).

Why a factory instead of a global singleton: each `llm.Open` may run with different `Config` (different tier maps, different StateStore for tests). the accumulator is a long-lived sibling of the LLMClient — operators build one per Open and dispose with the client.

The factory is installed by the production assembly (`internal/runtime/assemble.Assemble`,) whenever the operator config declares identity tiers, and cleared when it declares none. If unset when `llm.Open` runs, the governance hook is a no-op pass-through — preserving the latent default for callers (especially test code) that don't wire governance explicitly.

Index

Constants

View Source
const (
	// EventTypeBudgetExceeded — emitted from `CostAccumulator.PreCall`
	// when the per-identity total cost meets or exceeds the configured
	// tier ceiling.
	EventTypeBudgetExceeded events.EventType = "governance.budget_exceeded"

	// EventTypeRateLimited — emitted from `RateLimiter.PreCall` when
	// the token bucket for the request's (identity, model) underflows
	// the requested drain.
	EventTypeRateLimited events.EventType = "governance.rate_limited"

	// EventTypeMaxTokensExceeded — emitted from
	// `MaxTokensEnforcer.PreCall` when `req.MaxTokens` exceeds the
	// resolved tier's cap.
	EventTypeMaxTokensExceeded events.EventType = "governance.maxtokens_exceeded"

	// EventTypePostureReadAdmin — Emitted when an
	// admin-scoped caller reads ANOTHER tenant's governance posture via
	// the `governance.posture` Protocol method. A caller reading its
	// OWN tenant does NOT emit this event (matches the
	// sessions.inspect convention — own-scope reads are not audited).
	// The cross-tenant read is a privileged action and lands on the
	// audit trail per CLAUDE.md §7 + RFC §6.15.
	EventTypePostureReadAdmin events.EventType = "governance.posture_read_admin"

	// EventTypeTenantOverridesSet — Emitted when an admin-scoped caller
	// sets (or clears) a tenant's default LLM-parameter overrides via the
	// `governance.set_tenant_overrides` Protocol method. The change is a
	// privileged, no-deploy reconfiguration of the tenant's default
	// model / temperature / max-tokens / reasoning-effort / extra
	// instructions and lands on the audit trail (CLAUDE.md §7, RFC §6.15
	// — the `ModelOverride` governance seam). The payload carries only
	// which dimensions were set (booleans) plus the model name — never
	// the extra-instructions text (operator-supplied prose runs through
	// the redactor and is not echoed onto the audit event).
	EventTypeTenantOverridesSet events.EventType = "governance.tenant_overrides_set"

	// EventTypeKeyRotated — Emitted when an admin-scoped caller rotates an
	// LLM provider API key via the `governance.rotate_key` Protocol
	// method. The change is a privileged, no-deploy credential swap and
	// lands on the audit trail (CLAUDE.md §7, RFC §6.15 — the key-rotation
	// seam). The payload carries the provider + a NON-REVERSIBLE
	// fingerprint of the new key + actor + timestamp — NEVER the key value
	// (CLAUDE.md §7: secrets are never logged or persisted to the bus).
	EventTypeKeyRotated events.EventType = "governance.key_rotated"
)

governance event types. Registered via init() so the canonical events registry stays the single source of truth (AGENTS.md §17.6's "wiring gap" lesson — register at declaration time, publish at use time).

All payloads are SafePayload (compose events.SafeSealed): identity, model, ceilings, and totals are operator-visible metadata, not secret-shaped. Cost values are USD floats — same wire shape as `llm.CostRecordedPayload`.

Variables

View Source
var (
	// ErrBudgetExceeded — PreCall blocked because the
	// (identity, tier) accumulator hit or exceeded the configured
	// BudgetCeilingUSD. Wraps with the identity + total + ceiling so
	// log handlers can render the failure deterministically.
	ErrBudgetExceeded = errors.New("governance: budget ceiling exceeded")

	// ErrRateLimited — PreCall blocked because the per-
	// (identity, model) token bucket underflowed the requested drain.
	ErrRateLimited = errors.New("governance: rate-limited")

	// ErrMaxTokensExceeded — PreCall blocked because the
	// request's MaxTokens exceeded the identity tier's cap.
	ErrMaxTokensExceeded = errors.New("governance: per-call MaxTokens exceeded")

	// ErrIdentityRequired — the request reached governance with a
	// missing or incomplete identity. AGENTS.md §6 rule 9 mandates a
	// fail-closed; governance enforces here.
	ErrIdentityRequired = errors.New("governance: identity required")

	// ErrStateUnavailable — a StateStore read failed during PreCall.
	// AGENTS.md §13 forbids silent permits on read failure; the
	// wrapper returns this error rather than fall through to a
	// "permit by default."
	ErrStateUnavailable = errors.New("governance: state store unavailable")

	// ErrClosed — the Subsystem (or its underlying StateStore) was
	// closed. Subsequent calls fail loud.
	ErrClosed = errors.New("governance: subsystem closed")

	// ErrInvalidConfig — operator config did not validate at
	// construction. Used by `NewCostAccumulator` / `NewRateLimiter` /
	// `NewMaxTokensEnforcer`.
	ErrInvalidConfig = errors.New("governance: invalid config")

	// ErrUnknownModel — an admin tried to set a tenant default model
	// that has no configured `ModelProfile`. Validation happens at set
	// time (fail loud) rather than deferring the failure to call time.
	ErrUnknownModel = errors.New("governance: unknown model (no configured ModelProfile)")

	// ErrInvalidOverride — a tenant override carried a structurally
	// invalid value (a temperature outside [0,2], a non-positive
	// max-tokens, an unknown reasoning-effort). Fails closed rather than
	// silently passing the value to the provider.
	ErrInvalidOverride = errors.New("governance: invalid tenant override")
)

Sentinel errors. Callers compare via errors.Is.

Each sentinel maps 1:1 to a `governance.*` event type — code that catches one of these knows which policy fired and can route the outcome (e.g. ErrBudgetExceeded → pause/resume primitive in +).

Functions

func CacheLen added in v1.5.1

func CacheLen(s Subsystem) int

CacheLen returns the total identity-scoped governance cache size for s — the sum across any enforcer that holds an identity-keyed cache (cost ceilings + rate-limit buckets). A Subsystem with no such cache (or a nil Subsystem — the latent no-enforcement default) returns 0. The runtime observability wiring calls this to source the harbor_runtime_governance_cache_entries gauge.

func ClearFactory

func ClearFactory()

ClearFactory removes the registered factory. Restores the latent default behaviour. Concurrent-safe.

func ErrIs

func ErrIs(err, target error) bool

ErrIs reports whether `err` wraps `target` and one of governance's sentinels. Useful in callers that branch on policy outcome (e.g. the runtime can route an ErrBudgetExceeded to the unified pause/resume primitive while letting other errors propagate).

Returns false on nil err.

func SetFactory

func SetFactory(f Factory)

SetFactory installs the per-`llm.Open` factory. Calling SetFactory twice is allowed (the second call wins) — the production assembly (`assemble.Assemble`, the seam's first production caller) calls this once at boot when `Config.IdentityTiers` is non-empty; tests may swap it repeatedly. Concurrent-safe.

Multi-runtime limitation: the factory is PROCESS-GLOBAL. An embedder assembling two runtime stacks with different tier maps in one process would collide here — the second SetFactory wins for every subsequent `llm.Open`, and the same wipe exists on the CLOSE side: a tier-bearing stack's `Stack.Close` runs `ClearFactory()` unconditionally, so closing an older stack removes a newer sibling stack's installed factory for any `llm.Open` issued afterwards (already-wrapped clients keep their enforcement). The binary assembles exactly one stack, so the zero-ceremony seam stays; multi-runtime embedders skip SetFactory entirely and compose per stack instead:

sub, err := governance.NewSubsystemFromConfig(cfg, store, bus)
client = governance.Wrap(llmClient, sub) // governance outermost

See `Wrap` and `NewSubsystemFromConfig` for the documented headless path.

func Wrap

func Wrap(inner llm.LLMClient, sub Subsystem) llm.LLMClient

Wrap composes a `Subsystem` around an `llm.LLMClient`. The returned client invokes `sub.PreCall` then the inner client then `sub.PostCall` on every Complete. PreCall non-nil short-circuits and propagates the error directly to the caller; the inner client is NOT invoked.

PostCall's return is observability-only — a non-nil PostCall error is logged at Warn but does NOT replace the original `(resp, callErr)` outcome. This is RFC §6.15 line 1128's "PostCall errors do not supplant the call's result" behaviour.

Compose order: governance is the OUTERMOST wrapper, sitting outside the retry wrapper. The wrapper closures here read identity from ctx via `identityFromCtx`; missing identity → fail-closed with `ErrIdentityRequired`.

Headless / multi-runtime path: Wrap + `NewSubsystemFromConfig` are the documented composition for embedders that run N runtime stacks (each with its own tier map) in one process — the process-global `SetFactory` seam would collide there (second SetFactory wins; see its godoc). Build one Subsystem per stack and wrap each `llm.Open` result directly:

sub, err := governance.NewSubsystemFromConfig(cfg, store, bus)
client = governance.Wrap(client, sub)

Concurrent reuse: the returned client is a thin functional adapter (no mutable state of its own); concurrent reuse depends on the inner client + Subsystem both honouring the contract.

Types

type BudgetExceededPayload

type BudgetExceededPayload struct {
	events.SafeSealed
	Identity   identity.Quadruple
	Tier       string
	Model      string
	TotalCost  float64
	Ceiling    float64
	Currency   string
	OccurredAt time.Time
}

BudgetExceededPayload is the typed payload for EventTypeBudgetExceeded. SafePayload — identity + cost figures + tier name are operator-visible.

`TotalCost` is the per-identity sum at the time of the block (i.e. the pre-call running total; the rejected call did not contribute). `Ceiling` is the operator-configured cap at the time of the emit. `Model` carries the request's model identifier; `Tier` is the resolved tier name.

type Clock

type Clock interface {
	Now() time.Time
}

Clock is governance's time source. The standard `time` package implements this implicitly via the default `realClock` value; tests inject a fake clock with controllable Now().

var DefaultClock Clock = RealClock{}

DefaultClock is the production clock. Used when `Config.Clock` is nil.

type Config

type Config struct {
	// DefaultTier is the tier name applied to an identity that does
	// not match a custom Resolver mapping. Empty = no default tier =
	// no enforcement for unmatched identities (latent).
	DefaultTier string

	// IdentityTiers maps tier name → policy shape. A nil / empty map
	// disables all enforcement (latent default).
	IdentityTiers map[string]TierConfig

	// Resolver maps identity → tier name. nil = "use DefaultTier for
	// every identity." Operators with claim-based or session-attribute-
	// based tier assignment wire their resolver here.
	Resolver TierResolver

	// Clock is the time source for bucket-refill math. nil =
	// `time.Now` (the production default). Tests inject a fake clock.
	Clock Clock
}

Config is the operator-supplied governance shape. Zero-value = latent (no enforcement). Operators populate `IdentityTiers` + `DefaultTier` (or a custom `Resolver`) to switch on per-policy enforcement.

func ConfigFromOperator added in v1.3.0

func ConfigFromOperator(in config.GovernanceConfig) Config

ConfigFromOperator projects the operator-supplied `config.GovernanceConfig` onto the `governance.Config` shape. Only the declarative policy fields are projected — `DefaultTier` + `IdentityTiers` (each tier's `BudgetCeilingUSD` / `RateLimit` / `MaxTokens`). The runtime-injection fields (`Resolver` / `Clock`) stay zero by design: they are Go-level seams (a custom TierResolver, a test clock) with no YAML surface; callers wire them after projection when needed. The read-only posture surface consumes the projection as-is (a nil Resolver is correct — every caller's `ResolvedTier` falls back to `DefaultTier`).

`config.GovernanceConfig.RepairAttempts` is NOT part of this projection: it is the LLM repair-loop knob (consumed by the retry surface), not a tier-policy field — `governance.Config` carries no equivalent. The field-parity test names this exclusion.

An empty `IdentityTiers` in the YAML (the latent default) yields a `governance.Config` with an empty tier map — the posture surface reports it verbatim and enforcement stays latent.

type CostAccumulator

type CostAccumulator struct {
	// contains filtered or unexported fields
}

CostAccumulator is the `Subsystem` that aggregates LLM cost per `(tenant, user, session, model)` and enforces per-tier ceilings in PreCall. State persists to a `state.StateStore` so the accumulator survives runtime restart; three V1 drivers (in-mem / SQLite / Postgres) pass identical conformance tests.

Latent default: an empty `Config.IdentityTiers` map disables every enforcement path. PreCall returns nil; PostCall still records cost so the accumulator's observability surface stays alive (operators get per-identity cost dashboards without opting into enforcement). This satisfies the V1 scoping: "interface + math only, ceilings opt-in."

Concurrent reuse: the accumulator's per-key state lives in a `sync.Map` of `*costKeyState`; each `costKeyState` uses an atomic `uint64`-packed float64 for the cumulative total. The CAS loop in `add` is lock-free and N concurrent PostCalls cannot lose updates. PreCall reads the atomic snapshot — the race window between PreCall (read) and the next call's PostCall (write) is bounded by `in_flight × per_call_cost`; this is documented in the phase plan.

func NewCostAccumulator

func NewCostAccumulator(state state.StateStore, bus events.EventBus, cfg Config) (*CostAccumulator, error)

NewCostAccumulator constructs a `CostAccumulator`. `state` is the persistence floor (mandatory); `bus` is the observability bus (mandatory); `cfg` carries operator-supplied tier ceilings.

Validation: nil state or nil bus → `ErrInvalidConfig`. Config itself may have an empty IdentityTiers map — that's the latent default.

func (*CostAccumulator) CacheLen added in v1.5.1

func (a *CostAccumulator) CacheLen() int

CacheLen returns the number of distinct identity-scoped cache keys the accumulator currently holds. Identity-scoped keying (RFC §6.15) means this counts identities, not runs, so the value is bounded by the active-identity set — a safe, low-cardinality reading for the runtime governance-cache gauge. It is a lock-safe Range count over the internally-synchronised sync.Map; no per-run state is read.

func (*CostAccumulator) Close

func (a *CostAccumulator) Close(_ context.Context) error

Close marks the accumulator closed; subsequent PreCall / PostCall / Snapshot calls fail loud with `ErrClosed`. Idempotent.

func (*CostAccumulator) PostCall

PostCall accumulates `resp.Cost.TotalCost` plus any intermediate provider-attempt spend the retry / downgrade wrappers reported into the per-call attempt-cost tap, regardless of `callErr`. Failures still incur whatever cost the provider reported (some report 0 on failure; that's fine — the accumulator records 0), and attempt spend accumulates even when the outer call ultimately errors (retry / downgrade exhaustion — spend is spend).

The accumulator path is in-band synchronous (RFC §6.15 line 1128) rather than event-subscriber: the next PreCall sees the latest total without a bus-delivery race. The attempt-cost tap extends that in-band posture one level deeper — intermediate attempts are folded here, not re-accumulated off a bus subscription.

`governance.budget_exceeded` events are NOT emitted from PostCall; they fire only from PreCall on the NEXT call that exceeds. A PostCall that pushes the accumulator over the ceiling is accepted (the call already happened); the operator sees the breach via the cost-recorded observability stream that bifrost already publishes.

func (*CostAccumulator) PreCall

func (a *CostAccumulator) PreCall(ctx context.Context, req llm.CompleteRequest) error

PreCall checks the cost ceiling for the request's identity. With no ceiling configured (latent default) the function returns nil unconditionally. On state-read failure → wrapped `ErrStateUnavailable` (no silent permit per AGENTS.md §13).

func (*CostAccumulator) Snapshot

Snapshot returns the current per-(identity, model) accumulator total + the identity-level grand total. Used by tests and by the Console-driven inspection (post-V1). Concurrent-safe.

type Factory

type Factory func(cfg llm.ConfigSnapshot, deps llm.Deps) (Subsystem, error)

Factory builds a `Subsystem` from the same `(cfg, deps)` pair that `llm.Open` consumes. Returning a nil Subsystem disables governance for this Open invocation (the wrapper passes through unchanged) — useful for tests that share a single `cmd/harbor` blank-import with production code but don't want governance to fire.

type KeyRotatedPayload added in v1.5.0

type KeyRotatedPayload struct {
	events.SafeSealed
	// Actor is the admin-scoped caller that performed the rotation.
	Actor identity.Quadruple
	// Provider is the provider whose key was rotated.
	Provider string
	// Fingerprint is the non-reversible digest of the NEW key
	// (`sha256:<prefix>`). Never the key itself.
	Fingerprint string
	// OccurredAt is the wall-clock time of the rotation.
	OccurredAt time.Time
}

KeyRotatedPayload is the typed payload for EventTypeKeyRotated. SafePayload — the actor identity, the provider, and the NON-REVERSIBLE key fingerprint are operator-visible audit metadata. The key VALUE is NEVER carried here (CLAUDE.md §7: secrets never reach the bus); only the `sha256:<prefix>` fingerprint is, so an operator can correlate a rotation without the secret ever leaving the runtime's key holder. The payload still runs through the audit Redactor before publish.

type MaxTokensEnforcer

type MaxTokensEnforcer struct {
	// contains filtered or unexported fields
}

MaxTokensEnforcer is the Subsystem that gates `req.MaxTokens` against the resolved identity tier's cap. Stateless — no StateStore dependency. Fail-loud per master plan line 420 + RFC §6.15 line 1122.

Latent default: with `TierConfig.MaxTokens == 0` for the resolved tier, PreCall is a permit no-op. Same with an unresolved tier.

Concurrency: the enforcer holds no mutable state beyond `Config`; the concurrent-reuse contract is trivially satisfied. Operators may share one enforcer across the entire runtime.

func NewMaxTokensEnforcer

func NewMaxTokensEnforcer(bus events.EventBus, cfg Config) *MaxTokensEnforcer

NewMaxTokensEnforcer constructs the enforcer. The bus is required for `governance.maxtokens_exceeded` emit. Nil bus rejected.

func (*MaxTokensEnforcer) Close

func (e *MaxTokensEnforcer) Close() error

Close marks the enforcer closed.

func (*MaxTokensEnforcer) PostCall

PostCall is a no-op. MaxTokens enforcement is a pre-call gate only.

func (*MaxTokensEnforcer) PreCall

PreCall checks `req.MaxTokens` against the tier cap. Permits if either is unset / zero.

type MaxTokensExceededPayload

type MaxTokensExceededPayload struct {
	events.SafeSealed
	Identity   identity.Quadruple
	Tier       string
	Model      string
	Requested  int
	Cap        int
	OccurredAt time.Time
}

MaxTokensExceededPayload is the typed payload for EventTypeMaxTokensExceeded. `Requested` is the value on `CompleteRequest.MaxTokens`; `Cap` is the tier's configured ceiling.

type PostureProvider

type PostureProvider struct {
	// contains filtered or unexported fields
}

PostureProvider is the read-only accessor over a configured `governance.Config`. Built once per Runtime process via NewPostureProvider; `Posture` is safe for concurrent use by N goroutines.

func NewPostureProvider

func NewPostureProvider(cfg Config) *PostureProvider

NewPostureProvider builds a PostureProvider over the operator-supplied governance configuration. The Config is copied by value at construction; the provider holds its own immutable copy. A latent (zero-value) Config is valid — `Posture` returns an empty `IdentityTiers` map and empty tier selectors, which the Console renders as the explicit "No tiers configured" state.

func (*PostureProvider) Posture

func (p *PostureProvider) Posture(ctx context.Context) (Snapshot, error)

Posture returns a deep-copied Snapshot of the configured governance posture for the caller's identity. Identity is mandatory (CLAUDE.md §6 rule 9, RFC §5.5): a missing / incomplete identity in `ctx` fails loudly with wrapped `ErrIdentityRequired` — there is no opt-out.

The returned `IdentityTiers` map is a fresh deep copy; the `ResolvedTier` is computed by applying the Config's `TierResolver` (falling back to `DefaultTier`) to the caller's identity. A nil resolver makes `ResolvedTier == DefaultTier` for every caller.

type PostureReadAdminPayload

type PostureReadAdminPayload struct {
	events.SafeSealed
	// Actor is the identity of the admin-scoped caller that performed
	// the cross-tenant read.
	Actor identity.Quadruple
	// RequestedTenant is the tenant_id the caller asked to read — a
	// tenant other than the caller's own.
	RequestedTenant string
	// OccurredAt is the wall-clock time of the cross-tenant read.
	OccurredAt time.Time
}

PostureReadAdminPayload is the typed payload for EventTypePostureReadAdmin. SafePayload — the actor's identity and the requested tenant are operator-visible audit metadata, not secret-shaped. The payload still runs through the audit Redactor before the bus publish (CLAUDE.md §7 rule 6).

type RateLimitConfig

type RateLimitConfig struct {
	Capacity       int
	RefillTokens   int
	RefillInterval time.Duration
}

RateLimitConfig is the token-bucket shape. Capacity is the bucket ceiling (max tokens reservable). RefillTokens fill the bucket every RefillInterval. A zero Capacity disables the rate limit even if other fields are set.

func (RateLimitConfig) IsEnabled

func (r RateLimitConfig) IsEnabled() bool

IsEnabled reports whether the RateLimitConfig has the minimum fields set to drive enforcement (Capacity > 0; refill knobs may be zero for non-refilling buckets, but Capacity is the gate).

func (RateLimitConfig) IsZero

func (r RateLimitConfig) IsZero() bool

IsZero reports whether the RateLimitConfig is fully zero-valued (no rate-limit enforcement configured).

type RateLimitedPayload

type RateLimitedPayload struct {
	events.SafeSealed
	Identity     identity.Quadruple
	Tier         string
	Model        string
	Requested    int
	Available    int
	Capacity     int
	RefillTokens int
	RefillEvery  time.Duration
	OccurredAt   time.Time
}

RateLimitedPayload is the typed payload for EventTypeRateLimited. `Requested` is the number of tokens the drain attempted to remove; `Available` is the bucket's level at the moment of the block.

type RateLimiter

type RateLimiter struct {
	// contains filtered or unexported fields
}

RateLimiter is the `Subsystem` implementing a token bucket per `(identity, model)`. Bucket state lives in `state.StateStore` so it survives restart; latent default: zero `TierConfig.RateLimit` → no enforcement.

Time math (Clock):

  • `Capacity` is the bucket ceiling.
  • `RefillTokens` are added every `RefillInterval` (continuous accrual via `floor(elapsed / RefillInterval) * RefillTokens`).
  • `expected_tokens` per call defaults to `req.MaxTokens` if set, else 1. consciously does NOT pre-charge `MaxTokens` for unbounded requests — the operator who needs that sets a tier `MaxTokens` cap (the other policy).

Concurrency model: per-key state is a `sync.Map`; each `bucketKeyState` has its own mutex for drain serialisation. The fan-out at scale is per-(identity, model), which is naturally sharded — N concurrent calls on different keys never contend.

func NewRateLimiter

func NewRateLimiter(s state.StateStore, bus events.EventBus, cfg Config) (*RateLimiter, error)

NewRateLimiter constructs a RateLimiter. Validates deps.

func (*RateLimiter) CacheLen added in v1.5.1

func (r *RateLimiter) CacheLen() int

CacheLen returns the number of distinct identity-scoped cache keys the limiter currently holds. Identity-scoped keying (RFC §6.15) means this counts identities, not runs, so the value is bounded by the active-identity set — a safe, low-cardinality reading for the runtime governance-cache gauge. It is a lock-safe Range count over the internally-synchronised sync.Map; no per-run state is read.

func (*RateLimiter) Close

func (r *RateLimiter) Close(_ context.Context) error

Close marks the limiter closed; subsequent calls fail loud.

func (*RateLimiter) PostCall

PostCall is a no-op for the rate limiter — drain happens entirely in PreCall. Per RFC §6.15 simplicity, there are no refunds on call failure.

func (*RateLimiter) PreCall

func (r *RateLimiter) PreCall(ctx context.Context, req llm.CompleteRequest) error

PreCall drains the per-(identity, model) bucket by `expected_tokens`. Underflow → wrapped ErrRateLimited + governance.rate_limited event. Latent: a tier without RateLimit returns nil immediately.

func (*RateLimiter) Snapshot

func (r *RateLimiter) Snapshot(ctx context.Context, q identity.Quadruple) (map[string]int, error)

Snapshot reports the per-model bucket levels for the identity. Used by tests and inspection paths.

type RealClock

type RealClock struct{}

RealClock wraps `time.Now` and is the production default. Exported so callers that construct a Config with an explicit Clock for one dimension can keep the default for the rest.

func (RealClock) Now

func (RealClock) Now() time.Time

Now returns the current wall-clock time.

type Snapshot

type Snapshot struct {
	// DefaultTier is the operator-configured default tier name. Empty
	// when no default is configured.
	DefaultTier string
	// ResolvedTier is the tier the caller's identity resolves to via the
	// configured TierResolver (or DefaultTier when no resolver is set).
	// Empty when no tier resolves.
	ResolvedTier string
	// IdentityTiers is a deep copy of the configured tier map. Always
	// non-nil — an empty map signals no enforcement (latent default).
	IdentityTiers map[string]TierConfig
}

Snapshot is a deep-copied, immutable view of the configured governance posture: the `IdentityTiers` map, the `DefaultTier` selector, and the tier the caller's identity resolves to. Mutating any field of a returned Snapshot (including the `IdentityTiers` map) does NOT mutate the provider's underlying Config — the snapshot is a defensive deep copy.

type Subsystem

type Subsystem interface {
	PreCall(ctx context.Context, req llm.CompleteRequest) error
	PostCall(ctx context.Context, req llm.CompleteRequest, resp llm.CompleteResponse, callErr error) error
}

Subsystem is governance's enforcement seam — the interface `governance.Wrap` consumes to gate / observe an LLM call. Implementations MUST be safe for N concurrent invocations against a single shared instance.

`PreCall` is invoked before the wrapped `LLMClient.Complete`. A non-nil return short-circuits the call: the wrapper returns the error directly and does NOT invoke the inner client. Implementations emit the corresponding `governance.*` event from PreCall on rejection.

`PostCall` is invoked after the wrapped `LLMClient.Complete` returns, whether or not `callErr` is nil. It accumulates cost / token / latency state from `resp` and emits any observability events. A non-nil return from PostCall is an observability signal — it is logged at Warn level by the wrapper but does NOT replace the original call's `(resp, callErr)` outcome on the way back to the caller.

func NewCompound

func NewCompound(subs ...Subsystem) Subsystem

NewCompound bundles N Subsystems into a single Subsystem that fans PreCall + PostCall across all members. PreCall short-circuits on the first member that returns a non-nil error; subsequent members are NOT invoked. PostCall runs every member regardless of any individual failure (observability semantics) and joins their errors via `errors.Join` for the wrapper to log.

The fan-out order is the slice order — operators put cheap checks (MaxTokens) before expensive ones (cost-ceiling state lookup) to keep the rejection path fast. The binary order is:

  1. MaxTokensEnforcer — purely in-memory cap check; cheapest reject.
  2. RateLimiter — bucket drain (lock + small state lookup).
  3. CostAccumulator — accumulator lookup (state I/O).

Concurrent reuse: NewCompound's returned Subsystem is a thin adapter; concurrent reuse depends on each member honouring.

func NewSubsystemFromConfig added in v1.3.0

func NewSubsystemFromConfig(cfg Config, store state.StateStore, bus events.EventBus) (Subsystem, error)

NewSubsystemFromConfig composes the V1 enforcement Subsystem from an operator-shaped `Config`. The members fan out in the documented cheapest-reject-first order (see `NewCompound`'s godoc):

  1. MaxTokensEnforcer — purely in-memory cap check; cheapest reject.
  2. RateLimiter — bucket drain (lock + small state lookup).
  3. CostAccumulator — accumulator lookup (state I/O).

Latent default: an empty `cfg.IdentityTiers` map returns `(nil, nil)` — the ONE sanctioned "no enforcement" state. The `llm.Open` wrapper hook treats a nil Subsystem as pass-through, and the posture surface reports the empty map verbatim, so the latency is visible, never silent.

Fail-loud: a non-empty tier map with a nil store or nil bus returns a wrapped `ErrInvalidConfig` — enforcement without persistence or observability is not a degraded mode, it is a misconfiguration (CLAUDE.md §13, no silent degradation).

Lifecycle: the returned Subsystem owns no goroutines and no persistent handles beyond the caller-owned store/bus (each member's Close only flips a closed flag), so it rides the caller's lifecycle — the production assembly keeps it for the stack's lifetime; a headless embedder disposes it with the wrapped client.

Concurrent reuse: one returned Subsystem is safe to share across N concurrent goroutines — each member carries its own guarantee (atomics + per-key mutexes; per-run scope flows via ctx).

type TenantOverridePolicy added in v1.5.0

type TenantOverridePolicy struct {
	// contains filtered or unexported fields
}

TenantOverridePolicy realises the RFC §6.15 `ModelOverride` governance seam: an admin-set, identity-scoped, StateStore-backed default for a tenant's LLM parameters that takes effect on every session's NEXT run with no redeploy. It is NOT a `Subsystem` (it does not gate per-call in PreCall — a per-call read would change the model mid-run, violating the next-turn-only invariant); instead the run loop reads it ONCE at run start and pins the snapshot into the run's `RunContext`.

Concurrent reuse: the policy is immutable after construction except for its per-tenant cache (a `sync.Map` of `*tenantOverrideEntry`, each guarded by its own load mutex) and the `closed` atomic flag. Set / Get are safe for N concurrent goroutines; per-call state lives in arguments and locals, never on the policy struct.

Freshness scope: the cache is write-through on Set and lazy-loaded once per tenant, with no TTL — the same per-process cache model the cost accumulator uses. Within ONE runtime process (the writer and the run loop share the same instance) a Set is immediately visible to the next run, so the "every session's next run" guarantee holds. Across multiple runtime replicas over a shared store, freshness holds too: every Get re-reads the StateStore (per-read freshness — there is NO permanent loaded cache), so a Set on replica A is visible to replica B on B's next Get (the run loop's run-start resolution). The per-tenant mutex serialises a tenant's read/write; different tenants are independent via the sync.Map. The run loop already does StateStore reads at run start, so the per-Get Load is in the existing cost envelope.

func NewTenantOverridePolicy added in v1.5.0

func NewTenantOverridePolicy(st state.StateStore, bus events.EventBus, validModels []string, clock Clock) (*TenantOverridePolicy, error)

NewTenantOverridePolicy constructs the policy. `state` (the persistence floor) and `bus` (the audit/observability bus) are mandatory; a nil either fails loud with `ErrInvalidConfig`. `validModels` is the set of model names with a configured `ModelProfile` — `Set` rejects a model outside it with `ErrUnknownModel`. An empty `validModels` means no model is acceptable (any model set fails loud) — the binary always configures at least the bound default model, so an empty set signals a misconfiguration, not a latent default.

func (*TenantOverridePolicy) Close added in v1.5.0

Close marks the policy closed; subsequent Set / Get fail with `ErrClosed`. Idempotent.

func (*TenantOverridePolicy) Get added in v1.5.0

Get returns the tenant's current default-override spec and whether a record exists. A tenant with no record returns (zero spec, false, nil) — the run loop then falls through to the config defaults. A StateStore read failure fails loud with wrapped `ErrStateUnavailable` (no silent "no overrides" on read failure — CLAUDE.md §13).

func (*TenantOverridePolicy) Set added in v1.5.0

Set replaces tenant's default-override record with spec (desired-state replace: a nil field in spec clears that dimension; an all-nil spec clears the record entirely). It validates the spec, persists it, updates the cache, and emits `governance.tenant_overrides_set`.

Authorization (the admin-scope gate) is the Protocol surface's responsibility — this method assumes the caller is already authorized and focuses on validation + persistence. `tenant` MUST be non-empty (fail closed — identity is mandatory).

type TenantOverrideSpec added in v1.5.0

type TenantOverrideSpec struct {
	Model             *string
	ExtraInstructions *string
	Temperature       *float64
	MaxTokens         *int
	ReasoningEffort   *string
}

TenantOverrideSpec is the admin-set desired-state for a tenant's default LLM parameters. Every field is optional (nil = "inherit the config default for this dimension"). It is the in-process projection of the `governance.set_tenant_overrides` wire shape and the value the run loop resolves at run start.

  • Model overrides the default model name (validated against the configured ModelProfiles at set time).
  • ExtraInstructions is ADDITIVE — appended to the agent's system prompt, never a replacement.
  • Temperature / MaxTokens / ReasoningEffort override the matching LLM request fields.

func (TenantOverrideSpec) IsEmpty added in v1.5.0

func (s TenantOverrideSpec) IsEmpty() bool

IsEmpty reports whether the spec sets no dimension at all (a cleared desired-state). A cleared record means "the tenant inherits every config default."

type TenantOverridesSetPayload added in v1.5.0

type TenantOverridesSetPayload struct {
	events.SafeSealed
	// Actor is the admin-scoped caller that performed the set.
	Actor identity.Quadruple
	// Tenant is the tenant_id whose defaults were changed.
	Tenant string
	// Model is the new default model name, or "" when the model
	// dimension was left unset / cleared. Non-secret.
	Model string
	// SetModel / SetExtraInstructions / SetTemperature / SetMaxTokens /
	// SetReasoningEffort report which dimensions the new desired-state
	// record carries (true) versus inherits from config (false).
	SetModel             bool
	SetExtraInstructions bool
	SetTemperature       bool
	SetMaxTokens         bool
	SetReasoningEffort   bool
	OccurredAt           time.Time
}

TenantOverridesSetPayload is the typed payload for EventTypeTenantOverridesSet. SafePayload — the actor identity, the target tenant, the (non-secret) model name, and the per-dimension set/clear flags are operator-visible audit metadata. The extra-instructions PROSE is never carried here (only the boolean flag), so operator-supplied text never lands on the bus; the payload still runs through the audit Redactor before publish (CLAUDE.md §7 rule 6).

type TierConfig

type TierConfig struct {
	// BudgetCeilingUSD is the per-identity (per tier) cost ceiling in
	// USD. PreCall blocks when the accumulator's total for the request's
	// identity meets or exceeds this. 0 = no ceiling.
	BudgetCeilingUSD float64

	// RateLimit is the per-(identity, model) token-bucket config.
	// Zero-valued = no rate limit (latent).
	RateLimit RateLimitConfig

	// MaxTokens is the per-call cap. Requests whose `MaxTokens` exceed
	// this fail loudly. 0 = no cap (latent).
	MaxTokens int
}

TierConfig is one tier's policy bundle. Each field zero = latent for that policy. An operator who wants to enforce cost only sets `BudgetCeilingUSD`; rate-limit + MaxTokens fields stay zero.

type TierResolver

type TierResolver func(identity.Identity) string

TierResolver maps an `identity.Identity` to a tier name. Resolvers MUST be pure and deterministic — the same identity always maps to the same tier. Non-deterministic resolvers race against the per-identity state caches.

Directories

Path Synopsis
Package conformancetest exposes the canonical governance correctness suite that every supported `state.StateStore` driver must satisfy.
Package conformancetest exposes the canonical governance correctness suite that every supported `state.StateStore` driver must satisfy.

Jump to

Keyboard shortcuts

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