Skip to Content
ConceptsAuthorization Model

Authorization Model

1. Overview

AgentTrust ID 1.0 is a unified, protocol-agnostic authorization layer for AI agents. It provides session-based privilege management, effect classification, 3-tier Guardian evaluation, and human-in-the-loop elevation approval. Live MCP and A2A requests are mediated through the shared UnifiedChecker, and API JWTs plus federated identity tokens can be bridged into AgentTrust ID sessions at runtime before subsequent checks flow through the same core.

Key invariants:

  • Sessions default to read_only. If no default mode is specified, the session starts in read_only mode.
  • Scope ceilings are immutable. Set once at session creation, never widened.
  • Destructive actions fail closed. If the Guardian pipeline is unavailable, destructive and admin actions are denied.
  • Elevation is time-boxed. Maximum 5 minutes per elevation grant.
  • Unknown actions default to mutating. Safe default that requires higher guardian scrutiny.

2. Architecture

See the Architecture page for the full system diagram.

Core Files

FilePackageResponsibility
internal/agenttrust/types.goagenttrustAll type definitions: AgentTrustRequest, AgentTrustResponse, SessionState, ApprovalRequest, InitSessionRequest, RedisClient interface
internal/agenttrust/check.goagenttrustUnifiedChecker.Check() — single entry point for all authorization decisions
internal/agenttrust/session.goagenttrustSessionManager — session CRUD, elevation, metrics recording, auto-revert
internal/agenttrust/approval.goagenttrustApprovalGate — pending elevation request lifecycle (request, approve, deny)
internal/agenttrust/effect.goagenttrustEffectClassifier — pattern-based action classification into read/mutating/destructive/admin
internal/agenttrust/guardian.goagenttrustGuardianClient — HTTP calls to Fast Guard (auth service) and Guardian Router (Spot/Deep)
internal/agenttrust/api_adapter.goagenttrustAPIAdapter — translates JWT claims into AgentTrust ID sessions and checks
internal/mcp/proxy.gomcpProxy.Forward() — MCP JSON-RPC reverse proxy with AgentTrust ID integration
internal/a2a/adapter.goa2aA2AAdapter — translates A2A delegation contexts into AgentTrust ID sessions and checks
internal/mcp/scope_enforcer.gomcpScopeEnforcer — OAuth token scope storage/retrieval and scope validation helpers

3. Unified Check Flow

UnifiedChecker.Check() in internal/agenttrust/check.go is the single authorization entry point for mediated AgentTrust ID checks. MCP and live A2A requests call it directly, and the API/federation runtime bridges create sessions that are then checked through the same path.

Step-by-Step Walkthrough

Step 1: Effect Classification

if req.ActionEffect == "" { req.ActionEffect = uc.classifier.ClassifyWithOverride(req.ActionName, req.EffectOverride) }

If the protocol adapter did not pre-classify the effect, the EffectClassifier analyzes the action name using pattern matching (see Section 5). If the request carries an EffectOverride (from a per-tool annotation), that takes precedence over pattern matching. This means adapters can either pre-classify, supply a per-tool override, or defer entirely to the checker.

Step 2: Session Loading

if req.SessionID != "" { session, err = uc.sessions.Get(ctx, req.OrgID, req.SessionID) }

Sessions are loaded from Redis by key agenttrust_session:{org_id}:{session_id}. The key is org-scoped to prevent cross-tenant session access. If the session does not exist, the check proceeds without session-level enforcement (stateless mode).

Step 3: Session-Level Checks

When a session exists, four checks are applied in order:

3a. Session owner binding: If the session has an AgentID, requests that omit AgentID inherit it from the session. Requests that present a different AgentID are denied immediately with guard_tier: "session".

3b. Scope ceiling check: If the session has a non-empty ScopeCeiling, the ActionName must appear in that list. Violation results in immediate denial with guard_tier: "session".

3c. Allowed actions check: If the session has a non-empty AllowedActions list, the ActionName must appear in that list. This is a potentially narrower subset of the scope ceiling.

3d. Mode vs. effect check: If session.Mode == "read_only" and the action effect is not "read":

  • First, check if an active elevation covers this specific action (elevation not expired and action in ElevationScope). If so, proceed.
  • For mutating or destructive effects without active elevation: create an ApprovalRequest with status "pending" and return ElevationRequired: true with an ApprovalID. The agent must obtain approval before retrying.
  • For admin effects without elevation: denied outright.

Step 4: Guardian Pipeline Routing

The action is routed to the appropriate Guardian tier(s) based on its effect classification (see Section 6 for details).

Step 5: Session Recording

if session != nil { uc.sessions.RecordCall(ctx, session, req.ActionEffect, guardResp.Allowed) }

Session metrics are updated: TotalCalls, ReadCalls/WriteCalls, DeniedCalls. If an elevation has expired, the session auto-reverts to read_only during this step. The updated session is saved to Redis (best-effort, non-blocking).

4. Session Lifecycle

Initialization

Sessions are created via SessionManager.InitSession(). Initial mode depends on the default_mode configuration:

  • scoped mode allows actions within the scope ceiling without requiring elevation.
  • read_only mode requires elevation for non-read actions.
  • If no default mode is provided, read_only is applied automatically.
state := &SessionState{ Mode: initialMode, ... }

The InitSessionRequest fields determine the session’s scope:

FieldMCP SourceA2A SourceAPI Source
Source"mcp""a2a""api"
ServerIDMCP server ID
DelegationIDDelegation token ID
ParentAgentIDDelegating agent ID
ScopeCeilingServer’s registered toolsDelegation scopeJWT Scope claim
AllowedActionsServer’s registered toolsDelegation scopeJWT Scope claim

State Machine

A session starts in read_only (default) or scoped depending on the configured default_mode. From read_only, a non-read action is denied with an approval_id; an approval elevates the session, and the elevation reverts to read_only when its TTL expires (or stays read_only if the approval is denied or expires).

Session state machine Start transitions into read_only (default_mode read_only) or scoped (default_mode scoped). From read_only a non-read action moves to DENIED with an approval_id; an approved request elevates the session; the elevation reverts to read_only when its TTL expires; a denied or expired approval returns to read_only. start read_only default mode scoped default_mode = scoped DENIED returns approval_id elevated time-boxed grant default_mode = read_only default_mode = scoped non-read action (approval_id) approval granted approval denied or expires elevation TTL expires

Mode Transitions

TransitionTriggerMechanism
read_only -> DENIEDNon-read action attemptedCheck() step 3c returns ElevationRequired
DENIED -> elevatedApproval grantedApprovalGate.Approve() calls SessionManager.Elevate()
elevated -> read_onlyTTL expiresRecordCall() checks ElevatedUntil on each call
DENIED -> read_onlyApproval denied or expiresNo state change needed (session was already read_only)

Scope Ceiling

The scope ceiling is immutable after session creation. It represents the maximum set of actions a session can ever access, even with elevation. The Elevate() method enforces this:

if len(state.ScopeCeiling) > 0 { for _, s := range scope { if !contains(state.ScopeCeiling, s) { return fmt.Errorf("requested elevation scope %q exceeds session ceiling", s) } } }

Elevation

Elevation grants time-boxed access to specific action(s):

  • Maximum grant duration: 5 minutes (hard cap in Elevate())
  • Default approval grant: 5 minutes (set in ApprovalGate.Approve())
  • Scope: Only the specific action(s) approved, not the full ceiling
  • Auto-revert: On each RecordCall(), if time.Now().After(*state.ElevatedUntil), the session reverts to read_only and clears ElevationScope

Metrics

Each session tracks call counts for behavioral analysis and anomaly detection:

FieldIncremented When
TotalCallsEvery RecordCall() invocation
ReadCallsEffect is "read"
WriteCallsEffect is "mutating", "destructive", or "admin"
DeniedCallsallowed == false

Storage

  • Key format: agenttrust_session:{org_id}:{session_id} (org-scoped to prevent cross-tenant access)
  • TTL: 1 hour sliding window, refreshed on every Save()
  • Serialization: JSON via encoding/json
  • Integrity: HMAC-SHA256 signature computed on save, verified on load (prevents tampering)
  • Backend: Redis (via RedisClient interface)

5. Effect Classification

The EffectClassifier in internal/agenttrust/effect.go categorizes actions by matching the action name (case-insensitive) against pattern lists. Patterns are checked in priority order: destructive first, then admin, mutating, and finally read.

EffectPatternsRisk Level
destructivedelete, drop, destroy, purge, terminate, remove, truncateHigh
adminadmin, transfer_ownership, revoke, escalate, grant, impersonateCritical
mutatingwrite, update, create, execute, invoke, modify, send, put, post, commit, push, deployMedium
readget, list, read, describe, search, view, fetch, query, headLow

Classification Rules

  • Matching is case-insensitive (strings.ToLower applied first)
  • Matching uses strings.Contains, so file_delete matches destructive and admin_list matches admin
  • Priority order ensures the most dangerous classification wins: an action named delete_admin classifies as destructive (not admin)
  • Unknown actions default to "mutating" — this is a safe default that routes through a higher guardian tier than read

Examples

Action NameClassified EffectMatched Pattern
web_searchreadsearch
file_writemutatingwrite
database_drop_tabledestructivedrop
grant_permissionadmingrant
custom_toolmutating(no match — default)
list_usersreadlist
send_emailmutatingsend
remove_filedestructiveremove

Per-Tool Effect Override

MCP server operators can declare a tool’s effect at registration time using the EffectOverride field in ToolDefinition. When set, this overrides pattern-based classification.

The ClassifyWithOverride(actionName, override) method validates the override is one of the four recognized categories (read, mutating, destructive, admin). Invalid overrides fall through to pattern-based classification.

Additionally, the RequireApproval field on ToolDefinition forces human-in-the-loop approval for any non-read invocation of that tool, regardless of session mode or guardian result.

Session Default Mode

Each MCP server can declare a default_mode that controls the initial session mode for agents:

ModeSession ModeGuardian PipelineHuman Approval
read_only (default)read_onlyFull (effect-based routing)Required for non-read actions (via elevation)
scopedscopedFull (effect-based routing)Only for tools with require_approval: true

Every action always flows through the Guardian pipeline. There is no configuration that bypasses Guardian.

6. Guardian Routing by Effect

After session-level checks pass, the UnifiedChecker routes the action to the Guardian pipeline based on its effect classification.

Routing Table

EffectPipelineOn Fast Guard DenyOn Guardian Unavailable
readFast Guard onlyDenied (no escalation)N/A (only Fast Guard used)
mutatingFast Guard -> Guardian Router (Spot)Denied (no escalation)Falls back to Fast Guard result
destructiveFast Guard -> Guardian Router (Spot -> Deep)Denied (no escalation)Fails closed (denied)
adminFast Guard -> Guardian Router (Spot -> Deep)Denied (no escalation)Fails closed (denied)
Unknown action nameClassified as mutating, then follows mutating pathDenied (no escalation)Falls back to Fast Guard result

Routing Logic Detail

From check.go, the routing follows this decision tree:

Read actions:

FastGuard(req) -> return result

Mutating actions:

FastGuard(req) -> if denied: return denied (no escalation) -> if allowed AND HasGuardianRouter(): GuardianRouter(req, "write") -> if nil: return FastGuard result -> else: return Guardian result -> if allowed AND no Guardian: return FastGuard result

Destructive / Admin actions:

FastGuard(req) -> if denied: return denied (no escalation) -> if allowed AND HasGuardianRouter(): GuardianRouter(req, effect) -> if nil: return DENIED ("fail-closed") -> else: return Guardian result -> if allowed AND no Guardian: return DENIED ("no guardian configured")

Key design decisions:

  1. Fast Guard denials are never escalated. If the fast prefilter denies an action, it is denied immediately regardless of effect. This prevents wasted compute on higher tiers.
  2. Mutating actions degrade gracefully. If the Guardian Router is unreachable, the Fast Guard result is used. This avoids blocking agents on transient infrastructure issues.
  3. Destructive/admin actions fail closed. If the Guardian Router is unreachable or not configured, the action is denied. This is a hard safety invariant.

Guardian Client Details

Fast Guard calls the auth service at POST {authURL}/api/v1/actions/check with:

{ "agent_id": "...", "action": "tool_call", "tool_name": "...", "tool_input_summary": "...", "session_id": "..." }

Guardian Router calls the Guardian pipeline at POST {guardianURL}/v1/guardian/verify with:

{ "agent_id": "...", "org_id": "...", "action_type": "write|destructive|admin", "action_name": "...", "action_source": "mcp|a2a|api", "session_id": "..." }

The Guardian Router returns nil on any HTTP error, signaling unavailability. The decision field uses "approve" for allowed actions.

7. Elevation and Approval

When a session in read_only mode encounters a non-read action, the AgentTrust ID system supports a human-in-the-loop elevation flow.

Approval Lifecycle

  1. Trigger: UnifiedChecker.Check() encounters a mutating or destructive action in a read_only session without an active elevation covering that action.

  2. Request creation: An ApprovalRequest is created with:

    • ID: New UUID
    • Status: "pending"
    • ExpiresAt: now + 5 minutes
    • Stored in Redis at key agenttrust_approval:{id} with 5-minute TTL
  3. Response: The check returns:

    { "allowed": false, "elevation_required": true, "approval_id": "uuid-here", "guard_tier": "session", "reason": "session is read-only; 'action_name' (mutating) requires elevation" }
  4. Approval: A user or admin calls POST /mcp/approvals/{id}/approve with an optional decided_by field (defaults to "dashboard_user").

  5. Elevation grant: On approval, ApprovalGate.Approve():

    • Updates the ApprovalRequest status to "approved"
    • Loads the session via SessionManager.Get()
    • Calls SessionManager.Elevate() with scope [action_name] and duration 5 minutes
    • The session mode changes to "elevated"
  6. Auto-revert: After the 5-minute elevation TTL expires, the next RecordCall() detects the expiry and reverts the session to read_only.

ApprovalRequest Fields

From internal/agenttrust/types.go:

FieldTypeDescription
IDstringUnique identifier (UUID)
SessionIDstringThe session requesting elevation
AgentIDstringAgent requesting the action
OrgIDstringOrganization context
ActionNamestringThe action that triggered the request
ActionEffectstringEffect classification (mutating, destructive)
ActionSourcestringProtocol source (mcp, a2a, api)
InputSummarystringTruncated action input for human review
StatusstringOne of: pending, approved, denied, expired
CreatedAttime.TimeWhen the request was created
ExpiresAttime.TimeWhen the request expires (5 minutes after creation)
DecidedBystringWho approved or denied (empty while pending)

Denial

Calling POST /mcp/approvals/{id}/deny updates the status to "denied" and records the decided_by field. The session remains in read_only mode.

8. Scope Enforcement

AgentTrust ID enforces authorization at three layers, each progressively narrower:

Layer 1: Scope Ceiling (Immutable)

Set at session creation. Represents the absolute maximum permissions for this session. The action name must appear in the ceiling list if the ceiling is non-empty.

Source by protocol:

  • MCP: The registered tools of the MCP server (MCPServer.Tools)
  • A2A: The delegation token’s Scope field
  • API: The JWT token’s Scope claim
// check.go, step 3a if len(session.ScopeCeiling) > 0 && !contains(session.ScopeCeiling, req.ActionName) { // DENIED: "action 'X' not in session scope ceiling" }

Layer 2: Allowed Actions (Session-Specific)

A subset of the ceiling that may be further restricted by the protocol adapter. The action name must appear in this list if it is non-empty.

// check.go, step 3b if len(session.AllowedActions) > 0 && !contains(session.AllowedActions, req.ActionName) { // DENIED: "action 'X' not in session allowed actions" }

Layer 3: Elevation Scope (Time-Boxed)

Granted temporarily via the approval flow. Only the specific approved action(s) are elevated, and only for the grant duration. Elevation scope is validated against the ceiling:

// session.go, Elevate() if len(state.ScopeCeiling) > 0 { for _, s := range scope { if !contains(state.ScopeCeiling, s) { return fmt.Errorf("requested elevation scope %q exceeds session ceiling", s) } } }

Token Scope Enforcement (MCP OAuth)

The ScopeEnforcer in internal/mcp/scope_enforcer.go manages MCP OAuth token scope mapping:

  • Storage: StoreTokenScope(ctx, accessToken, scope, ttl) stores the mapping at Redis key mcp_token_scope:{accessToken}
  • Retrieval: GetTokenScope(ctx, accessToken) looks up the scope for a given token
  • Validation helpers: CheckScopeCeiling() and CheckAllowedActions() return true if the list is empty (open) or the action is present

This allows MCP OAuth tokens issued via CIMD to carry scope that is enforced at the session level.

9. Integration Surfaces

Each protocol adapter translates its native request format into the unified AgentTrustRequest and manages sessions through the shared SessionManager.

MCP Adapter

Source: internal/mcp/proxy.go (Proxy) and internal/mcp/handler.go (Handler)

Input: JSON-RPC 2.0 request to POST /mcp/{serverID}

Translation:

MCP ConceptAgentTrust ID Field
rpcReq.MethodActionName
rpcReq.Params (truncated to 200 chars)ActionInputSummary
X-Agent-ID headerAgentID
X-Org-ID headerOrgID
X-Session-ID headerSessionID
(not set — auto-classified)ActionEffect
"mcp"ActionSource

Session init: POST /mcp/sessions/init with agent_id and server_id. The server’s registered tools become both AllowedActions and ScopeCeiling.

Denied response: JSON-RPC error response:

  • Standard denial: code -32600, message "denied: {reason}"
  • Elevation required: code -32001, message "elevation required for '{method}' (approval_id: {id})"

A2A Adapter

Source: internal/a2a/handler.go, internal/a2a/server.go, internal/a2a/adapter.go

Input: Live A2A JSON-RPC requests plus delegation-backed session context

Translation:

A2A ConceptAgentTrust ID Field
req.Method (tasks/send, tasks/get, tasks/cancel)ActionName
req.Params (truncated to 200 chars)ActionInputSummary
Resolved task actor or source_agent_idAgentID
X-Org-ID headerOrgID
X-Session-ID headerSessionID
"a2a"ActionSource

Session init: POST /api/v1/delegations/{delegationID}/session calls A2AAdapter.InitDelegatedSession(), which verifies the delegation chain before creating the session. Chain verification checks continuity, scope narrowing at each hop, no revocations, no expirations, and self-delegation prohibition.

Delegation constraints:

  • Maximum chain depth: 5 (enforced by MaxChainDepth)
  • Child scope must be a subset of parent scope (scope narrowing)
  • Self-delegation is prohibited (from_agent == to_agent)

Live runtime behavior:

  • If X-Session-ID is present, the A2A runtime uses A2AAdapter.CheckDelegatedAction() and enforces the delegated session ceiling.
  • If no delegated session is present, the A2A runtime still resolves the acting agent and sends the live request through UnifiedChecker.Check().

Direct API

Source: internal/agenttrust/api_adapter.go

Input: JWT token with TokenClaims (from internal/pkg/crypto/tokens.go)

Translation:

JWT ConceptAgentTrust ID Field
claims.AgentIDAgentID
claims.ScopeScopeCeiling and AllowedActions
claims.DelegationChainDelegationChainIDs
"api"ActionSource

Session init: POST /api/v1/agenttrust/api-sessions/init verifies the bearer token, resolves org_id, and calls APIAdapter.InitAPISession(). The JWT Scope claim becomes both the scope ceiling and the allowed actions.

JWT extensions defined by TokenClaims:

  • ati_version: Protocol version ("1.0")
  • org_id: Organization context
  • action_source: Protocol origin
  • session_id: Reserved session binding field
  • effect_permissions: Reserved per-effect authorization flags (read, mutating, destructive, admin)
  • cnf: Reserved RFC 7800 Proof-of-Possession field via JWK thumbprint

Current issuance behavior: IssueToken() now populates the core claims plus delegation_chain, cert_serial, ati_version, org_id, and action_source. It does not populate session_id, effect_permissions, or cnf by default.

Federation Bridge

Source: internal/federation/handler.go, internal/federation/agenttrust_bridge.go

Input: Verified federated ID token plus provider trust configuration

Session init: POST /api/v1/federation/sessions/init verifies the federated token, resolves the provider by issuer, and creates a local AgentTrust ID session with source federation.

Scope model: The federation bridge maps provider trust level to the local session ceiling. The bridged session is then enforced by the same UnifiedChecker path as other AgentTrust ID sessions.

10. API Endpoints

MCP Server Management

MethodPathDescription
POST/mcp/serversRegister a new MCP server (name, URL, tools)
GET/mcp/serversList all registered MCP servers for the org
DELETE/mcp/servers/{serverID}Remove an MCP server registration

MCP Proxy

MethodPathDescription
POST/mcp/{serverID}Proxy a JSON-RPC request to an MCP server with AgentTrust ID enforcement

Required headers: X-Org-ID, X-Agent-ID, optional X-Session-ID.

Session Management

MethodPathDescription
POST/mcp/sessions/initInitialize a new AgentTrust ID session (returns session_id, mode, scope_ceiling)
GET/mcp/sessions/{sessionID}Retrieve current session state (mode, metrics, scope)
POST/api/v1/delegations/{delegationID}/sessionInitialize an AgentTrust ID session from a verified delegation
POST/api/v1/agenttrust/api-sessions/initInitialize an AgentTrust ID session from a verified API JWT
POST/api/v1/federation/sessions/initVerify a federated token and bridge it into a local AgentTrust ID session

Approval Management

MethodPathDescription
POST/mcp/approvals/{approvalID}/approveApprove a pending elevation request (elevates session for 2 min)
POST/mcp/approvals/{approvalID}/denyDeny a pending elevation request
GET/mcp/approvals/{approvalID}Retrieve the current state of an approval request

Auth Service (Guardian Fast Guard)

MethodPathDescription
POST/api/v1/actions/checkFast Guard action check (called internally by GuardianClient)

Guardian Router

MethodPathDescription
POST/v1/guardian/verifyFull Guardian pipeline verification (Spot + Deep guards, called internally)

Appendix: Type Reference

AgentTrustRequest

type AgentTrustRequest struct { AgentID string // Agent performing the action OrgID string // Organization context SessionID string // Session ID (empty for stateless checks) ActionName string // e.g. "web_search", "file_write" ActionEffect string // read, mutating, destructive, admin (auto-classified if empty) ActionSource string // mcp, a2a, api ActionInputSummary string // Truncated input, max 200 chars DelegationChainIDs []string // A2A delegation chain EffectOverride string // Per-tool effect annotation (overrides pattern classification) RequireApproval bool // Force human-in-the-loop for this action }

AgentTrustResponse

type AgentTrustResponse struct { Allowed bool // Whether the action is authorized CheckID string // Unique check identifier Confidence float64 // Guardian confidence score GuardTier string // fast, spot, deep, human, session, error, unavailable, none LatencyMs int64 // Check latency in milliseconds Reason string // Human-readable explanation ElevationRequired bool // True if elevation can unblock this action ApprovalID string // ID of the pending approval (when ElevationRequired is true) }

SessionState

type SessionState struct { SessionID string // Unique session identifier AgentID string // Owning agent OrgID string // Organization Source string // mcp, a2a, api, federation ServerID string // MCP server ID (MCP only) DelegationID string // Delegation token ID (A2A only) ParentAgentID string // Delegating agent ID (A2A only) AllowedActions []string // Permitted actions for this session ScopeCeiling []string // Immutable maximum scope Mode string // read_only, scoped, elevated DefaultMode string // read_only or scoped (from MCP server config) ElevatedUntil *time.Time // When current elevation expires ElevationScope []string // Actions covered by current elevation TotalCalls int // Total authorization checks ReadCalls int // Read-effect checks WriteCalls int // Write/destructive/admin checks DeniedCalls int // Denied checks CreatedAt time.Time // Session creation time LastActivityAt time.Time // Last authorization check time Signature string // HMAC-SHA256 integrity signature (computed on save, verified on load) }

Redis Key Patterns

Key PatternTTLContents
agenttrust_session:{org_id}:{session_id}1 hour (sliding)JSON-serialized SessionState with HMAC-SHA256 signature
agenttrust_approval:{approval_id}5 minutesJSON-serialized ApprovalRequest
mcp_token_scope:{access_token}ConfigurableOAuth token scope string
Last updated on