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
- Variables
- func CacheLen(s Subsystem) int
- func ClearFactory()
- func ErrIs(err, target error) bool
- func SetFactory(f Factory)
- func Wrap(inner llm.LLMClient, sub Subsystem) llm.LLMClient
- type BudgetExceededPayload
- type Clock
- type Config
- type CostAccumulator
- func (a *CostAccumulator) CacheLen() int
- func (a *CostAccumulator) Close(_ context.Context) error
- func (a *CostAccumulator) PostCall(ctx context.Context, req llm.CompleteRequest, resp llm.CompleteResponse, ...) error
- func (a *CostAccumulator) PreCall(ctx context.Context, req llm.CompleteRequest) error
- func (a *CostAccumulator) Snapshot(ctx context.Context, q identity.Quadruple) (float64, map[string]float64, error)
- type Factory
- type KeyRotatedPayload
- type MaxTokensEnforcer
- type MaxTokensExceededPayload
- type PostureProvider
- type PostureReadAdminPayload
- type RateLimitConfig
- type RateLimitedPayload
- type RateLimiter
- func (r *RateLimiter) CacheLen() int
- func (r *RateLimiter) Close(_ context.Context) error
- func (r *RateLimiter) PostCall(_ context.Context, _ llm.CompleteRequest, _ llm.CompleteResponse, _ error) error
- func (r *RateLimiter) PreCall(ctx context.Context, req llm.CompleteRequest) error
- func (r *RateLimiter) Snapshot(ctx context.Context, q identity.Quadruple) (map[string]int, error)
- type RealClock
- type Snapshot
- type Subsystem
- type TenantOverridePolicy
- type TenantOverrideSpec
- type TenantOverridesSetPayload
- type TierConfig
- type TierResolver
Constants ¶
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 ¶
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") // 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
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 ¶
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 ¶
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 ¶
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().
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 ¶
func (a *CostAccumulator) PostCall(ctx context.Context, req llm.CompleteRequest, resp llm.CompleteResponse, _ error) error
PostCall accumulates `resp.Cost.TotalCost` regardless of `callErr`. Failures still incur whatever cost the provider reported (some report 0 on failure; that's fine — the accumulator records 0).
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.
`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 ¶
func (a *CostAccumulator) Snapshot(ctx context.Context, q identity.Quadruple) (float64, map[string]float64, error)
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 ¶
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 ¶
func (e *MaxTokensEnforcer) PostCall(_ context.Context, _ llm.CompleteRequest, _ llm.CompleteResponse, _ error) error
PostCall is a no-op. MaxTokens enforcement is a pre-call gate only.
func (*MaxTokensEnforcer) PreCall ¶
func (e *MaxTokensEnforcer) PreCall(ctx context.Context, req llm.CompleteRequest) error
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 ¶
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 ¶
func (r *RateLimiter) PostCall(_ context.Context, _ llm.CompleteRequest, _ llm.CompleteResponse, _ error) error
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.
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.
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 ¶
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:
- MaxTokensEnforcer — purely in-memory cap check; cheapest reject.
- RateLimiter — bucket drain (lock + small state lookup).
- 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):
- MaxTokensEnforcer — purely in-memory cap check; cheapest reject.
- RateLimiter — bucket drain (lock + small state lookup).
- 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
func (p *TenantOverridePolicy) Close(_ context.Context) error
Close marks the policy closed; subsequent Set / Get fail with `ErrClosed`. Idempotent.
func (*TenantOverridePolicy) Get ¶ added in v1.5.0
func (p *TenantOverridePolicy) Get(ctx context.Context, tenant string) (TenantOverrideSpec, bool, error)
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
func (p *TenantOverridePolicy) Set(ctx context.Context, actor identity.Quadruple, tenant string, spec TenantOverrideSpec) error
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 ¶
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.
Source Files
¶
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. |