Documentation
¶
Index ¶
- Constants
- func LimitsFrom(limits []schemas.Limit, kinds ...LimitHolderKind) []schemas.Limit
- func LimitsHeldBy(kind LimitHolderKind, holderID, holderName, provider, model string, ...) []schemas.Limit
- func NewCredential(kind CredentialKind, value string) schemas.Credential
- type Access
- func (a *Access) AllowsEveryModel(provider string) bool
- func (a *Access) Bases() []schemas.Permit
- func (a *Access) DeniedPermitsForAgent(agentName string) []schemas.Permit
- func (a *Access) DeniedPermitsForMCPTool(toolPattern string) []schemas.Permit
- func (a *Access) DeniedPermitsForModel(provider string, model string) []schemas.Permit
- func (a *Access) DeniedPermitsForRelay(endpointName string) []schemas.Permit
- func (a *Access) GrantedProvidersForModel(model string) []string
- func (a *Access) IsAgentAllowed(agentName string) bool
- func (a *Access) IsMCPToolAllowed(toolPattern string) bool
- func (a *Access) IsModelAllowed(provider string, model string) bool
- func (a *Access) IsProviderAllowed(provider string) bool
- func (a *Access) IsRelayAllowed(endpointName string) bool
- func (a *Access) IsScoped() bool
- func (a *Access) KeysForModel(provider string, model string) (keyIDs []string, restricted bool)
- func (a *Access) MCPToolIncludeList() []string
- func (a *Access) Mode() string
- func (a *Access) NarrowMCPToolIncludeList(requested []string) []string
- func (a *Access) PermitsForModel(provider string, model string) []schemas.Permit
- func (a *Access) ProvidersForModel(model string) []schemas.ProviderCandidate
- func (a *Access) Scoping() schemas.Permit
- type CompositionMode
- type CredentialKind
- type Grant
- type Identity
- func (p *Identity) BusinessUnits() []schemas.EntityRef
- func (p *Identity) Credential() schemas.Credential
- func (p *Identity) Customers() []schemas.EntityRef
- func (p *Identity) Presented() bool
- func (p *Identity) Project() *schemas.EntityRef
- func (p *Identity) Teams() []schemas.EntityRef
- func (p *Identity) User() *schemas.UserRef
- func (p *Identity) VirtualKey() *schemas.EntityRef
- type LimitHolderKind
- type Limits
- type ModelMatcher
- type Permit
- func (p *Permit) AgentPermits() []string
- func (p *Permit) AllowsAllProviders() bool
- func (p *Permit) ID() string
- func (p *Permit) IsActive() bool
- func (p *Permit) IsExpired() bool
- func (p *Permit) MCPPermits() []schemas.MCPPermit
- func (p *Permit) Name() string
- func (p *Permit) ProviderPermits() []schemas.ProviderPermit
- func (p *Permit) RelayPermits() []string
- func (p *Permit) Type() string
- type PermitOption
- type PermitType
Constants ¶
const Wildcard = "*"
Wildcard marks an unrestricted list: every value is allowed, or every value is blocked, depending on which kind of list carries it.
The lists a permit carries are schemas.WhiteList and schemas.BlackList, so what a list means travels on its type: a list holding only the wildcard is unrestricted, an empty list holds nothing, and membership ignores case. Key IDs are the one exception: they are identifiers, so whoever matches them does so exactly rather than through the type's membership methods (see composeKeyIDs), and only their wildcard is read off the type.
Variables ¶
This section is empty.
Functions ¶
func LimitsFrom ¶
func LimitsFrom(limits []schemas.Limit, kinds ...LimitHolderKind) []schemas.Limit
LimitsFrom keeps the limits held by any of kinds, and is how a caller that enforces one holder's limits asks about exactly that holder.
It exists because "does anything govern this?" and "does this holder govern this?" are different questions, and a request's limits come from several holders at once. A check written against one holder, a key's budgets say, must gate on that holder's own limits, or it fires on a team's budget and then reports on the key's, which passes while the team is exhausted. Passing no kinds keeps nothing, since asking about no holder is asking about nothing.
func LimitsHeldBy ¶
func LimitsHeldBy(kind LimitHolderKind, holderID, holderName, provider, model string, ids ...string) []schemas.Limit
LimitsHeldBy names the limits one holder imposes, from the identifiers of the records that enforce them. provider and model are recorded on each for description, empty when the limit is scoped to neither; see schemas.Limit for why nothing selects on them.
Identifiers rather than records, for the reason on schemas.Limit, and it is also what keeps this package out of the business of what a budget row looks like: whoever reads those rows already knows their shape, and all this needs from them is an identity. Records without one are skipped, since a limit that cannot be loaded cannot be enforced.
func NewCredential ¶
func NewCredential(kind CredentialKind, value string) schemas.Credential
NewCredential is a schemas.Credential of the given kind.
Types ¶
type Access ¶
type Access struct {
// contains filtered or unexported fields
}
Access is the one implementation of schemas.Access: the fold of the permits a request carries.
Per attempt rather than per request, because a request that fails over resolves again for each provider it tries. Configuration can change between two slow calls, and the attempt running second has to answer to what is in force when it runs.
The caller's permits are a list because a caller can hold several at once, and they are read as one, as if they were a single permit holding all of their provider and MCP permits: what any of them permits, the caller may reach; any key any of them allows may serve the request; and every one of them that permits the pair pays for it (see PermitsForModel). Their order is the order the resolver gave them, and is the order candidates and refusals are reported in.
There is one scoping slot, not a list. A request is scoped by at most one thing at a time, and composing under one mode is what keeps every answer here decidable.
func NewAccess ¶
func NewAccess(bases []schemas.Permit, scoping schemas.Permit, mode CompositionMode, matcher ModelMatcher) *Access
NewAccess folds the permits the caller holds together with the permit scoping this request, under one composition mode.
A request composes under exactly one mode. Choosing it belongs to whoever resolved the permits: that layer knows what it asked and what answered. Passing no scoping permit leaves the mode irrelevant and the answer is the caller's permits alone. Nil entries in bases are dropped, so a resolver that found nothing for one source does not have to say so twice.
func (*Access) AllowsEveryModel ¶ added in v1.9.0
AllowsEveryModel implements schemas.Access.
func (*Access) DeniedPermitsForAgent ¶ added in v1.9.0
DeniedPermitsForAgent implements schemas.Access.
func (*Access) DeniedPermitsForMCPTool ¶
DeniedPermitsForMCPTool implements schemas.Access.
func (*Access) DeniedPermitsForModel ¶
DeniedPermitsForModel implements schemas.Access.
What is named is whatever refused and whose permission would have changed the answer. The caller's permits are read as one, so when none of them permits the pair all of them refused and all are named: a caller holding two profiles is told that neither permits the model, not that one of them does not. Under an intersection, when one of them permits it and the scoping permit does not, the scoping permit alone stands in the way and alone is named. Under a union both sides must have refused, and both are named. A caller holding no permit at all has nothing to be named.
func (*Access) DeniedPermitsForRelay ¶ added in v1.11.0
DeniedPermitsForRelay implements schemas.Access.
func (*Access) GrantedProvidersForModel ¶
GrantedProvidersForModel implements schemas.Access. Because this gate cannot ask ProvidersForModel anything, the composition it needs is re-derived by name below.
func (*Access) IsAgentAllowed ¶ added in v1.9.0
IsAgentAllowed implements schemas.Access.
func (*Access) IsMCPToolAllowed ¶
IsMCPToolAllowed implements schemas.Access.
func (*Access) IsModelAllowed ¶
IsModelAllowed implements schemas.Access.
func (*Access) IsProviderAllowed ¶
IsProviderAllowed implements schemas.Access.
func (*Access) IsRelayAllowed ¶ added in v1.11.0
IsRelayAllowed implements schemas.Access.
func (*Access) KeysForModel ¶
KeysForModel implements schemas.Access.
The caller's side is the union of every key restriction its permitting permits hold for the provider, across all of their provider permits for it: the caller's permits are read as one, and a request served by any of them may use any key any of them allows. That is composed with the scoping permit's restriction only where the scoping permit also authorizes the pair. A scoping permit that narrows a request to two of the provider's keys has to mean it, and taking the caller's list instead would hand the request a key the scope refused; but a permit the request is not proceeding on has no say, which is why the model is asked for.
The result is a plain []string rather than any list type of the permit's own: it is handed straight to consumers that type-assert []string, where a named slice type would fail the assertion silently and read as "no restriction at all".
func (*Access) MCPToolIncludeList ¶
MCPToolIncludeList implements schemas.Access.
The caller's permits are read as one, so their entries are merged in order before the scoping permit is composed on: a tool any of them grants, the caller may execute.
func (*Access) NarrowMCPToolIncludeList ¶
NarrowMCPToolIncludeList implements schemas.Access.
A "<client>-*" wildcard is kept as a wildcard only when the access grants that client every tool, because downstream a wildcard reads as every tool of the client; otherwise it is replaced by the specific tools the access grants for that client, which may be none. A specific entry is kept when the access permits it. Empty entries and repeats are dropped. No access at all permits nothing.
func (*Access) PermitsForModel ¶
PermitsForModel implements schemas.Access.
func (*Access) ProvidersForModel ¶
func (a *Access) ProvidersForModel(model string) []schemas.ProviderCandidate
ProvidersForModel implements schemas.Access. Model names resolve through the matcher here. Unlike GrantedProvidersForModel, this is the decision itself rather than a gate over one, so cross-provider naming and provider-prefixed entries must resolve exactly as the deployment resolves them.
type CompositionMode ¶
type CompositionMode string
CompositionMode is how a scoping permit combines with the permits the caller holds. These are the values a schemas.Access reports from Mode.
const ( // Intersect permits what the caller's permits and the scoping permit both permit. Intersect CompositionMode = "intersect" // Union permits what either of them permits. Union CompositionMode = "union" )
type CredentialKind ¶
type CredentialKind string
CredentialKind names what a request presented to say who it is: one of the things this gateway identifies a caller by. These are the values a schemas.Credential carries in Kind. A raw provider key is not among them: it selects which key serves the request and says nothing about who made it.
const ( // CredentialVirtualKey marks a virtual key. CredentialVirtualKey CredentialKind = "virtual_key" // CredentialSessionToken marks a dashboard session token. CredentialSessionToken CredentialKind = "session_token" // CredentialIdentityToken marks a token the identity provider issued for the user. CredentialIdentityToken CredentialKind = "idp_jwt" // CredentialMCPToken marks a token this deployment's own authorization server issued to an MCP // client. CredentialMCPToken CredentialKind = "mcp_jwt" // CredentialAPIKey marks a management API key. CredentialAPIKey CredentialKind = "api_key" // CredentialEdgeSession marks a Bifrost Edge agent's session token. Its value is the user's ID. CredentialEdgeSession CredentialKind = "edge_session" )
type Grant ¶
type Grant struct {
// contains filtered or unexported fields
}
Grant is the envelope a request carries its sections in. See schemas.Grant for what each section is and when it settles.
Each section is held behind an atomic pointer and replaced whole. A consumer that read a section keeps the answer it read, so two readers of one attempt never see a torn or partially updated value, and a section being replaced cannot change a decision that was already made against it.
func (*Grant) SetAccess ¶
SetAccess implements schemas.Grant. Recording nothing is a no-op, as the interface says.
func (*Grant) SetIdentity ¶
SetIdentity implements schemas.Grant. Recording nothing is a no-op, as the interface says.
type Identity ¶
type Identity struct {
// contains filtered or unexported fields
}
Identity is the one implementation of schemas.Identity. It is built whole and never edited: a layer that learns more about the caller builds the replacement from what the getters return plus what it learned, which is what lets a request's context hand a identity out without a lock.
func NewIdentity ¶
func NewIdentity( credential schemas.Credential, user *schemas.UserRef, virtualKey *schemas.EntityRef, teams []schemas.EntityRef, customers []schemas.EntityRef, businessUnits []schemas.EntityRef, project *schemas.EntityRef, ) *Identity
NewIdentity builds a Identity. See schemas.Identity for what each value means. The lists are copied, so a caller that keeps its slices cannot alter what the request is attributed to.
func (*Identity) BusinessUnits ¶
BusinessUnits implements schemas.Identity.
func (*Identity) Credential ¶
func (p *Identity) Credential() schemas.Credential
Credential implements schemas.Identity.
func (*Identity) VirtualKey ¶
VirtualKey implements schemas.Identity.
type LimitHolderKind ¶
type LimitHolderKind string
LimitHolderKind identifies what kind of entity a limit hangs off. These are the values a schemas.Limit carries in HolderKind. An open string, like PermitType, so a deployment resolving a holder of its own can name it without this package changing. The kinds that do exist are named below rather than wherever each is resolved, because telling one limit from another is a single vocabulary and a reader should not have to know which layer introduced a kind.
const ( // Held by a virtual key. LimitHolderVirtualKey LimitHolderKind = "vk" LimitHolderVirtualKeyProviderConfig LimitHolderKind = "vk_provider_config" LimitHolderVirtualKeyModelConfig LimitHolderKind = "vk_model_config" // Held by an access profile attached to a user. Named for the user rather than generically, // because a profile attached to something else is a different holder: see the team, business // unit and customer profile kinds below, which exist for exactly the reason this comment once // anticipated - a refusal has to say whose profile it was. LimitHolderUserAccessProfile LimitHolderKind = "user_access_profile" LimitHolderUserAccessProfileProviderConfig LimitHolderKind = "user_access_profile_provider_config" LimitHolderUserAccessProfileModelConfig LimitHolderKind = "user_access_profile_model_config" // Held by an access profile attached to a team, business unit or customer. The money is the // entity's: it is charged on every request made under the entity - by a member or with a key the // entity owns - whichever permit admitted the model. One holder each, for the same reason the // permit kinds are separate: a caller told only that "an access profile is exhausted" cannot tell // whose. LimitHolderTeamAccessProfile LimitHolderKind = "team_access_profile" LimitHolderTeamAccessProfileProviderConfig LimitHolderKind = "team_access_profile_provider_config" LimitHolderTeamAccessProfileModelConfig LimitHolderKind = "team_access_profile_model_config" LimitHolderBusinessUnitAccessProfile LimitHolderKind = "business_unit_access_profile" LimitHolderBusinessUnitAccessProfileProviderConfig LimitHolderKind = "business_unit_access_profile_provider_config" LimitHolderBusinessUnitAccessProfileModelConfig LimitHolderKind = "business_unit_access_profile_model_config" LimitHolderCustomerAccessProfile LimitHolderKind = "customer_access_profile" LimitHolderCustomerAccessProfileProviderConfig LimitHolderKind = "customer_access_profile_provider_config" LimitHolderCustomerAccessProfileModelConfig LimitHolderKind = "customer_access_profile_model_config" // Held by the user a request is attributed to, directly — a per-model limit assigned straight // to the user rather than derived from an access profile they hold. Kept distinct from the // LimitHolderUserAccessProfile* kinds because a refusal has to say which one ran out, and from // LimitHolderVirtualKey because a caller can be told to stop counting a request against the key // that granted it while still counting it against the user who made it. LimitHolderUserModelConfig LimitHolderKind = "user_model_config" // Held by the organization above the caller. Spent by every request made under anything they // contain. LimitHolderTeam LimitHolderKind = "team" LimitHolderBusinessUnit LimitHolderKind = "business_unit" LimitHolderCustomer LimitHolderKind = "customer" // Held by a project, which is not part of the organization above the caller. A project is a // scope a request opts into rather than a container the caller sits inside, so what it funds // is spent by whoever names it and by nobody else: a caller's own limits and their // organization's are untouched by it, and the same caller reaching the same model outside the // project answers to none of these. LimitHolderProject LimitHolderKind = "project" LimitHolderProjectProviderConfig LimitHolderKind = "project_provider_config" LimitHolderProjectModelConfig LimitHolderKind = "project_model_config" // Held by no one in the organization. A provider's limits are the deployment's own, and a model // config's are scoped to a (model, provider) pattern. Neither is anybody's allowance, so a // refusal naming one of these is telling the caller about the deployment rather than about // themselves. LimitHolderProvider LimitHolderKind = "provider" LimitHolderModelConfig LimitHolderKind = "model_config" )
Every kind of limit holder, declared together and grouped by holder. Within a holder the kinds run from the widest scope to the narrowest: the holder's own limits, spent by every request it makes; its per-provider limits, spent only by requests that provider serves; and its per-model limits, spent only by requests for that model.
Once resolved for an attempt, a request's limits are one flat list, and these are what tell them apart afterwards: a refusal has to say whether it was your key's budget or your team's, and a caller looking at several limits of the same shape needs to know which is which.
Nothing registers or enumerates them. Enforcement covers every kind, so a limit carrying any of these is checked and charged by the same path, and a deployment that resolves a holder of its own declares its kind here and is enforced by construction.
type Limits ¶
type Limits struct {
// contains filtered or unexported fields
}
Limits is the one implementation of schemas.Limits.
Resolving them is the caller's to do and its answer is taken as given. Which holders are charged is a question about how a deployment is configured, and this package would have to learn what a project, a team or a model config is in order to have an opinion. So it holds the answer without deriving it.
One limit reached twice is still one limit, so what is held is deduplicated. That is not an opinion about which holders are charged; it is that charging one budget twice for a single request is never what a deployment meant, however its holders overlap. A team or customer reached through two permits, or a customer named directly as well as through its team, arrives twice and must not be billed twice.
func NewLimits ¶
NewLimits holds the limits an attempt answers to. Both lists are copied as well as deduplicated, so a caller that keeps its slice cannot alter what the attempt is held to.
func (*Limits) Budgets ¶
Budgets implements schemas.Limits. A copy, not the internal slice: enforcement, billing, and logging all read the same settled Limits for one attempt, and a caller sorting or overwriting what it got back must not change what the next reader sees.
func (*Limits) RateLimits ¶
RateLimits implements schemas.Limits. See Budgets for why this returns a copy.
type ModelMatcher ¶
ModelMatcher decides whether model, as the request names it, is one of allowed on provider. Whoever resolves a request's access supplies it, because deciding that needs the deployment's own view of the provider: how it names models, and which provider-prefixed or cross-provider entries in allowed stand for the same model. Without one, entries are matched by name.
type Permit ¶
type Permit struct {
// contains filtered or unexported fields
}
Permit is the one implementation of schemas.Permit.
It is a snapshot for one attempt. The provider permits carry a copy of the configuration they came from, so a source reloaded mid-attempt cannot change what that attempt has already resolved. A later attempt builds its own and picks the reload up.
func NewPermit ¶
func NewPermit( permitType PermitType, id string, name string, isActive bool, isExpired bool, providerPermits []schemas.ProviderPermit, mcpPermits []schemas.MCPPermit, opts ...PermitOption, ) *Permit
NewPermit builds a Permit. See schemas.Permit for what each value means. The lists are deep copied, down to each entry's own slices and its Weight pointer, so a resolver that keeps its slices (or the value behind a Weight pointer) cannot alter what an attempt was admitted under.
func (*Permit) AgentPermits ¶ added in v1.9.0
AgentPermits implements schemas.Permit. See ProviderPermits for why this returns a copy.
func (*Permit) AllowsAllProviders ¶
AllowsAllProviders implements schemas.Permit.
func (*Permit) MCPPermits ¶
MCPPermits implements schemas.Permit. See ProviderPermits for why this returns a copy.
func (*Permit) ProviderPermits ¶
func (p *Permit) ProviderPermits() []schemas.ProviderPermit
ProviderPermits implements schemas.Permit. A copy, not the permit's own slice: the permit is a snapshot for the whole attempt, read by every consumer that asks what it permits, and a caller mutating what it got back must not change what the next reader sees.
func (*Permit) RelayPermits ¶ added in v1.11.0
RelayPermits implements schemas.Permit. See ProviderPermits for why this returns a copy.
type PermitOption ¶
type PermitOption func(*Permit)
PermitOption configures a Permit at construction. Options keep NewPermit's required arguments stable while letting a source set the occasional extra, so a resolver that does not need one is unaffected.
func WithAgentPermits ¶ added in v1.9.0
func WithAgentPermits(agentNames []string) PermitOption
WithAgentPermits grants the named Agent Gateway registrations.
func WithAllowAllProviders ¶
func WithAllowAllProviders(allow bool) PermitOption
WithAllowAllProviders grants every provider, including ones the permit holds no provider permit for: those are allowed with all models and all keys, while a provider it does hold a permit for still applies that permit's rules. See schemas.Permit.AllowsAllProviders.
func WithRelayPermits ¶ added in v1.11.0
func WithRelayPermits(endpointNames []string) PermitOption
WithRelayPermits grants the named relays.
type PermitType ¶
type PermitType string
PermitType identifies what kind of source a permit's access comes from. These are the values a schemas.Permit reports from Type. An open string: kinds are declared by whoever resolves permits of that kind, so this package needs no list of them.
const ( // PermitVirtualKey marks permits whose access comes from a virtual key. PermitVirtualKey PermitType = "vk" // PermitAccessProfile marks permits whose access comes from a profile attached to a caller. PermitAccessProfile PermitType = "access_profile" // PermitTeamAccessProfile, PermitBusinessUnitAccessProfile and PermitCustomerAccessProfile mark // permits whose access comes from a profile attached to a team, business unit or customer. Such a // profile applies to everything under the entity - its members' requests and the keys it owns - // and joins the caller's other permits under union, so it widens what may be reached. One kind // each rather than one shared kind, because a refusal has to say whose profile it named: "your // team's access profile" and "your business unit's" are different answers. PermitTeamAccessProfile PermitType = "team_access_profile" PermitBusinessUnitAccessProfile PermitType = "business_unit_access_profile" PermitCustomerAccessProfile PermitType = "customer_access_profile" // PermitProject marks permits whose access comes from a project a request names. A project is // not something the caller belongs to but something the request opts into, so it grants // alongside whatever the caller already holds rather than instead of it. PrettyString needs no // case for it: the identifier already reads as prose, which is the only reason the others have // one. PermitProject PermitType = "project" )
The permit kinds this package names. The type stays open, so these are not the only kinds that can exist. They are the ones PrettyString can render, which is why they are declared here rather than wherever permits of each kind are resolved: a refusal has to read the same whichever kind it names.
func (PermitType) PrettyString ¶
func (t PermitType) PrettyString() string
PrettyString is what a refusal calls this permit kind: "your team's access profile", not "team_access_profile". Every kind is named here because every kind is declared here - the labels are fixed words, so there is nothing for another build to supply. A kind with no case renders as its identifier, which is ugly but still says what ran out.