agentauth

package
v1.9.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 31 Imported by: 0

README

agentauth

Lets a user hand an AI agent scoped, expiring, revocable access to act on their behalf.

An agent session is not a copy of the user's session. What the agent can do is the intersection of two things: the scopes the human granted it, and what that human is allowed to do anyway. Narrow the human's role and every agent acting for them narrows with it. Revoke the grant and the agent stops, without touching the human's own access.

Agent sessions live 15 minutes and cannot be refreshed. Engine.Refresh refuses to rotate a session whose principal kind is agent, on purpose, so no agent credential can outlive the grant that authorised it. You re-issue instead.

Read this before you wire it

Registering the plugin enforces nothing. It is a set of primitives, not a finished feature, and a host application has to do five things before an agent session carries any real restriction. Until all five are done, an agent that authenticates through the ordinary OAuth2 flow gets a plain human session with that user's full access.

  1. Point oauth2provider at the consent gate, so org policy is consulted when a user approves an agent. OnInit does this for you when an oauth2provider plugin is registered on the same engine. Wire oauth2provider yourself, or register it after agentauth, and you must call oauth2provider.SetConsentGate by hand.
  2. Call CreateGrant from your own consent handler and persist the returned AgentGrant.ID. There is no consent screen in this package, only the primitive behind one.
  3. Mint the credential with IssueAgentSession. The standard OAuth2 token endpoint knows nothing about this package and will hand back an ordinary human session.
  4. Mount Guard(action, resource) on every route an agent can reach, in addition to your normal RBAC check. middleware.RequirePermission is not principal-aware: it resolves the permission of sess.UserID, which for an agent session is the delegating human. A route with RequirePermission alone gives the agent that human's full access. Guard alone gives it no RBAC check at all. You need both.
  5. Re-issue before expiry. There is no long-lived agent credential. The grant is the durable thing; the session is not.

Use it when

  • You are letting agents call your API for a user and "the user's own token" is too much access
  • You need to show a person which agents act for them, and give them a revoke button
  • An org admin needs to approve or block agents across the whole tenant

Skip it when

  • Your agents are first-party and act as themselves, not for a user. apikey or OAuth2 client credentials fit that
  • You cannot audit every route an agent reaches. The intersection only holds where Guard is mounted, and a single unguarded route undoes it
  • You want it working by registering the plugin. Read the five steps above first

Wiring

agents := agentauth.New(
    agentauth.WithDefaultGrantTTL(30*24*time.Hour),
    agentauth.WithScope("calendar.read", agentauth.Permission{Action: "read", Resource: "calendar"}),
)

engine, err := authsome.NewEngine(
    authsome.WithStore(store),
    authsome.WithPlugin(oauth2provider.New(oauth2provider.Config{Issuer: "https://auth.example.com"})),
    authsome.WithPlugin(agents),
)

Then guard the routes:

router.GET("/v1/calendar", handler,
    middleware.RequirePermission(engine, "read", "calendar"), // human RBAC
    agents.Guard("read", "calendar"),                         // agent scope intersection
)

Options

Option What it does
WithStore(s Store) Use a specific store. Defaults to the engine store
WithDefaultGrantTTL(d time.Duration) How long a new grant lasts before it expires
WithScope(scope string, p Permission) Map a scope string onto an action and resource pair

Endpoints

User surface under /v1/me/agents, admin surface under /v1/admin/agents. Both refuse agent-principal sessions, so an agent cannot manage its own delegation.

Method Path Purpose
GET /v1/me/agents List agents acting on my behalf
DELETE /v1/me/agents/:id Revoke an agent's delegation
GET /v1/admin/agents List registered agents
POST /v1/admin/agents Register an agent
PATCH /v1/admin/agents/:id/status Approve or block an agent
GET /v1/admin/agents/policy Read the org's delegation policy
PUT /v1/admin/agents/policy Set the org's delegation policy

Lifecycle hooks

Hook What happens
AfterUserUpdate Re-checks grants when the delegating human's access changes
AfterUserDelete Tears down grants belonging to a deleted user
AfterOrgDelete Tears down grants belonging to a deleted org
BeforeMemberRemove Handles delegations held through an org membership being removed

It is also a MigrationProvider for its agent and grant tables, and a RouteProvider.

oauth2provider is where the consent gate plugs in. organization supplies the org whose policy the admin surface reads and writes. doc.go carries the full host integration contract, and is the authority if this README and it ever disagree.

Documentation

Overview

Package agentauth: see doc.go for the package doc comment and the host integration contract. Agent is a non-human principal that always acts on behalf of a human, which is what separates it from a serviceaccount.ServiceAccount.

Package agentauth adds delegated agent identity to authsome: a way for a user to hand an AI agent scoped, expiring, revocable access, enforced as the intersection of what the agent was granted and what the delegating human may themselves do. See docs/superpowers/specs/2026-08-24-agentauth-delegation-design.md for the full design.

Host integration contract

This package is a set of primitives, not a wired feature. Registering the plugin alone enforces nothing. A host application must do all five of the following, in this order, before an agent session carries any real restriction:

  1. Call oauth2provider.SetConsentGate(agentauthPlugin), or set oauth2provider.Config.ConsentGate to the agentauth plugin, so that org policy is consulted at the moment a user consents to an agent. agentauth's own OnInit does this automatically when an "oauth2provider" plugin is registered on the same engine — see Plugin.OnInit — but a host that constructs and wires oauth2provider independently of the plugin registry (or that registers oauth2provider after agentauth has already run its own OnInit) must still call one of these itself.

  2. Call agentauth.CreateGrant from the application's own consent handler — the screen or endpoint where a user approves an agent for a set of scopes — and persist the returned AgentGrant.ID somewhere the application can find again (its own consent record, typically). There is no consent UI or handler in this package; only the primitive that backs one.

  3. Call agentauth.IssueAgentSession to mint the agent's credential, rather than relying on oauth2provider's own token endpoint. The standard OAuth2 token endpoint mints an ordinary human session: no PrincipalKind, no grant, the application's normal session TTL, a long-lived refresh token, and the delegating human's full role set. Nothing about the OAuth2 authorization-code exchange knows this package exists. IssueAgentSession is the only path that produces a session carrying PrincipalKind "agent", AgentID and GrantID.

  4. Mount agentauth.Guard(action, resource) on every route an agent may reach — IN ADDITION TO middleware.RequirePermission or an equivalent RBAC check, not instead of it. Guard enforces nothing for a session that is not agent-principal; it exists purely to add the scope intersection on top of whatever a human-facing permission check already does. middleware.RequirePermission and middleware.RequireAnyRole are not principal-kind aware: they resolve the permission or role of sess.UserID, which for an agent session is the delegating human, so an unguarded route grants the agent that human's full permission, not the scoped subset the grant named. A host that mounts RequirePermission alone, or Guard alone, has not implemented the intersection this package exists to provide.

  5. Re-issue, don't refresh. Agent sessions cannot be refreshed: Engine.Refresh (service.go) refuses to rotate a session whose PrincipalKind is "agent", by design, so that a session can never outlive the grant that authorized it through the generic refresh path. An agent session's TTL is short — 15 minutes, see agentSessionTTL in issue.go — and a host must call IssueAgentSession again before it expires to keep the agent working. There is no long-lived agent credential; the grant is the long-lived thing, the session is not.

Until an application has wired all five of these, agentauth enforces nothing: an agent that authenticates through the ordinary OAuth2 flow receives a plain human session with that user's full access, and every route not explicitly wrapped in Guard is reachable with it regardless of what any grant says.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInsufficientScope means the grant does not confer the permission the
	// route requires. Safe to report to the agent: it names only what the
	// agent was granted, which the agent already knows.
	ErrInsufficientScope = errors.New("agentauth: insufficient scope")
	// ErrGrantInactive means the grant is missing, revoked, expired, does not
	// belong to this session's principal, or the session itself is malformed
	// in a way that makes "is this an active agent grant" unanswerable.
	ErrGrantInactive = errors.New("agentauth: grant is not active")
	// ErrNoPermissionChecker means the engine exposes no RBAC, so the user
	// gate cannot run and the request is denied.
	ErrNoPermissionChecker = errors.New("agentauth: no permission checker available")
	// ErrUserNotPermitted means the grant and scope were fine but the
	// delegating human cannot do this. Distinct from ErrInsufficientScope so
	// callers can map both without string matching.
	ErrUserNotPermitted = errors.New("agentauth: delegating user lacks permission")
)

Sentinel errors so callers can map to the right HTTP response without string matching.

View Source
var ErrConflict = errors.New("agentauth: conflict")

ErrConflict is returned when a write would violate a uniqueness invariant this package depends on — currently just an agent's ClientID, which GetAgentByClientID and Evaluate both assume resolves to at most one agent. A store that let two agents share a ClientID would make that resolution nondeterministic: whichever record a range-based lookup happens to visit first decides whether a blocked agent's client is treated as blocked.

View Source
var ErrNotFound = errors.New("agentauth: not found")

ErrNotFound is returned when an agent or grant cannot be found.

View Source
var MongoMigrations = migrate.NewGroup("authsome-agentauth", migrate.DependsOn("authsome"))

MongoMigrations is the MongoDB migration group for the agentauth plugin. MongoDB is schemaless, so this exists to create the client_id uniqueness index — the same atomicity guarantee CreateAgent needs on SQL, since a check-then-insert at the application layer races the same way there.

View Source
var PostgresMigrations = migrate.NewGroup("authsome-agentauth", migrate.DependsOn("authsome"))

PostgresMigrations is the postgres migration group for the agentauth plugin.

View Source
var SqliteMigrations = migrate.NewGroup("authsome-agentauth", migrate.DependsOn("authsome"))

SqliteMigrations is the SQLite migration group for the agentauth plugin.

Functions

func HeaderOf

func HeaderOf(err error, name string) string

HeaderOf returns a response header the error carries, or "" if it carries none or the header was never set.

func StatusOf

func StatusOf(err error) int

StatusOf returns the HTTP status an error carries, or 0 if it carries none.

Types

type Agent

type Agent struct {
	ID          id.AgentID  `json:"id"`
	AppID       id.AppID    `json:"app_id"`
	OrgID       id.OrgID    `json:"org_id,omitempty"`
	ClientID    string      `json:"client_id"`
	Name        string      `json:"name"`
	Description string      `json:"description,omitempty"`
	LogoURI     string      `json:"logo_uri,omitempty"`
	Origin      AgentOrigin `json:"origin"`
	Status      AgentStatus `json:"status"`
	CreatedBy   id.UserID   `json:"created_by,omitempty"`
	CreatedAt   time.Time   `json:"created_at"`
	UpdatedAt   time.Time   `json:"updated_at"`
}

Agent is a non-human principal that always acts for a human. It is keyed to an oauth2provider client by ClientID rather than embedded in it, so this package never migrates that plugin's schema.

type AgentGrant

type AgentGrant struct {
	ID         id.AgentGrantID `json:"id"`
	AppID      id.AppID        `json:"app_id"`
	AgentID    id.AgentID      `json:"agent_id"`
	UserID     id.UserID       `json:"user_id"`
	OrgID      id.OrgID        `json:"org_id,omitempty"`
	Scopes     []string        `json:"scopes"`
	ConsentID  id.ConsentID    `json:"consent_id,omitempty"`
	ExpiresAt  time.Time       `json:"expires_at"`
	LastUsedAt *time.Time      `json:"last_used_at,omitempty"`
	RevokedAt  *time.Time      `json:"revoked_at,omitempty"`
	CreatedAt  time.Time       `json:"created_at"`
	UpdatedAt  time.Time       `json:"updated_at"`
}

AgentGrant is one user's delegation to one agent. UserID is never the zero value and ExpiresAt is a value rather than a pointer, so both invariants this package depends on are carried by the type.

func (*AgentGrant) IsActive

func (g *AgentGrant) IsActive(now time.Time) bool

IsActive reports whether the grant may authorize a request at now.

type AgentOrigin

type AgentOrigin string

AgentOrigin records how an agent came to exist.

const (
	OriginSelfRegistered AgentOrigin = "self_registered"
	OriginOrgRegistered  AgentOrigin = "org_registered"
	OriginFirstParty     AgentOrigin = "first_party"
)

type AgentStatus

type AgentStatus string

AgentStatus is an agent's approval state.

const (
	StatusPending  AgentStatus = "pending"
	StatusApproved AgentStatus = "approved"
	StatusBlocked  AgentStatus = "blocked"
)

type CreateGrantInput

type CreateGrantInput struct {
	AppID        id.AppID
	AgentID      id.AgentID
	UserID       id.UserID
	OrgID        id.OrgID
	Scopes       []string
	ConsentID    id.ConsentID
	RequestedTTL time.Duration
}

CreateGrantInput describes a delegation a user is about to authorize.

type GrantView

type GrantView struct {
	ID         string     `json:"id"`
	AgentID    string     `json:"agent_id"`
	AgentName  string     `json:"agent_name"`
	Scopes     []string   `json:"scopes"`
	ExpiresAt  time.Time  `json:"expires_at"`
	LastUsedAt *time.Time `json:"last_used_at,omitempty"`
}

GrantView is one delegation as the granting user sees it.

type IssueMeta

type IssueMeta struct {
	IPAddress string
	UserAgent string
	DeviceID  id.DeviceID
}

IssueMeta carries request-scoped metadata onto the minted session. Without it the risk plugins this task exists to inform have nothing to score: impossibletravel returns immediately on an empty IPAddress and geoip returns on a nil lookup, so the hooks would fire but teach those plugins nothing. There are no callers yet, so this shape is free to change.

type ListAgentsRequest

type ListAgentsRequest struct {
	AppID string `query:"app_id,omitempty" description:"Application identifier; must match the caller's own app"`
	OrgID string `query:"org_id,omitempty" description:"Organization identifier; must match the caller's own org"`
}

ListAgentsRequest binds the query parameters for GET /admin/agents. Both fields are optional and, when present, must agree with the caller's own app/org from context — see requestAppMatchesCaller and callerOrgOrReject. Without that check this endpoint let any admin caller enumerate another app's agents just by naming its app_id in the query.

type ListAgentsResponse

type ListAgentsResponse struct {
	Agents []*Agent `json:"agents"`
}

ListAgentsResponse is the GET /admin/agents payload.

type ListGrantsResponse

type ListGrantsResponse struct {
	Grants []GrantView `json:"grants"`
}

ListGrantsResponse is the /v1/me/agents payload.

type MemoryStore

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

MemoryStore is an in-process Store for tests and development.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty in-memory store.

func (*MemoryStore) CreateAgent

func (s *MemoryStore) CreateAgent(_ context.Context, a *Agent) error

func (*MemoryStore) CreateAgentGrant

func (s *MemoryStore) CreateAgentGrant(_ context.Context, g *AgentGrant) error

func (*MemoryStore) GetActiveGrant

func (s *MemoryStore) GetActiveGrant(_ context.Context, agentID id.AgentID, userID id.UserID, orgID id.OrgID) (*AgentGrant, error)

GetActiveGrant returns the most recently created matching grant when more than one exists. Plugin.CreateGrant no longer leaves duplicates lying around in the ordinary case — it now revokes any existing active grant for the same agent+user+org triple before inserting the new one, so an ordinary re-consent leaves exactly one active row. This method's own invariant is still "at most one match may legitimately be active", not "exactly one row exists": a caller writing to this Store directly, without going through Plugin.CreateGrant (this store exposes CreateAgentGrant with no uniqueness enforcement of its own), or two CreateGrant calls racing between the read-existing and revoke-then-insert steps, can still leave more than one matching row simultaneously active. Ranging over s.grants in Go's randomized map-iteration order and returning the first match would make this backend disagree, run to run, with the SQL/Mongo backends' deterministic "newest wins" in that scenario — this scans every match instead of stopping at the first, to agree with them.

func (*MemoryStore) GetAgent

func (s *MemoryStore) GetAgent(_ context.Context, agentID id.AgentID) (*Agent, error)

func (*MemoryStore) GetAgentByClientID

func (s *MemoryStore) GetAgentByClientID(_ context.Context, clientID string) (*Agent, error)

func (*MemoryStore) GetAgentGrant

func (s *MemoryStore) GetAgentGrant(_ context.Context, grantID id.AgentGrantID) (*AgentGrant, error)

func (*MemoryStore) GetOrgPolicy

func (s *MemoryStore) GetOrgPolicy(_ context.Context, orgID id.OrgID) (*OrgAgentPolicy, error)

func (*MemoryStore) ListAgents

func (s *MemoryStore) ListAgents(_ context.Context, appID id.AppID, orgID id.OrgID) ([]*Agent, error)

func (*MemoryStore) ListGrantsByUser

func (s *MemoryStore) ListGrantsByUser(_ context.Context, userID id.UserID) ([]*AgentGrant, error)

func (*MemoryStore) PutOrgPolicy

func (s *MemoryStore) PutOrgPolicy(_ context.Context, p *OrgAgentPolicy) error

func (*MemoryStore) RevokeAgentGrant

func (s *MemoryStore) RevokeAgentGrant(_ context.Context, grantID id.AgentGrantID) error

func (*MemoryStore) RevokeGrantsByAgent

func (s *MemoryStore) RevokeGrantsByAgent(_ context.Context, agentID id.AgentID, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*MemoryStore) RevokeGrantsByOrg

func (s *MemoryStore) RevokeGrantsByOrg(_ context.Context, orgID id.OrgID) ([]id.AgentGrantID, error)

RevokeGrantsByOrg revokes every grant scoped to orgID, regardless of which user issued it. Deleting an organization has to disarm every agent acting under it, not just one member's — that is the whole org's authorization surface, not a single user-org pair the way RevokeGrantsByUserOrg is.

func (*MemoryStore) RevokeGrantsByUser

func (s *MemoryStore) RevokeGrantsByUser(_ context.Context, userID id.UserID) ([]id.AgentGrantID, error)

func (*MemoryStore) RevokeGrantsByUserOrg

func (s *MemoryStore) RevokeGrantsByUserOrg(_ context.Context, userID id.UserID, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*MemoryStore) StampLastUsed

func (s *MemoryStore) StampLastUsed(_ context.Context, grantID id.AgentGrantID, t time.Time) error

StampLastUsed mutates only LastUsedAt/UpdatedAt on the stored grant, leaving RevokedAt and Scopes untouched — see the Store interface doc comment for why that matters.

func (*MemoryStore) UpdateAgent

func (s *MemoryStore) UpdateAgent(_ context.Context, a *Agent) error

func (*MemoryStore) UpdateAgentGrant

func (s *MemoryStore) UpdateAgentGrant(_ context.Context, g *AgentGrant) error

type MongoStore

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

MongoStore implements agentauth.Store using the Grove MongoDB driver.

func NewMongoStore

func NewMongoStore(db *grove.DB) *MongoStore

NewMongoStore creates a new MongoDB-backed agentauth store.

func (*MongoStore) CreateAgent

func (s *MongoStore) CreateAgent(ctx context.Context, a *Agent) error

func (*MongoStore) CreateAgentGrant

func (s *MongoStore) CreateAgentGrant(ctx context.Context, g *AgentGrant) error

func (*MongoStore) GetActiveGrant

func (s *MongoStore) GetActiveGrant(ctx context.Context, agentID id.AgentID, userID id.UserID, orgID id.OrgID) (*AgentGrant, error)

GetActiveGrant returns the most recently created matching grant when more than one exists. See the identical comment on PostgresStore.GetActiveGrant for why duplicates are the normal state and why an explicit order is required for the four backends to agree on which one "the" active grant is.

func (*MongoStore) GetAgent

func (s *MongoStore) GetAgent(ctx context.Context, agentID id.AgentID) (*Agent, error)

func (*MongoStore) GetAgentByClientID

func (s *MongoStore) GetAgentByClientID(ctx context.Context, clientID string) (*Agent, error)

func (*MongoStore) GetAgentGrant

func (s *MongoStore) GetAgentGrant(ctx context.Context, grantID id.AgentGrantID) (*AgentGrant, error)

func (*MongoStore) GetOrgPolicy

func (s *MongoStore) GetOrgPolicy(ctx context.Context, orgID id.OrgID) (*OrgAgentPolicy, error)

func (*MongoStore) ListAgents

func (s *MongoStore) ListAgents(ctx context.Context, appID id.AppID, orgID id.OrgID) ([]*Agent, error)

func (*MongoStore) ListGrantsByUser

func (s *MongoStore) ListGrantsByUser(ctx context.Context, userID id.UserID) ([]*AgentGrant, error)

func (*MongoStore) PutOrgPolicy

func (s *MongoStore) PutOrgPolicy(ctx context.Context, p *OrgAgentPolicy) error

func (*MongoStore) RevokeAgentGrant

func (s *MongoStore) RevokeAgentGrant(ctx context.Context, grantID id.AgentGrantID) error

func (*MongoStore) RevokeGrantsByAgent

func (s *MongoStore) RevokeGrantsByAgent(ctx context.Context, agentID id.AgentID, orgID id.OrgID) ([]id.AgentGrantID, error)

RevokeGrantsByAgent revokes the agent's grants. A nil orgID means "every org", not "the org whose id is empty" — the org_id key is omitted from the filter entirely rather than matched against "", matching MemoryStore.

func (*MongoStore) RevokeGrantsByOrg

func (s *MongoStore) RevokeGrantsByOrg(ctx context.Context, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*MongoStore) RevokeGrantsByUser

func (s *MongoStore) RevokeGrantsByUser(ctx context.Context, userID id.UserID) ([]id.AgentGrantID, error)

func (*MongoStore) RevokeGrantsByUserOrg

func (s *MongoStore) RevokeGrantsByUserOrg(ctx context.Context, userID id.UserID, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*MongoStore) StampLastUsed

func (s *MongoStore) StampLastUsed(ctx context.Context, grantID id.AgentGrantID, t time.Time) error

StampLastUsed writes ONLY last_used_at and updated_at — never revoked_at, which RevokeAgentGrant owns exclusively. See the Store interface doc comment: a targeted $set that never mentions revoked_at cannot resurrect a grant a concurrent revoke just set it on, the way a whole-document ReplaceOne built from a stale read (UpdateAgentGrant) could.

func (*MongoStore) UpdateAgent

func (s *MongoStore) UpdateAgent(ctx context.Context, a *Agent) error

func (*MongoStore) UpdateAgentGrant

func (s *MongoStore) UpdateAgentGrant(ctx context.Context, g *AgentGrant) error

type Option

type Option func(*Plugin)

Option configures the plugin at construction.

func WithDefaultGrantTTL

func WithDefaultGrantTTL(d time.Duration) Option

WithDefaultGrantTTL sets the fallback cap on grant lifetime. An org policy with a shorter MaxGrantTTL still wins.

func WithScope

func WithScope(scope string, p Permission) Option

WithScope maps a delegation scope onto a Warden permission. A scope with no mapping is rejected at consent time.

func WithStore

func WithStore(s Store) Option

WithStore injects a persistent store. Without it the plugin uses an in-memory store. A nil s is a no-op rather than installing a nil store: Store is an interface, so pl.store = nil would leave every store call in the plugin (Authorize's grant read included, at the very first thing an agent request does) calling a method on a nil interface value, which panics rather than erroring. New() already sets a working default before any Option runs, so there is nothing useful a nil argument here could mean other than "leave it alone".

type OrgAgentPolicy

type OrgAgentPolicy struct {
	OrgID         id.OrgID      `json:"org_id"`
	Mode          PolicyMode    `json:"mode"`
	MaxGrantTTL   time.Duration `json:"max_grant_ttl"`
	AllowedScopes []string      `json:"allowed_scopes,omitempty"`
}

OrgAgentPolicy is an org's gate on agent delegation.

type Permission

type Permission struct {
	Action   string
	Resource string
}

Permission is the Warden action and resource a delegation scope maps to.

func Grants

func Grants(action, resource string) Permission

Grants builds the Permission a scope confers. It reads as Grants("read", "invoice") at the call site.

type Plugin

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

Plugin is the delegated agent identity plugin.

func New

func New(opts ...Option) *Plugin

New creates the agentauth plugin.

func (*Plugin) Authorize

func (p *Plugin) Authorize(ctx context.Context, sess *session.Session, action, resource string) error

Authorize enforces the intersection: an agent may do something only if its grant confers it AND the delegating user can do it. A non-agent session passes straight through, since this plugin has no opinion on human traffic.

The scope gate runs first. It is cheaper, a map lookup ahead of a Warden call, and it is the safe order: reporting a user-gate failure first would let an agent enumerate its owner's permissions one probe at a time.

func (*Plugin) AuthorizeHTTP

func (p *Plugin) AuthorizeHTTP(ctx context.Context, sess *session.Session, action, resource string) error

AuthorizeHTTP wraps Authorize and maps its sentinels onto the responses an agent developer can act on.

The scope case names the scope that would satisfy the route, which is safe because it describes the route rather than the owner. Every other failure — an inactive grant aside, which is a distinct re-authorize signal — stays opaque on purpose: reporting a user-gate refusal in the same shape as an RBAC outage or any other denial would let an agent enumerate its owner's permissions one request at a time.

The returned error's headers (the WWW-Authenticate on the 401 and 403 cases) only reach the client through Guard, or through a caller that reads HeaderOf itself and sets them by hand — forge has no header support on any error type it recognizes (see httpError's doc comment), so a caller that returns this error directly from a route handler gets the right status and body but a silently dropped header.

func (*Plugin) CreateGrant

func (p *Plugin) CreateGrant(ctx context.Context, in CreateGrantInput) (*AgentGrant, error)

CreateGrant writes the delegation. It refuses a grant with no delegating human, which is the invariant the whole authorization model rests on. It also re-runs the same policy check Evaluate performs, once per governing org, rather than trusting that every caller ran Evaluate first: CreateGrant is exported, so nothing at the type level stops a caller from reaching it directly, and the invariant that a scope or a blocked policy never reaches a stored grant has to hold regardless of call order.

func (*Plugin) DefaultGrantTTL

func (p *Plugin) DefaultGrantTTL() time.Duration

DefaultGrantTTL returns the fallback cap on grant lifetime.

func (*Plugin) Evaluate

func (p *Plugin) Evaluate(ctx context.Context, clientID string, _ id.UserID, orgID id.OrgID, appID id.AppID, scopes []string) error

Evaluate implements oauth2provider.ConsentGate. It runs at the moment a user consents, which is the only point where an org's stance on a given agent is a well-formed question. Registration is app-global.

func (*Plugin) Guard

func (p *Plugin) Guard(action, resource string) forge.Middleware

Guard returns middleware enforcing the agent intersection for a route that requires action on resource. It resolves the session with middleware.SessionFrom — the whole *session.Session, not just a user id, since Authorize needs PrincipalKind, AgentID and GrantID off it, which is why this is not middleware/rbac.go's UserIDFrom (rbac.go's two middlewares only ever need the id). It then delegates the decision entirely to AuthorizeHTTP: a human session (or a request AuthorizeHTTP otherwise has no opinion on) passes through untouched, and every denial gets the response AuthorizeHTTP built for it, headers included, since forge's own error handling would otherwise drop them on the floor.

func (*Plugin) IssueAgentSession

func (p *Plugin) IssueAgentSession(ctx context.Context, grant *AgentGrant, meta IssueMeta) (*session.Session, error)

IssueAgentSession mints a session for an agent acting under grant.

Issuance deliberately goes through the engine's normal store and hook path so BeforeSessionCreate and AfterSignIn fire. That is what puts agent traffic in front of riskengine, impossibletravel, ipreputation and the rest. The API key plugin builds a synthetic session by hand and fires neither, which is why its traffic is invisible to all of them. Do not copy that shape here.

riskengine's own OnBeforeSessionCreate is a documented no-op today, so nothing actually vetoes an agent session through this hook yet. What this function guarantees is the mechanism: an emit that returns an error and aborts the mint before anything is persisted, so whichever plugin later implements the veto can rely on it working. See TestIssueAgentSession_VetoBlocksPersistence.

func (*Plugin) ListMyGrants

func (p *Plugin) ListMyGrants(ctx context.Context, userID id.UserID) (*ListGrantsResponse, error)

ListMyGrants returns every active delegation the user has issued.

func (*Plugin) MigrationGroups

func (p *Plugin) MigrationGroups(driverName string) []*migrate.Group

MigrationGroups returns the agentauth migration groups for the given driver.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin name.

func (*Plugin) OnAfterOrgDelete

func (p *Plugin) OnAfterOrgDelete(ctx context.Context, orgID id.OrgID) error

OnAfterOrgDelete revokes every grant scoped to the deleted organization, regardless of which user issued it. organization.Plugin.DeleteOrganization cascades member deletion without emitting BeforeMemberRemove (it deletes members as part of its own atomic cascade, not one at a time through RemoveMember), so OnBeforeMemberRemove never sees an org's members leave when the org itself is deleted. Without this hook, deleting an org would leave every grant scoped to it live until each one separately expired — the same offboarding hole member removal closes, just at the org level.

func (*Plugin) OnAfterUserDelete

func (p *Plugin) OnAfterUserDelete(ctx context.Context, userID id.UserID) error

OnAfterUserDelete revokes every grant a deleted user issued.

func (*Plugin) OnAfterUserUpdate

func (p *Plugin) OnAfterUserUpdate(ctx context.Context, u *user.User) error

OnAfterUserUpdate revokes every grant a user issued once they are banned. Sessions die separately through DeleteUserSessions; the grant has to go too, or the next refresh mints a replacement session. OnAfterUserUpdate fires on every user update, ordinary profile edits included, so it must check the ban flag rather than acting unconditionally.

func (*Plugin) OnBeforeMemberRemove

func (p *Plugin) OnBeforeMemberRemove(ctx context.Context, m *organization.Member) error

OnBeforeMemberRemove revokes the leaving member's grants in that org only. Their grants in the user's other orgs are untouched: leaving one organization must not disarm agents acting for the user elsewhere.

func (*Plugin) OnInit

func (p *Plugin) OnInit(_ context.Context, engine plugin.Engine) error

OnInit captures engine capabilities.

func (*Plugin) RegisterAgent

func (p *Plugin) RegisterAgent(ctx context.Context, in *Agent) (*Agent, error)

RegisterAgent creates a new org-registered agent. An org admin registering an agent has already decided to trust it, so it starts approved, unlike a self-registered agent (created elsewhere, outside this task's scope) which would start pending review.

ClientID must be unique: Evaluate resolves an agent by ClientID through GetAgentByClientID, and a store that let two agents share one would make that resolution nondeterministic — whichever record a range-based lookup happens to visit first decides whether a blocked agent's client is treated as blocked. CreateAgent enforces the invariant; this maps its ErrConflict onto an HTTP 409 rather than the generic 500 forge.InternalError would give every other store failure.

func (*Plugin) RegisterRoutes

func (p *Plugin) RegisterRoutes(router forge.Router) error

RegisterRoutes registers the user and admin surfaces: a place for a user to see and revoke which agents are acting for them, and an admin surface for registering and blocking agents org-wide.

func (*Plugin) RevokeGrant

func (p *Plugin) RevokeGrant(ctx context.Context, grantID id.AgentGrantID) error

RevokeGrant revokes a delegation, drops it from the cache so the next request sees the revocation without waiting out the cache ttl, and deletes the sessions it issued.

The last part closes a gap the design promised and nothing implemented: an agent session carries the delegating human's UserID (session.GrantID names the grant that authorized it), and the engine's role and permission middleware are not principal-kind aware. Without this, a live post-revocation session would keep authenticating as that human on any route not guarded by agentauth.Authorize, until the session separately expired.

func (*Plugin) RevokeMyGrant

func (p *Plugin) RevokeMyGrant(ctx context.Context, userID id.UserID, grantID id.AgentGrantID) error

RevokeMyGrant revokes one of the caller's own delegations. It refuses a grant belonging to anybody else, and returns the same response for that case as for a grant that does not exist at all, so a grant id is never an authorization: the endpoint cannot be used to probe whether a given grant id is real.

func (*Plugin) Scopes

func (p *Plugin) Scopes() *ScopeRegistry

Scopes returns the delegation scope registry.

func (*Plugin) SetAgentStatus

func (p *Plugin) SetAgentStatus(ctx context.Context, agentID id.AgentID, orgID id.OrgID, status AgentStatus) error

SetAgentStatus changes an agent's approval state. Blocking also revokes every grant the agent holds in that org, since leaving them live would mean a blocked agent kept working until its sessions aged out, and deletes the sessions those grants issued: without that, a live agent session (which carries the delegating human's UserID) keeps authenticating as that human on any route not guarded by agentauth.Authorize, for up to its remaining TTL, even though the agent that session belongs to has just been blocked.

func (*Plugin) SetLogger

func (p *Plugin) SetLogger(l log.Logger)

SetLogger injects the logger AuthorizeHTTP's default branch uses to record an authorization check failure that isn't one of Authorize's named sentinels (a store or RBAC transport error). OnInit does this from the engine; tests use it directly, mirroring SetPermissionChecker above.

func (*Plugin) SetPermissionChecker

func (p *Plugin) SetPermissionChecker(pc plugin.PermissionChecker)

SetPermissionChecker injects the RBAC checker. OnInit does this from the engine; tests use it directly.

func (*Plugin) Store

func (p *Plugin) Store() Store

Store returns the plugin's store.

type PolicyMode

type PolicyMode string

PolicyMode is an org's stance on agent delegation.

const (
	ModeOpen      PolicyMode = "open"
	ModeAllowlist PolicyMode = "allowlist"
	ModeBlocked   PolicyMode = "blocked"
)

type PostgresStore

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

PostgresStore implements agentauth.Store using the Grove ORM with PostgreSQL.

func NewPostgresStore

func NewPostgresStore(db *grove.DB) *PostgresStore

NewPostgresStore creates a new PostgreSQL-backed agentauth store.

func (*PostgresStore) CreateAgent

func (s *PostgresStore) CreateAgent(ctx context.Context, a *Agent) error

func (*PostgresStore) CreateAgentGrant

func (s *PostgresStore) CreateAgentGrant(ctx context.Context, g *AgentGrant) error

func (*PostgresStore) GetActiveGrant

func (s *PostgresStore) GetActiveGrant(ctx context.Context, agentID id.AgentID, userID id.UserID, orgID id.OrgID) (*AgentGrant, error)

GetActiveGrant returns the most recently created matching grant when more than one exists. Duplicates are the normal state, not an edge case: CreateGrant always inserts a fresh grant rather than upserting, so an ordinary re-consent leaves an older active grant for the same agent+user+org triple lying around alongside the new one. Without an explicit order, which row a plain "one matching row" query returns is backend-specific — Postgres/SQLite/Mongo have no defined tie-break and MemoryStore's map iteration is randomized — so the four backends would disagree about which grant is "the" active one. Ordering by CreatedAt DESC makes all four agree: the newest grant wins.

func (*PostgresStore) GetAgent

func (s *PostgresStore) GetAgent(ctx context.Context, agentID id.AgentID) (*Agent, error)

func (*PostgresStore) GetAgentByClientID

func (s *PostgresStore) GetAgentByClientID(ctx context.Context, clientID string) (*Agent, error)

func (*PostgresStore) GetAgentGrant

func (s *PostgresStore) GetAgentGrant(ctx context.Context, grantID id.AgentGrantID) (*AgentGrant, error)

func (*PostgresStore) GetOrgPolicy

func (s *PostgresStore) GetOrgPolicy(ctx context.Context, orgID id.OrgID) (*OrgAgentPolicy, error)

func (*PostgresStore) ListAgents

func (s *PostgresStore) ListAgents(ctx context.Context, appID id.AppID, orgID id.OrgID) ([]*Agent, error)

func (*PostgresStore) ListGrantsByUser

func (s *PostgresStore) ListGrantsByUser(ctx context.Context, userID id.UserID) ([]*AgentGrant, error)

func (*PostgresStore) PutOrgPolicy

func (s *PostgresStore) PutOrgPolicy(ctx context.Context, p *OrgAgentPolicy) error

func (*PostgresStore) RevokeAgentGrant

func (s *PostgresStore) RevokeAgentGrant(ctx context.Context, grantID id.AgentGrantID) error

func (*PostgresStore) RevokeGrantsByAgent

func (s *PostgresStore) RevokeGrantsByAgent(ctx context.Context, agentID id.AgentID, orgID id.OrgID) ([]id.AgentGrantID, error)

RevokeGrantsByAgent revokes the agent's grants. A nil orgID means "every org", not "the org whose id is empty" — the org filter is only applied when orgID carries a real value, matching MemoryStore.

func (*PostgresStore) RevokeGrantsByOrg

func (s *PostgresStore) RevokeGrantsByOrg(ctx context.Context, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*PostgresStore) RevokeGrantsByUser

func (s *PostgresStore) RevokeGrantsByUser(ctx context.Context, userID id.UserID) ([]id.AgentGrantID, error)

func (*PostgresStore) RevokeGrantsByUserOrg

func (s *PostgresStore) RevokeGrantsByUserOrg(ctx context.Context, userID id.UserID, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*PostgresStore) StampLastUsed

func (s *PostgresStore) StampLastUsed(ctx context.Context, grantID id.AgentGrantID, t time.Time) error

StampLastUsed writes ONLY last_used_at and updated_at — never revoked_at, which RevokeAgentGrant owns exclusively. See the Store interface doc comment: a targeted UPDATE that never mentions revoked_at cannot resurrect a grant a concurrent revoke just set it on, the way a whole-row UpdateAgentGrant built from a stale read could.

func (*PostgresStore) UpdateAgent

func (s *PostgresStore) UpdateAgent(ctx context.Context, a *Agent) error

func (*PostgresStore) UpdateAgentGrant

func (s *PostgresStore) UpdateAgentGrant(ctx context.Context, g *AgentGrant) error

type PutOrgPolicyRequest

type PutOrgPolicyRequest struct {
	OrgID             string    `json:"org_id" description:"Organization identifier; must match the caller's own org"`
	Mode              string    `json:"mode" description:"open, allowlist or blocked"`
	MaxGrantTTLSecond *int64    `json:"max_grant_ttl_seconds,omitempty" description:"Must not be negative. Omit to leave the existing ceiling unchanged."`
	AllowedScopes     *[]string `` /* 137-byte string literal not displayed */
}

PutOrgPolicyRequest binds the body for PUT /admin/agents/policy. OrgID is required and must match the caller's own org from context — see requiredCallerOrgID. Without that check, any admin caller holding "write agent" in any org could flip a different org's delegation policy wide open, since HasPermission carries no org dimension of its own.

MaxGrantTTLSecond and AllowedScopes are pointers, not bare values: this is a partial-update endpoint (see handleGetOrgPolicy, which exists so an admin can read the current policy before changing one field of it), and a bare int64/[]string cannot distinguish "the caller omitted this field, leave it alone" from "the caller explicitly sent its zero value" — both decode to the same zero value with ordinary JSON unmarshaling. That distinction matters here specifically because both fields fail OPEN at their zero value: clampTTL (grant.go) treats a non-positive MaxGrantTTL as "no ceiling", and scopeAllowed treats an empty AllowedScopes as unrestricted. Before this, every PUT was a full overwrite, so a mode-only request — the natural way to flip just ModeBlocked on or off — silently reset both to unlimited/unrestricted.

type RegisterAgentRequest

type RegisterAgentRequest struct {
	AppID       string `json:"app_id,omitempty" description:"Application identifier; must match the caller's own app"`
	OrgID       string `json:"org_id,omitempty" description:"Organization identifier; must match the caller's own org"`
	ClientID    string `json:"client_id" description:"The oauth2provider client id this agent authenticates as"`
	Name        string `json:"name" description:"Human-readable agent name"`
	Description string `json:"description,omitempty"`
	LogoURI     string `json:"logo_uri,omitempty"`
}

RegisterAgentRequest binds the body for POST /admin/agents. AppID and OrgID are optional and, when present, must agree with the caller's own app/org from context (see requestAppMatchesCaller and callerOrgOrReject) — the agent is always registered under the caller's actual app regardless.

type RevokeMyGrantRequest

type RevokeMyGrantRequest struct {
	GrantID string `path:"id" description:"Agent grant identifier"`
}

RevokeMyGrantRequest binds the path parameter for DELETE /me/agents/:id.

type ScopeRegistry

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

ScopeRegistry holds the host app's delegation vocabulary. Scopes are type-level: "invoices:read" means invoices, not one customer's invoices. Instance narrowing comes from the user gate, where Warden's ReBAC has already decided which invoices the delegating user may read.

func NewScopeRegistry

func NewScopeRegistry() *ScopeRegistry

NewScopeRegistry returns an empty registry.

func (*ScopeRegistry) Covers

func (r *ScopeRegistry) Covers(scopes []string, action, resource string) bool

Covers reports whether any of the granted scopes confers the given permission. An unregistered scope confers nothing.

func (*ScopeRegistry) Known

func (r *ScopeRegistry) Known(scope string) bool

Known reports whether a scope has a registered mapping. Consent uses this to reject unmapped scopes before a grant is written, so a stored grant can never carry a scope that means nothing.

func (*ScopeRegistry) Lookup

func (r *ScopeRegistry) Lookup(scope string) (Permission, bool)

Lookup returns the permission a scope confers.

func (*ScopeRegistry) Register

func (r *ScopeRegistry) Register(scope string, p Permission)

Register maps a delegation scope onto a Warden permission. Re-registering a scope replaces its mapping.

type SetAgentStatusRequest

type SetAgentStatusRequest struct {
	AgentID string `path:"id" description:"Agent identifier"`
	OrgID   string `` /* 136-byte string literal not displayed */
	Status  string `json:"status" description:"pending, approved or blocked"`
}

SetAgentStatusRequest binds the path and body for PATCH /admin/agents/:id/status.

type SqliteStore

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

SqliteStore implements agentauth.Store using the Grove ORM with SQLite.

func NewSqliteStore

func NewSqliteStore(db *grove.DB) *SqliteStore

NewSqliteStore creates a new SQLite-backed agentauth store.

func (*SqliteStore) CreateAgent

func (s *SqliteStore) CreateAgent(ctx context.Context, a *Agent) error

func (*SqliteStore) CreateAgentGrant

func (s *SqliteStore) CreateAgentGrant(ctx context.Context, g *AgentGrant) error

func (*SqliteStore) GetActiveGrant

func (s *SqliteStore) GetActiveGrant(ctx context.Context, agentID id.AgentID, userID id.UserID, orgID id.OrgID) (*AgentGrant, error)

GetActiveGrant returns the most recently created matching grant when more than one exists. See the identical comment on PostgresStore.GetActiveGrant for why duplicates are the normal state and why an explicit order is required for the four backends to agree on which one "the" active grant is.

func (*SqliteStore) GetAgent

func (s *SqliteStore) GetAgent(ctx context.Context, agentID id.AgentID) (*Agent, error)

func (*SqliteStore) GetAgentByClientID

func (s *SqliteStore) GetAgentByClientID(ctx context.Context, clientID string) (*Agent, error)

func (*SqliteStore) GetAgentGrant

func (s *SqliteStore) GetAgentGrant(ctx context.Context, grantID id.AgentGrantID) (*AgentGrant, error)

func (*SqliteStore) GetOrgPolicy

func (s *SqliteStore) GetOrgPolicy(ctx context.Context, orgID id.OrgID) (*OrgAgentPolicy, error)

func (*SqliteStore) ListAgents

func (s *SqliteStore) ListAgents(ctx context.Context, appID id.AppID, orgID id.OrgID) ([]*Agent, error)

func (*SqliteStore) ListGrantsByUser

func (s *SqliteStore) ListGrantsByUser(ctx context.Context, userID id.UserID) ([]*AgentGrant, error)

func (*SqliteStore) PutOrgPolicy

func (s *SqliteStore) PutOrgPolicy(ctx context.Context, p *OrgAgentPolicy) error

func (*SqliteStore) RevokeAgentGrant

func (s *SqliteStore) RevokeAgentGrant(ctx context.Context, grantID id.AgentGrantID) error

func (*SqliteStore) RevokeGrantsByAgent

func (s *SqliteStore) RevokeGrantsByAgent(ctx context.Context, agentID id.AgentID, orgID id.OrgID) ([]id.AgentGrantID, error)

RevokeGrantsByAgent revokes the agent's grants. A nil orgID means "every org", not "the org whose id is empty" — the org filter is only applied when orgID carries a real value, matching MemoryStore.

func (*SqliteStore) RevokeGrantsByOrg

func (s *SqliteStore) RevokeGrantsByOrg(ctx context.Context, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*SqliteStore) RevokeGrantsByUser

func (s *SqliteStore) RevokeGrantsByUser(ctx context.Context, userID id.UserID) ([]id.AgentGrantID, error)

func (*SqliteStore) RevokeGrantsByUserOrg

func (s *SqliteStore) RevokeGrantsByUserOrg(ctx context.Context, userID id.UserID, orgID id.OrgID) ([]id.AgentGrantID, error)

func (*SqliteStore) StampLastUsed

func (s *SqliteStore) StampLastUsed(ctx context.Context, grantID id.AgentGrantID, t time.Time) error

StampLastUsed writes ONLY last_used_at and updated_at — never revoked_at, which RevokeAgentGrant owns exclusively. See the Store interface doc comment: a targeted UPDATE that never mentions revoked_at cannot resurrect a grant a concurrent revoke just set it on, the way a whole-row UpdateAgentGrant built from a stale read could.

func (*SqliteStore) UpdateAgent

func (s *SqliteStore) UpdateAgent(ctx context.Context, a *Agent) error

func (*SqliteStore) UpdateAgentGrant

func (s *SqliteStore) UpdateAgentGrant(ctx context.Context, g *AgentGrant) error

type StatusResponse

type StatusResponse struct {
	Status string `json:"status"`
}

StatusResponse is a generic status response for actions with no body worth naming, matching the shape used elsewhere across the plugin surfaces.

type Store

type Store interface {
	CreateAgent(ctx context.Context, a *Agent) error
	GetAgent(ctx context.Context, agentID id.AgentID) (*Agent, error)
	GetAgentByClientID(ctx context.Context, clientID string) (*Agent, error)
	UpdateAgent(ctx context.Context, a *Agent) error
	ListAgents(ctx context.Context, appID id.AppID, orgID id.OrgID) ([]*Agent, error)

	CreateAgentGrant(ctx context.Context, g *AgentGrant) error
	GetAgentGrant(ctx context.Context, grantID id.AgentGrantID) (*AgentGrant, error)
	GetActiveGrant(ctx context.Context, agentID id.AgentID, userID id.UserID, orgID id.OrgID) (*AgentGrant, error)
	ListGrantsByUser(ctx context.Context, userID id.UserID) ([]*AgentGrant, error)
	UpdateAgentGrant(ctx context.Context, g *AgentGrant) error
	// StampLastUsed records that grantID was used at t, touching ONLY
	// LastUsedAt and UpdatedAt — never RevokedAt, Scopes or any other field.
	// IssueAgentSession uses this instead of a read-modify-write through
	// UpdateAgentGrant specifically because UpdateAgentGrant replaces the
	// whole row: a grant read at the start of issuance and stamped after the
	// session is persisted can have been revoked in between, and writing
	// that stale copy back would silently restore RevokedAt to nil (and the
	// pre-revocation Scopes with it). A targeted update can't do that,
	// because it never reads RevokedAt in the first place.
	StampLastUsed(ctx context.Context, grantID id.AgentGrantID, t time.Time) error
	RevokeAgentGrant(ctx context.Context, grantID id.AgentGrantID) error
	// RevokeGrantsByUser, RevokeGrantsByUserOrg, RevokeGrantsByOrg and
	// RevokeGrantsByAgent each return the ids of the grants matched by their
	// query (not necessarily newly revoked — an already-revoked grant that
	// still matches is included too), so the caller can sweep the sessions
	// those grants issued through session.Store.DeleteSessionsByGrant.
	// Sweeping an already-revoked grant's sessions again is harmless and, if
	// anything, safer: the alternative of narrowing to only-just-revoked ids
	// risks under-sweeping. Without the sweep at all, a bulk revocation
	// (banning a user, a member leaving an org, an org being deleted, an
	// agent being blocked) would revoke the grant but leave any session it
	// already minted live — carrying the delegating human's UserID — until
	// the session separately expired.
	RevokeGrantsByUser(ctx context.Context, userID id.UserID) ([]id.AgentGrantID, error)
	RevokeGrantsByUserOrg(ctx context.Context, userID id.UserID, orgID id.OrgID) ([]id.AgentGrantID, error)
	RevokeGrantsByOrg(ctx context.Context, orgID id.OrgID) ([]id.AgentGrantID, error)
	RevokeGrantsByAgent(ctx context.Context, agentID id.AgentID, orgID id.OrgID) ([]id.AgentGrantID, error)

	GetOrgPolicy(ctx context.Context, orgID id.OrgID) (*OrgAgentPolicy, error)
	PutOrgPolicy(ctx context.Context, p *OrgAgentPolicy) error
}

Store persists agents, grants and org policy.

Jump to

Keyboard shortcuts

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