Delegation
The principal, actor and delegation model, the attenuation invariants that keep an agent from exceeding its user, how adapters fill the agent half of the subject, and how RAR authorization_details are emitted and verified.
A PermDock subject has two halves. principal is the human or service whose grants are evaluated; actor is the agent acting for them; delegation is the authority the principal handed over. A decision is the principal's grants intersected with the delegation. This page is the security view of that model; the API view is in subject, the standards behind it in OAuth for agent delegation, and the decision record in ADR 0012.
The three parts
const permdock = await createPermDock(policy, user, {
actor: { id: 'https://agent.example/cimd.json', kind: 'mcp' },
delegation: {
scopes: ['post:read', 'post:update'],
authorizationDetails: [{ type: 'permdock:post.update', where: { authorId: '$subject.id' } }],
},
})| Part | Who supplies it | What it is | Trust |
|---|---|---|---|
principal | definePolicy's subject function from the verified session or token | The user's id, org, roles and other values behind subject.* | Trusted after authentication |
actor | The adapter, from authInfo.clientId, runtime context, signature or Agent Card | Who is making the call | Trusted after the runtime verified it; never from prompt content |
delegation | The adapter, from token scopes, authorization_details, a GNAP access array or a delegation chain | What the principal allowed the actor to do | Trusted after token verification; intersected, never added to |
A call with no actor is a human call and behaves exactly as createPermDock(policy, user). A call with an actor and no delegation has no delegated authority: every check is denied. Adapters therefore always supply at least the token's scopes.
Attenuation invariants
- Agent ≤ user. For every permission,
asAgent.can(p, x)impliesasUser.can(p, x). Delegation can only remove or narrow grants. This is enforced by construction:decideevaluates the principal's grants first, then filters by delegation. - Chain hops only narrow. In a multi-hop chain (user to orchestrator to sub-agent), each hop's authority is the intersection of the previous hop's authority with what it passes on. A hop that names a scope or
authorization_detailsentry the previous hop lacked is rejected, and the decision isdeniedwith a reason of kinddelegationnaming the offending hop. - Conditions intersect. When a grant has
where: { authorId: subject.id }and the delegation'sauthorization_detailsfor the same permission has a further constraint, both must hold. A delegation cannot relax a grant condition. - Approval survives delegation. A grant with
approval: 'human'still requires approval when exercised through an agent; delegation cannot pre-approve. The approvaltokenincludesactor, so the approval is bound to the agent that asked. - Deny is not delegable away. A
denygrant applies regardless of delegation; an agent cannot be delegated around a deny. - Fail closed on unknowns. An
authorization_detailstype PermDock does not recognise, a scope that matches no permission, or a malformed chain contributes nothing; it never widens.
How adapters fill actor and delegation
| Adapter | actor | delegation |
|---|---|---|
permdock/mcp | authInfo.clientId (an OAuth client id or a CIMD URL), kind: 'mcp' | authInfo.scopes; authorization_details when the token carries them (including ID-JAG-derived tokens under Enterprise-Managed Authorization) |
permdock/ai-sdk | actor: ({ runtimeContext }) => ({ id: runtimeContext.agentId, kind: 'ai-sdk' }) | Supplied by the application per call; defaults to the principal's full authority only when explicitly configured |
permdock/claude-agent | The Claude Agent SDK session, kind: 'claude-agent' | Configured per session; the tools map bounds what the agent can even ask for |
permdock/a2a | The calling agent's identity from its card or token, kind: 'a2a' | Caller's token scopes and authorization_details |
| HTTP adapters with Web Bot Auth | Verified RFC 9421 signer key id, kind: 'web-bot-auth' | From the bearer token on the same request, if any; otherwise empty |
permdock/webmcp | The page's agent context, if the browser exposes one | The client snapshot is the ceiling; the server re-checks with the real delegation |
Human-only adapters (permdock/next, permdock/react) | None | None; principal grants apply directly |
The AI SDK case deserves a note: the SDK gives PermDock the agent's identity but not a token, so what the agent is delegated is an application decision. The plan's default is that the application states it in createPermDock options; the alternative, treating an in-process agent as having the user's full authority, is a conscious choice the docs call out rather than a silent default. See the ai-sdk adapter.
RAR authorization_details
Every permission reference carries a scope and an authorizationDetails type. This lets PermDock participate in RFC 9396 Rich Authorization Requests in both directions:
- Emission for consent screens. Given a set of permissions an agent wants, PermDock produces
authorization_detailsentries whosetypenames the permission and whose constraint payload is the portable condition. An authorization server renders "update posts you authored" from it and issues a token carrying exactly that. - Verification on incoming tokens.
delegation.authorizationDetailsentries are matched bytypeto permissions; their constraints are parsed as portable conditions and intersected with the grant's conditions. Unknown types are ignored (fail closed); malformed constraints deny the permission they name.
[
{ "type": "permdock:post.read" },
{ "type": "permdock:post.update", "where": { "authorId": "$subject.id" } }
]Scopes remain the coarse layer: post:update in delegation.scopes is required for permissions.post.update to be exercisable at all; the authorization_details entry, when present, narrows it further.
GNAP access as a third input
RFC 9635 (GNAP) describes delegated rights as an access array whose objects carry type, actions, locations, datatypes, identifier and privileges, a structure the RFC itself calls analogous to RAR. delegation.access accepts those objects next to scopes and authorizationDetails; permdock/jwt fills it from an access claim when one is present. The three inputs are unioned into one delegated set before the intersection with the principal's grants, and every invariant above applies to access objects unchanged: an unknown type contributes nothing, identifier narrows to one resource, privileges never adds a role. Evaluation of access objects in the intersection is Phase 4; until then they are carried and audited but grant nothing.
delegation: {
scopes: ['post:read'],
authorizationDetails: [{ type: 'permdock:post.update', where: { authorId: '$subject.id' } }],
access: [{ type: 'https://api.example.com/permdock/resources/post', actions: ['update'], identifier: 'post_123' }],
}RAR metadata and error remediation
When a delegated check is denied with reason not-delegated, the caller's next move is a step-up: go back to the authorization server and ask for the authority that was missing. RFC 6750 only offers insufficient_scope and a flat scope hint, which cannot express "you need post.update restricted to posts you authored". The IETF draft OAuth 2.0 RAR Metadata and Error Remediation (August 2026) fills that gap with two pieces: authorization-server metadata describing the RAR type values it supports, and a structured remediation object returned on insufficient authorization that names the authorization_details the client should request.
PermDock already computes the content of that object. Decision.alternatives lists the permissions on the same resource that the principal holds and that the delegation could cover, and every permission carries an authorizationDetails type with its constraint payload. When the caller is an OAuth client, HTTP adapters and permdock/mcp therefore render a denied Decision as follows:
WWW-Authenticate: Bearer error="insufficient_scope"with thescopevalues of the alternatives, for clients that only understand RFC 6750.- The RFC 9457 Problem Details body with
alternativesexpressed asauthorization_detailsobjects in the draft's remediation shape, so a RAR-aware client can copy them straight into its next authorization request. - For MCP clients, the same alternatives inside the
scopeChallenge, since SEP-2350 step-up accumulates scopes (MCP authorization).
The mapping is one-directional and fail-closed: remediation tells the client what to ask for; it never changes the decision that produced it, and the authorization server remains free to refuse. The draft is a working-group document and its field names may change before Phase 2 ships the HTTP rendering; the Problem Details alternatives member is stable regardless, and the remediation shape is emitted next to it rather than instead of it. Its draft posture is therefore build (ADR 0025): the HTTP adapters pin the draft-ietf-oauth-rar-metadata-remediation revision they render (recorded on this page and in the Problem Details fixtures when Phase 2 starts), alternatives is the stable twin, and a pin bump is a maintainer change with fixtures and a changeset. The metadata half of the draft is where PermDock's catalog type URIs would be published so an authorization server can validate authorization_details requests against them; that is an open question on the GNAP page as well.
Transaction tokens
Inside one trust domain (a set of services behind the same gateway, owned by the same team) requests fan out across several services, and each of them needs to know who the original requester was and what they were authorized to do. Re-sending the user's access token to every hop leaks a long-lived credential; re-authenticating at every hop is slow and loses the delegation context. The IETF OAuth working group's Transaction Tokens draft (revision 11, July 2026) solves this with a short-lived JWT, issued by a Transaction Token Service through RFC 8693 token exchange, that carries the requester's identity in sub_id, the request context, and an authorization-details object in azd, and that is valid only for an aud inside the domain.
PermDock's design for it:
- Carrier, not evaluator. The service at the edge builds a
PermDockfrom the verified access token, takes thesnapshot()and theDecisionfor the entry-point permission, and places their identifiers, the snapshot id and the decision id, inazdwhen it requests the transaction token. Downstream services do not re-run the edge decision; they re-derive the subject fromsub_idandazdwithpermdock/jwtand evaluate their own permissions against the same principal, actor and delegation. - Delegation travels intact.
azdcarries thescopes,authorization_detailsandaccessthat the edge saw, so the attenuation invariants hold at every hop: a downstream service can be more restrictive than the edge, never less. - Never from outside the domain. A transaction token is accepted only when its
issis the domain's Transaction Token Service and itsaudnames this service; one that arrives from outside the trust boundary, or whoseaudis another service, resolves to the anonymous subject and every check is denied. Cross-domain propagation is the separate Identity and Authorization Chaining specification (RFC Editor queue) and maps todelegation.chain, not toazd. - Audit correlates by id. The snapshot id and decision id in
azdappear on every downstreamon('decision')event, so an incident review can walk from the edge decision through every hop that acted on it.
The Transaction Tokens for Agents and Cross-domain Transaction Tokens drafts extend the same claims to agent context and to federated domains; both are tracked on the watch list and neither changes the rule above. WIMSE's architecture uses transaction tokens for security-context propagation between workloads, which is why a workload appears in PermDock as a service principal rather than as an actor without a principal.
Audit
Every on('decision') event includes actor and delegation alongside the principal, outcome and reasons, and permdock/otel records them as span attributes. Two questions an incident review must answer, "which agent did this" and "under whose authority", are answered by the event itself. The AuthZEN endpoint carries the same fields in subject.properties.actor and context.delegation so a remote PDP sees them too.
Sources
- Product plan, "Subject model" paragraph and differentiator 17 ("Delegation-aware").
- Agent Delegation Chain draft for monotonic attenuation across hops.
- OAuth for AI agents on behalf of a user draft for
requested_actorandactor_token. - MCP Enterprise-Managed Authorization for ID-JAG-derived tokens.
- RFC 9635, GNAP, section 8, for the
accessarray. - Transaction Tokens (draft-ietf-oauth-transaction-tokens) for
sub_id,azdand the trust-domainaudrule; the WIMSE architecture for workload context propagation. - RFC 6750, RFC 8693 and the RAR Metadata and Error Remediation draft (draft-ietf-oauth-rar-metadata-remediation) are referenced by name; see the watch list for their status.
- ADR 0012: two-principal subject.
Open questions
- Verify chains in core or in the token layer. Verifying an Agent Delegation Chain offline in core gives every adapter the same guarantee but pulls signature verification into a zero-dependency package; trusting the token layer keeps core small but means core sees only a flattened
authorization_detailsarray. The plan schedules chain verification for Phase 4 and leaves the placement open. - The
authorizationDetailstypenaming scheme (permdock:<key>is a placeholder) and how portable conditions are serialised inside RAR objects so authorization servers can render them. - The default
delegationfor in-process AI SDK and Claude Agent SDK agents when no token exists: required explicit configuration, or full principal authority with a documented warning. - Whether a service acting on its own behalf (no human principal) is a principal with its own role or a distinct subject shape.
Approvals (human in the loop)
How approval: 'human' grants produce approval-required decisions, how the replay-safe token binds an approval to one call, and how each runtime surfaces and resumes the approval.
Comparison
How PermDock differs from permix, CASL, Kilpi, zap-studio/permit, Better Auth access control, Cedar and Amazon Verified Permissions, Open Policy Agent, the Zanzibar family (OpenFGA, Auth0 FGA, SpiceDB, WorkOS FGA), Casbin, accesscontrol, hosted PDPs, ZenStack and the AI SDK OPA adapter.