delegation

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jun 23, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

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

View Source
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

func Attenuate(parent, requested []string) []string

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.

func HasScope

func HasScope(scopeStr, scope string) bool

HasScope reports whether scope is present in the space-delimited scope string.

func IsSubset

func IsSubset(child, parent []string) bool

IsSubset reports whether every scope in child is present in parent.

Types

type ActClaim

type ActClaim struct {
	Sub string    `json:"sub"`
	Act *ActClaim `json:"act,omitempty"`
}

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

func (g *Grant) ActorChainRootToLeaf() []string

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

func (g *Grant) EffectiveScopes(requested []string) []string

EffectiveScopes returns the requested scopes attenuated against this grant.

func (*Grant) RootDelegator

func (g *Grant) RootDelegator() string

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 NewSigner

func NewSigner(issuer, kid string, key *rsa.PrivateKey) *Signer

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

func NewSingleKeyVerifier(issuer, kid string, pub *rsa.PublicKey) *Verifier

NewSingleKeyVerifier is a convenience for the one-key case (tests, examples).

func NewVerifier

func NewVerifier(issuer string, keysByKID map[string]*rsa.PublicKey) *Verifier

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.

Jump to

Keyboard shortcuts

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