rfc: 0049
title: Agent delegation — refusing silent impersonation via the RFC 8693 act claim
status: specified
author: Jens Holdgaard Pedersen jens@holdgaard.org
drafting-assistance: Claude
created: 2026-08-21
supersedes: —
superseded-by: —
RFC 0049 — Agent delegation and the act claim
Status:
specified(2026-08-21). §5 criteria written and testable. Prerequisites: RFC 0026 (OIDC,accepted), RFC 0047 + RFC 0048 (the graph, bothaccepted). This RFC closes a gap left by those RFCs: Ourios derives its principal fromsubalone, so a standards-compliant delegation token authenticates as the subject, discarding the actor — impersonation, which RFC 8693 exists to distinguish from delegation.
1. Summary
RFC 8693 defines the act (actor) claim: a token whose
subject is one party and whose acting party is another. Ourios’s OIDC
verifier never inspects it (crates/ourios-core/src/auth/oidc.rs: the
principal is sub, and agent: versus user: is decided solely by the
configured agent_claim). A deployment that turns on token exchange
therefore gets silent impersonation: an agent presenting a valid
delegation token authenticates as the human, inherits every one of that
human’s grants, and no signal anywhere records that an agent was
involved. Nothing is malformed and nothing is misread — the actor is
simply dropped.
Three changes, in the order they matter:
- A token carrying
actis refused by default (401, named reason), because a principal Ourios cannot represent must never be silently downgraded to one it can. - An opt-in
actormode maps the principal to the current actor — never the subject — so a delegation token grants exactly what the agent itself holds, and the delegating subject travels as attribution only. actnever writes to the graph and never becomes a contextual tuple. The group claim stays the single contextual carrier (RFC 0048 §3.5). A blanketact→delegateexpansion is rejected here on the record, so nobody reaches for it later.
2. Motivation
2.1 What the specification actually says
RFC 8693 separates two things that look alike:
With impersonation, “A is given all the rights that B has within some defined rights context and is indistinguishable from B in that context.” With delegation, “principal A still has its own identity separate from B, and it is explicitly understood that while B may have delegated some of its rights to A, any actions taken are being taken by A representing B.”
Discarding act collapses the second into the first. The RFC also
bounds what a consumer may believe: a chain of delegation nests act
claims, but “the consumer of a token MUST only consider the token’s
top-level claims and the party identified as the current actor by the
act claim. Prior actors identified by any nested act claims are
informational only.” And on the risk: “Any time one principal is
delegated the rights of another principal, the potential for abuse is a
concern.”
2.2 Why this is not hypothetical
Ourios already treats agents as first-class principals (RFC 0047 §3.1)
and ships an MCP surface (RFC 0027) whose callers are agents by
construction. Token exchange is how an agent platform obtains a
credential for a user’s session; the moment a deployment’s IdP issues
one, our resolver reads sub and hands the agent the human’s visibility
— the exact outcome layer 2 exists to prevent. OWASP’s LLM06 (Excessive
Agency) names the mitigation in the same terms: “Track user
authorization and security scope to ensure actions taken on behalf of a
user are executed on downstream systems in the context of that specific
user, and with the minimum privileges necessary.”
2.3 What the ecosystem recommends instead
OpenFGA’s agent guidance is the shape this project already follows — “‘on behalf of’ is not the same as ‘as.’ A well-modeled agent has its own identity, inherits only the permissions it actually needs, and can be revoked independently of the user it serves” — and its current recommendation for actual delegation is task-based authorization: agents start with zero permissions and receive narrowly scoped, optionally session-bounded grants, with a contextual tuple binding the calling agent to the task. That is a successor to this RFC (§3.5), not its content.
3. Proposed design
3.1 act is a refusal by default
The OIDC verifier inspects the validated claim set for a top-level
act member. When present and the deployment has not opted in (§3.2),
verification fails with each surface’s existing unauthenticated path —
401 on the querier, /mcp and OTLP/HTTP ingest, a trailers-only
UNAUTHENTICATED (grpc-status 16) on OTLP/gRPC — carrying the stable
kind delegation_unsupported and error.type = "unauthenticated" on
ourios.auth.resolutions. The message names the claim and the config
key that enables handling, so an operator meets a diagnosis rather than
a mystery.
Refusal — not “ignore and continue as the subject” — is the point: the token asserts something about authority that this server does not implement, and RFC 0047’s posture is that an unanswerable authorization question fails closed.
3.2 actor mode: the principal is the actor, never the subject
auth:
oidc:
delegation: reject # default; `actor` opts in
delegation changes nothing for a token without an act claim: in
either mode such a token keeps today’s mapping — the principal is the
top-level sub, typed by agent_claim — so enabling the mode cannot
alter ordinary authentication. The rules below apply only when act is
present.
Under delegation: actor a token carrying act authenticates as the
current actor:
- the principal id is the actor’s
sub(theactobject’ssubmember); a missing or non-stringsubinsideactis a refusal; - the principal type applies the existing rule to the
actobject —agent:when the configuredagent_claimappears inside it, elseuser:; - nested
actclaims are ignored for authorization (RFC 8693’s MUST); only the outermost actor decides the principal; - the token’s own
sub(the delegating party) grants nothing. It is not consulted, not intersected, not unioned. An actor with no grants is refused or scoped exactly as that actor would be without the token.
The guarantee is precise, and narrower than “a delegation token can only reduce access”: the subject’s grants are never inherited. What the bearer sees is exactly what the actor sees authenticating alone — which may be less than the subject’s access, and may be more where the actor holds grants the subject does not. The property that matters is that a delegation token is never a way to acquire someone else’s visibility; it is only ever a way to authenticate as the actor, with attribution attached.
3.3 Attribution, not authority
An accepted delegation records the delegating subject on the request
span and the auth audit event — attribution, so an operator can answer
“who was this agent acting for?” — and nowhere else. It is never a
principal, never part of a predicate, never a graph object. The
attribute name is minted through semconv/registry/ with the OTel
naming rules applied (the project’s standing rule: query the OTel MCP
and check for a semconv collision before adding any name), so the exact
key is settled at implementation, not asserted here.
3.4 act never reaches the graph
No tuple — persisted or contextual — is ever derived from the act
claim. (The OIDC group claim remains the system’s one claim-derived
tuple and is unchanged: request-scoped team:<group>#member through the
sealed carrier.) RFC 0048 §3.5 sealed the contextual carrier so that
its only constructor is the validated group-claim path; this RFC adds
nothing to it. Concretely, the rejected design is act ⇒
conversation:T/<id>#delegate@agent:A for every conversation the
subject participates in: act carries no resource scope, so expanding
it into a per-conversation grant over the subject’s whole footprint is
impersonation reached by a longer route — precisely what §2.1
distinguishes, and what LLM06’s minimum-privilege guidance forbids.
3.5 Where real delegation goes (not built here)
If a deployment needs an agent to read a user’s conversations, the
answer stays an explicit, revocable grant: today the delegate
relation an operator or user writes on a specific conversation
(RFC 0047 §3.2); tomorrow, if the scenario justifies it, OpenFGA’s
task-based pattern — a task (and optionally session) object, grants
scoped to it, and a contextual tuple binding the calling agent to the
task so a task cannot be replayed by a different agent. That is a
separate RFC with its own producer story; naming it here is what stops
act → delegate being rediscovered as a shortcut.
3.6 One identity per agent (deployment requirement)
OWASP’s Non-Human Identities Top 10 lists NHI9 NHI Reuse — “sharing
identities across multiple services or agents, complicating
attribution” — and NHI5 Overprivileged NHI. Ourios cannot detect
sharing: a fleet behind one sub is one principal, so per-agent
revocation and attribution both quietly disappear, and every grant is
held by all of them. This RFC therefore requires the rule be written
into the authentication guide beside agent_claim when it is
implemented: one subject per agent identity. It is a deployment
contract the server cannot enforce.
4. Alternatives considered
- Keep today’s behaviour (ignore
act). This is the vulnerability: a valid delegation token becomes full impersonation of the subject with no record of the actor. Rejected. - Accept, warn, and continue as the subject. A warning nobody reads does not change who the query ran as. Rejected.
- Mint
delegatetuples fromact. §3.4. Rejected on the record. - Treat
actas a contextualdelegate(request-scoped, not persisted). Better than persisting, still wrong: the grant is unscoped, so it is impersonation for the life of the request. Rejected. - Build task-based authorization now. The right destination (§3.5), but it needs a task/session model, a producer contract for creating tasks, and a consent story. Deferred; this RFC makes the unsafe interim behaviour impossible rather than shipping the full feature.
5. Acceptance criteria
Scenario ids RFC0049.<n>. Eight criteria (RFC0049.1–.8).
RFC0049.1 — a delegation token is refused by default. Given
auth.oidcconfigured withoutdelegation, When a token carrying a top-levelactclaim is presented to the OTLP receiver (HTTP and gRPC), the querier and/mcp, Then each refuses with the surface’s unauthenticated status (401/UNAUTHENTICATED), the stable kinddelegation_unsupported, a message naming theactclaim and theauth.oidc.delegationkey, and oneourios.auth.resolutionsincrement witherror.type = "unauthenticated"; And no data is read, written or acknowledged.
RFC0049.2 — the actor is the principal, and the subject grants nothing. Given
delegation: actor, a subjectuser:aliceholding tenant-widereaderand an actoragent:botholding nothing in the tenant, When a token withsub = aliceandact.sub = botqueries, Then the principal isagent:bot, the visibility branch is the oneagent:botwould get alone (scoped or refused — never alice’s tenant-wide read), and alice’s grants are not consulted.
RFC0049.3 — only the outermost actor counts. Given
delegation: actorand a token whoseactnests a furtheract(alice→bot→inner), When it is presented, Then the principal is the outermost actor (bot) and the nested actor is ignored for authorization — including when the nestedactis itself malformed, which never affects the outcome; And Given a top-levelactthat is not an object (null, a string, a number, an array), or an object whosesubis absent, not a string, empty, or not a valid object id, Then the token is refused with the same unauthenticated path as RFC0049.1 — a malformed delegation is never downgraded to the subject.
RFC0049.4 — the actor’s principal type follows the same rule. Given
delegation: actorandagent_claim: ourios_principal_type=agent, When theactobject carries that claim/value, Then the principal isagent:<act.sub>; And when it does not, Then it isuser:<act.sub>.
RFC0049.5 — attribution without authority. Given an accepted delegation, Then the delegating subject appears on the request span and the auth audit event under the registry-minted attribute, And it never appears as a principal, in a visibility predicate, or as a graph object (asserted on the fake’s request log, as RFC0048.6 does).
RFC0049.6 —
actnever reaches the graph. Given any delegation token in either mode, Then noWriteis issued and no contextual tuple other than the group-claim tuples is sent (the sealedContextualTuplescarrier gains no new constructor).
RFC0049.7 — the knob is validated. Given
auth.oidc.delegation: <anything else>, Then startup fails naming the key and the accepted values.
RFC0049.8 — a token without
actis untouched. Givendelegation: actorand a token carrying noactclaim, When it authenticates on any surface, Then the principal is the top-levelsubtyped byagent_claim, exactly as withdelegation: rejectand exactly as before this RFC — enabling the mode changes nothing for ordinary tokens.
6. Testing strategy
Unit: the verifier’s claim handling as a table (absent act, top-level
act in each mode, nested act, malformed act, the agent_claim
inside act) next to the existing OIDC tests; config validation for the
knob. Integration (ourios-server it/): RFC0049.1 across the three
surfaces on the served binary with the existing issuer fixture;
RFC0049.2 and .4 against the fake graph, asserting the branch and the
request log; RFC0049.5 on the span exporter and the audit sink. No
OpenFGA container test is required — the authorization model does not
change, which is itself the point of §3.4.
7. Open questions
-
may_act. RFC 8693’s companion claim states that a party is authorized to become an actor. It is an authorization-server concern; is there any value in Ourios reading it (for instance to refuse anactthe issuer never sanctioned), or is that double-checking the IdP’s own job? - Per-tenant delegation mode.
delegationis deployment-wide as specified. A deployment that trusts token exchange for one tenant and not another would need it per credential or per tenant — no scenario asks for it yet. - Task-based authorization (§3.5). Its own RFC when a scenario arrives: what creates a task, who consents, how expiry is expressed (OpenFGA conditions vs a TTL on the object).
8. References
- RFC 8693 — OAuth 2.0 Token Exchange:
act(§4.1),may_act(§4.4), delegation versus impersonation (§1.1), security considerations (§5). - OWASP Top 10 for LLM Applications 2025, LLM06 Excessive Agency — minimum privileges, acting in the context of the specific user, human-in-the-loop for high-impact actions.
- OWASP Non-Human Identities Top 10 (2025) — NHI5 Overprivileged NHI, NHI9 NHI Reuse.
- OpenFGA modelling guides: AI agent authorization, Modeling agents as principals, Task-Based Authorization (the §3.5 successor pattern).
- RFC 0047 §3.1 (principal mapping), §3.2 (
delegate), RFC 0048 §3.5 (the sealed contextual-tuple carrier).