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_onlymode. - 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
| File | Package | Responsibility |
|---|---|---|
internal/agenttrust/types.go | agenttrust | All type definitions: AgentTrustRequest, AgentTrustResponse, SessionState, ApprovalRequest, InitSessionRequest, RedisClient interface |
internal/agenttrust/check.go | agenttrust | UnifiedChecker.Check() — single entry point for all authorization decisions |
internal/agenttrust/session.go | agenttrust | SessionManager — session CRUD, elevation, metrics recording, auto-revert |
internal/agenttrust/approval.go | agenttrust | ApprovalGate — pending elevation request lifecycle (request, approve, deny) |
internal/agenttrust/effect.go | agenttrust | EffectClassifier — pattern-based action classification into read/mutating/destructive/admin |
internal/agenttrust/guardian.go | agenttrust | GuardianClient — HTTP calls to Fast Guard (auth service) and Guardian Router (Spot/Deep) |
internal/agenttrust/api_adapter.go | agenttrust | APIAdapter — translates JWT claims into AgentTrust ID sessions and checks |
internal/mcp/proxy.go | mcp | Proxy.Forward() — MCP JSON-RPC reverse proxy with AgentTrust ID integration |
internal/a2a/adapter.go | a2a | A2AAdapter — translates A2A delegation contexts into AgentTrust ID sessions and checks |
internal/mcp/scope_enforcer.go | mcp | ScopeEnforcer — 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
mutatingordestructiveeffects without active elevation: create anApprovalRequestwith status"pending"and returnElevationRequired: truewith anApprovalID. The agent must obtain approval before retrying. - For
admineffects 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:
scopedmode allows actions within the scope ceiling without requiring elevation.read_onlymode requires elevation for non-read actions.- If no default mode is provided,
read_onlyis applied automatically.
state := &SessionState{
Mode: initialMode,
...
}The InitSessionRequest fields determine the session’s scope:
| Field | MCP Source | A2A Source | API Source |
|---|---|---|---|
Source | "mcp" | "a2a" | "api" |
ServerID | MCP server ID | — | — |
DelegationID | — | Delegation token ID | — |
ParentAgentID | — | Delegating agent ID | — |
ScopeCeiling | Server’s registered tools | Delegation scope | JWT Scope claim |
AllowedActions | Server’s registered tools | Delegation scope | JWT 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).
Mode Transitions
| Transition | Trigger | Mechanism |
|---|---|---|
read_only -> DENIED | Non-read action attempted | Check() step 3c returns ElevationRequired |
DENIED -> elevated | Approval granted | ApprovalGate.Approve() calls SessionManager.Elevate() |
elevated -> read_only | TTL expires | RecordCall() checks ElevatedUntil on each call |
DENIED -> read_only | Approval denied or expires | No 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(), iftime.Now().After(*state.ElevatedUntil), the session reverts toread_onlyand clearsElevationScope
Metrics
Each session tracks call counts for behavioral analysis and anomaly detection:
| Field | Incremented When |
|---|---|
TotalCalls | Every RecordCall() invocation |
ReadCalls | Effect is "read" |
WriteCalls | Effect is "mutating", "destructive", or "admin" |
DeniedCalls | allowed == 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
RedisClientinterface)
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.
| Effect | Patterns | Risk Level |
|---|---|---|
destructive | delete, drop, destroy, purge, terminate, remove, truncate | High |
admin | admin, transfer_ownership, revoke, escalate, grant, impersonate | Critical |
mutating | write, update, create, execute, invoke, modify, send, put, post, commit, push, deploy | Medium |
read | get, list, read, describe, search, view, fetch, query, head | Low |
Classification Rules
- Matching is case-insensitive (
strings.ToLowerapplied first) - Matching uses
strings.Contains, sofile_deletematchesdestructiveandadmin_listmatchesadmin - Priority order ensures the most dangerous classification wins: an action named
delete_adminclassifies asdestructive(notadmin) - Unknown actions default to
"mutating"— this is a safe default that routes through a higher guardian tier thanread
Examples
| Action Name | Classified Effect | Matched Pattern |
|---|---|---|
web_search | read | search |
file_write | mutating | write |
database_drop_table | destructive | drop |
grant_permission | admin | grant |
custom_tool | mutating | (no match — default) |
list_users | read | list |
send_email | mutating | send |
remove_file | destructive | remove |
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:
| Mode | Session Mode | Guardian Pipeline | Human Approval |
|---|---|---|---|
read_only (default) | read_only | Full (effect-based routing) | Required for non-read actions (via elevation) |
scoped | scoped | Full (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
| Effect | Pipeline | On Fast Guard Deny | On Guardian Unavailable |
|---|---|---|---|
read | Fast Guard only | Denied (no escalation) | N/A (only Fast Guard used) |
mutating | Fast Guard -> Guardian Router (Spot) | Denied (no escalation) | Falls back to Fast Guard result |
destructive | Fast Guard -> Guardian Router (Spot -> Deep) | Denied (no escalation) | Fails closed (denied) |
admin | Fast Guard -> Guardian Router (Spot -> Deep) | Denied (no escalation) | Fails closed (denied) |
| Unknown action name | Classified as mutating, then follows mutating path | Denied (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 resultMutating 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 resultDestructive / 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:
- 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.
- Mutating actions degrade gracefully. If the Guardian Router is unreachable, the Fast Guard result is used. This avoids blocking agents on transient infrastructure issues.
- 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
-
Trigger:
UnifiedChecker.Check()encounters amutatingordestructiveaction in aread_onlysession without an active elevation covering that action. -
Request creation: An
ApprovalRequestis created with:ID: New UUIDStatus:"pending"ExpiresAt:now + 5 minutes- Stored in Redis at key
agenttrust_approval:{id}with 5-minute TTL
-
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" } -
Approval: A user or admin calls
POST /mcp/approvals/{id}/approvewith an optionaldecided_byfield (defaults to"dashboard_user"). -
Elevation grant: On approval,
ApprovalGate.Approve():- Updates the
ApprovalRequeststatus to"approved" - Loads the session via
SessionManager.Get() - Calls
SessionManager.Elevate()with scope[action_name]and duration5 minutes - The session mode changes to
"elevated"
- Updates the
-
Auto-revert: After the 5-minute elevation TTL expires, the next
RecordCall()detects the expiry and reverts the session toread_only.
ApprovalRequest Fields
From internal/agenttrust/types.go:
| Field | Type | Description |
|---|---|---|
ID | string | Unique identifier (UUID) |
SessionID | string | The session requesting elevation |
AgentID | string | Agent requesting the action |
OrgID | string | Organization context |
ActionName | string | The action that triggered the request |
ActionEffect | string | Effect classification (mutating, destructive) |
ActionSource | string | Protocol source (mcp, a2a, api) |
InputSummary | string | Truncated action input for human review |
Status | string | One of: pending, approved, denied, expired |
CreatedAt | time.Time | When the request was created |
ExpiresAt | time.Time | When the request expires (5 minutes after creation) |
DecidedBy | string | Who 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
Scopefield - API: The JWT token’s
Scopeclaim
// 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 keymcp_token_scope:{accessToken} - Retrieval:
GetTokenScope(ctx, accessToken)looks up the scope for a given token - Validation helpers:
CheckScopeCeiling()andCheckAllowedActions()returntrueif 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 Concept | AgentTrust ID Field |
|---|---|
rpcReq.Method | ActionName |
rpcReq.Params (truncated to 200 chars) | ActionInputSummary |
X-Agent-ID header | AgentID |
X-Org-ID header | OrgID |
X-Session-ID header | SessionID |
| (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 Concept | AgentTrust ID Field |
|---|---|
req.Method (tasks/send, tasks/get, tasks/cancel) | ActionName |
req.Params (truncated to 200 chars) | ActionInputSummary |
Resolved task actor or source_agent_id | AgentID |
X-Org-ID header | OrgID |
X-Session-ID header | SessionID |
"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-IDis present, the A2A runtime usesA2AAdapter.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 Concept | AgentTrust ID Field |
|---|---|
claims.AgentID | AgentID |
claims.Scope | ScopeCeiling and AllowedActions |
claims.DelegationChain | DelegationChainIDs |
"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 contextaction_source: Protocol originsession_id: Reserved session binding fieldeffect_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
| Method | Path | Description |
|---|---|---|
POST | /mcp/servers | Register a new MCP server (name, URL, tools) |
GET | /mcp/servers | List all registered MCP servers for the org |
DELETE | /mcp/servers/{serverID} | Remove an MCP server registration |
MCP Proxy
| Method | Path | Description |
|---|---|---|
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
| Method | Path | Description |
|---|---|---|
POST | /mcp/sessions/init | Initialize 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}/session | Initialize an AgentTrust ID session from a verified delegation |
POST | /api/v1/agenttrust/api-sessions/init | Initialize an AgentTrust ID session from a verified API JWT |
POST | /api/v1/federation/sessions/init | Verify a federated token and bridge it into a local AgentTrust ID session |
Approval Management
| Method | Path | Description |
|---|---|---|
POST | /mcp/approvals/{approvalID}/approve | Approve a pending elevation request (elevates session for 2 min) |
POST | /mcp/approvals/{approvalID}/deny | Deny a pending elevation request |
GET | /mcp/approvals/{approvalID} | Retrieve the current state of an approval request |
Auth Service (Guardian Fast Guard)
| Method | Path | Description |
|---|---|---|
POST | /api/v1/actions/check | Fast Guard action check (called internally by GuardianClient) |
Guardian Router
| Method | Path | Description |
|---|---|---|
POST | /v1/guardian/verify | Full 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 Pattern | TTL | Contents |
|---|---|---|
agenttrust_session:{org_id}:{session_id} | 1 hour (sliding) | JSON-serialized SessionState with HMAC-SHA256 signature |
agenttrust_approval:{approval_id} | 5 minutes | JSON-serialized ApprovalRequest |
mcp_token_scope:{access_token} | Configurable | OAuth token scope string |