Documentation
¶
Overview ¶
Package delegation implements the core authorization primitives that make Legant an agent-identity layer rather than a generic OIDC server:
- scope attenuation — a delegated grant can only ever narrow scopes
- constraint policy — fine-grained limits carried in the token and enforced at the resource server
- composite (sub/act) — RFC 8693 delegation tokens that record the full delegation tokens user -> agent -> sub-agent provenance chain
It is deliberately free of database or HTTP dependencies so it can be unit tested in isolation and reused both by the /oauth2/token exchange endpoint and by example apps. See examples/agent-obo for a runnable demonstration.
Index ¶
Constants ¶
const DefaultMaxDepth = 5
DefaultMaxDepth bounds how deep a delegation chain may go (user -> agent -> sub-agent -> ...). It is a backstop against runaway or malicious re-delegation.
Variables ¶
This section is empty.
Functions ¶
func Attenuate ¶
Attenuate returns the requested scopes that are also present in the parent grant, preserving the requested order. A delegated token can never widen the scope it was granted.
Types ¶
type ActClaim ¶
ActClaim models the RFC 8693 "act" claim. The most recent actor is the top-most member; earlier actors are nested inside, giving a verifiable record of the whole delegation chain.
type Action ¶
type Action struct {
Scope string // scope the operation requires, e.g. "expenses:submit"
Amount float64 // monetary value, if any
Category string // category the action targets, if any
Tool string // MCP tool being invoked, if any
Resource string // resource audience being accessed, if any
At time.Time // instant of the action; zero means "now" (time-window check)
}
Action describes the concrete thing a token holder is trying to do. Fields left at their zero value are treated as "not applicable" and skip the corresponding constraint check.
type Constraints ¶
type Constraints struct {
// MaxAmount caps the monetary value of an action (e.g. an expense submission).
MaxAmount *float64 `json:"max_amount,omitempty"`
// Categories, when non-empty, is the allow-list of categories an action may target.
Categories []string `json:"categories,omitempty"`
// Tools, when non-empty, is the allow-list of MCP tool names the holder may invoke.
Tools []string `json:"tools,omitempty"`
// Resources, when non-empty, restricts which resource audiences (RFC 8707) are allowed.
Resources []string `json:"resources,omitempty"`
// TimeWindow, when set, restricts WHEN the authority may be used. It is enforced
// offline by the resource server (and the SDK) against the request time.
TimeWindow *TimeWindow `json:"time_window,omitempty"`
// Rate, when set, caps how often the authority may be exercised. Because a rate
// cap needs shared state, it is NOT enforced offline by a resource server; it is
// enforced by Legant at token-exchange time (mint frequency per delegation). It
// rides in the token for transparency and audit.
Rate *RateLimit `json:"rate,omitempty"`
}
Constraints are fine-grained limits attached to a delegation. An empty field means "no restriction on this dimension". They are carried inside the issued token (the "cnst" claim) and re-checked by the resource server against the concrete action being attempted — this is the authorization layer that a plain OAuth scope cannot express.
func Tighten ¶
func Tighten(parent, child Constraints) Constraints
Tighten merges two constraint sets into the strictest combination of both. It is used when a delegation is re-delegated: a child can only ever be at least as restricted as its parent, never looser.
func (Constraints) Permit ¶
func (c Constraints) Permit(a Action) error
Permit reports whether the action satisfies the constraints, returning a human-readable reason when it does not.
type DelegationClaims ¶
type DelegationClaims struct {
jwt.RegisteredClaims
Scope string `json:"scope"`
Act *ActClaim `json:"act,omitempty"`
Constraints *Constraints `json:"cnst,omitempty"`
}
DelegationClaims is the body of a composite delegation token: the subject is the resource owner, "act" records the acting agent chain, "scope" the effective (attenuated) scopes, and "cnst" the constraints to enforce.
func (*DelegationClaims) ActorChain ¶
func (c *DelegationClaims) ActorChain() []string
ActorChain returns the acting principals most-recent-first, for audit logging, e.g. ["agent:ocr", "agent:assistant"].
func (*DelegationClaims) Authorize ¶
func (c *DelegationClaims) Authorize(a Action) error
Authorize enforces, in order: that the token carries the scope the action requires, and that the action satisfies every constraint. This is the resource server's policy decision point.
func (*DelegationClaims) Provenance ¶
func (c *DelegationClaims) Provenance() string
Provenance renders the full delegation path for human-readable audit lines, e.g. "user:alice -> agent:assistant -> agent:ocr".
type Grant ¶
type Grant struct {
Delegator string // who granted this authority ("user:alice", "agent:assistant", ...)
Delegatee string // who received it ("agent:assistant", ...)
Scopes []string
Constraints Constraints
ExpiresAt time.Time
Parent *Grant
}
Grant is a single link in a delegation chain. A root grant has Parent == nil and is created by a human user delegating to an agent; subsequent links are agents re-delegating an attenuated slice of their authority to sub-agents.
func NewRootGrant ¶
func NewRootGrant(user, agent string, scopes []string, c Constraints, ttl time.Duration, now time.Time) *Grant
NewRootGrant creates the first link of a chain: a user delegating to an agent.
func (*Grant) ActorChainRootToLeaf ¶
ActorChainRootToLeaf returns every delegatee in the chain ordered from the principal closest to the user to the leaf agent actually holding the grant, e.g. ["agent:assistant", "agent:ocr"].
func (*Grant) Delegate ¶
func (parent *Grant) Delegate(delegatee string, scopes []string, c Constraints, ttl time.Duration, now time.Time, maxDepth int) (*Grant, error)
Delegate re-delegates an attenuated slice of a parent grant to a new delegatee. It enforces the two invariants that make chains safe:
- monotonic attenuation: scopes must be a subset of the parent's scopes
- no cycles / bounded depth: the delegatee must not already appear in the chain, and the chain may not exceed maxDepth links
Constraints are tightened against the parent so a child is never looser.
func (*Grant) EffectiveScopes ¶
EffectiveScopes returns the requested scopes attenuated against this grant.
func (*Grant) RootDelegator ¶
RootDelegator returns the human (or root principal) at the head of the chain — the resource owner on whose behalf the leaf agent ultimately acts.
type RateLimit ¶
type RateLimit struct {
MaxPerHour int `json:"max_per_hour"`
}
RateLimit caps how many times a delegation may be exercised per rolling hour.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer mints composite delegation tokens. In Legant proper this wraps the same RSA signing key used for ID tokens.
func (*Signer) IssueClaims ¶
func (s *Signer) IssueClaims(subject string, act *ActClaim, scopes []string, audience string, constraints *Constraints, exp, now time.Time) (string, error)
IssueClaims mints a composite delegation token from explicit claims. The MCP gateway uses it to mint a fresh, narrowly-scoped, audience-bound downstream token that PRESERVES the inbound provenance (sub + act chain) — confused-deputy protection, so the gateway never forwards the token it received.
func (*Signer) IssueForGrant ¶
func (s *Signer) IssueForGrant(g *Grant, requestedScopes []string, audience string, now time.Time) (string, error)
IssueForGrant performs the on-behalf-of token exchange for a grant: it attenuates the requested scopes, builds the nested act chain, and signs a short-lived token bound to a single resource audience. This is exactly what the /oauth2/token token-exchange grant will call.
type TimeWindow ¶
type TimeWindow struct {
Weekdays []int `json:"weekdays,omitempty"` // 0=Sunday … 6=Saturday; empty = any day
StartMin int `json:"start_min"` // inclusive minute-of-day [0,1439]
EndMin int `json:"end_min"` // inclusive minute-of-day [0,1439]
TZ string `json:"tz,omitempty"` // IANA location; empty = UTC
}
TimeWindow restricts the times at which a delegation may be used: an optional weekday allow-list and an inclusive minute-of-day range, evaluated in TZ (an IANA location name; empty means UTC). It is stateless, so a resource server enforces it offline from the token alone.
func (*TimeWindow) Allows ¶
func (w *TimeWindow) Allows(at time.Time) bool
Allows reports whether the instant at is inside the window. An unknown TZ fails closed (denies).
func (*TimeWindow) Validate ¶
func (w *TimeWindow) Validate() error
Validate checks the window is well-formed (used by the consent / re-delegation entry points before a window is persisted).
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier validates delegation tokens at a resource server. It holds only public keys — the resource server never needs Legant's signing key or database. Keys are indexed by key id (kid) so that during a key rotation the resource server can verify tokens signed by either the old or the new key, and so that a retired key (removed from the set) immediately stops being trusted.
func NewSingleKeyVerifier ¶
NewSingleKeyVerifier is a convenience for the one-key case (tests, examples).
func NewVerifier ¶
NewVerifier builds a verifier over a set of public keys indexed by kid — typically sourced from the issuer's JWKS endpoint.
func (*Verifier) Verify ¶
func (v *Verifier) Verify(tokenStr, expectedAudience string) (*DelegationClaims, error)
Verify parses and validates a token: RS256 signature under the key named by the token's kid header, plus issuer, audience, and expiry. It fails closed if the token carries no kid or names a key the verifier does not hold.
func (*Verifier) VerifyAny ¶
func (v *Verifier) VerifyAny(tokenStr string) (*DelegationClaims, error)
VerifyAny validates signature, issuer, and expiry without binding to a specific audience. Used by introspection, which does not know the audience the caller intends. Callers that consume scopes/resources must still check the audience and the revocation store themselves.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package chains is the database adapter for delegation grants: it hydrates a stored delegation into the pure delegation.Grant the signer mints from, and records the consent that authorizes a root delegation.
|
Package chains is the database adapter for delegation grants: it hydrates a stored delegation into the pure delegation.Grant the signer mints from, and records the consent that authorizes a root delegation. |