PermDock
Security

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' } }],
  },
})
PartWho supplies itWhat it isTrust
principaldefinePolicy's subject function from the verified session or tokenThe user's id, org, roles and other values behind subject.*Trusted after authentication
actorThe adapter, from authInfo.clientId, runtime context, signature or Agent CardWho is making the callTrusted after the runtime verified it; never from prompt content
delegationThe adapter, from token scopes, authorization_details, a GNAP access array or a delegation chainWhat the principal allowed the actor to doTrusted 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

  1. Agent ≤ user. For every permission, asAgent.can(p, x) implies asUser.can(p, x). Delegation can only remove or narrow grants. This is enforced by construction: decide evaluates the principal's grants first, then filters by delegation.
  2. 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_details entry the previous hop lacked is rejected, and the decision is denied with a reason of kind delegation naming the offending hop.
  3. Conditions intersect. When a grant has where: { authorId: subject.id } and the delegation's authorization_details for the same permission has a further constraint, both must hold. A delegation cannot relax a grant condition.
  4. Approval survives delegation. A grant with approval: 'human' still requires approval when exercised through an agent; delegation cannot pre-approve. The approval token includes actor, so the approval is bound to the agent that asked.
  5. Deny is not delegable away. A deny grant applies regardless of delegation; an agent cannot be delegated around a deny.
  6. Fail closed on unknowns. An authorization_details type 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

Adapteractordelegation
permdock/mcpauthInfo.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-sdkactor: ({ 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-agentThe Claude Agent SDK session, kind: 'claude-agent'Configured per session; the tools map bounds what the agent can even ask for
permdock/a2aThe calling agent's identity from its card or token, kind: 'a2a'Caller's token scopes and authorization_details
HTTP adapters with Web Bot AuthVerified RFC 9421 signer key id, kind: 'web-bot-auth'From the bearer token on the same request, if any; otherwise empty
permdock/webmcpThe page's agent context, if the browser exposes oneThe client snapshot is the ceiling; the server re-checks with the real delegation
Human-only adapters (permdock/next, permdock/react)NoneNone; 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_details entries whose type names 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.authorizationDetails entries are matched by type to 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 the scope values of the alternatives, for clients that only understand RFC 6750.
  • The RFC 9457 Problem Details body with alternatives expressed as authorization_details objects 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 PermDock from the verified access token, takes the snapshot() and the Decision for the entry-point permission, and places their identifiers, the snapshot id and the decision id, in azd when it requests the transaction token. Downstream services do not re-run the edge decision; they re-derive the subject from sub_id and azd with permdock/jwt and evaluate their own permissions against the same principal, actor and delegation.
  • Delegation travels intact. azd carries the scopes, authorization_details and access that 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 iss is the domain's Transaction Token Service and its aud names this service; one that arrives from outside the trust boundary, or whose aud is 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 to delegation.chain, not to azd.
  • Audit correlates by id. The snapshot id and decision id in azd appear on every downstream on('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

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_details array. The plan schedules chain verification for Phase 4 and leaves the placement open.
  • The authorizationDetails type naming scheme (permdock:<key> is a placeholder) and how portable conditions are serialised inside RAR objects so authorization servers can render them.
  • The default delegation for 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.

On this page