| Internet-Draft | Agent Delegation Profile | September 2026 |
| Hassan | Expires 23 March 2027 | [Page] |
This document builds on HTTP Message Signatures (RFC 9421) for requests that automated agents send on behalf of people. RFC 9421 shows who signed a request. It does not show who the agent acts for, or what the agent may do. This document defines an HTTP header field, Agent-Delegation, that carries a chain of delegation links. Each link can only narrow the scope, floors, and expiry of the link before it, and a verifier checks every link. Floors cover two things. The first is the person or account behind the agent: an Attestation Issuer, such as a mobile network operator or an identity-document check, answers with a signed yes or no, bound to the verifier's nonce and to an expiry, and never returns the value behind the answer. The second is what the agent may do: each call has a class, r, w, or x, with an optional count per class, and a signed menu gives the class of each call. This document does not define a credential format, an identity system, or revocation.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 23 March 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
Automated agents now act on the web for people: they search, fill in forms, and call APIs. An API Provider that receives such a request has two problems. It cannot tell whether an accountable person stands behind the software, so today it mostly tries to detect and block automation, or it asks a person to finish the last step, for example with a CAPTCHA. And it cannot rely on the agent to stay within what the person asked for, because a language model is probabilistic: an instruction such as "only search" is a request to the model, not a limit on it.¶
A person asks an agent to search for a red-eye flight from SFO to LAX under USD 400. The agent finds the airline's API, searches, and then books the only seat at that price, reasoning that the fare can be cancelled within 24 hours. The airline says the fare cannot be refunded: the booking was made from the person's own verified account. A model-based check was in place, but it is probabilistic too, and it did not stop the booking. The person asked for a search, a call that changes nothing. The agent made a booking, a call that spends money.¶
[RFC9421] lets a receiver check who signed a request. It does not carry who delegated the authority, whether an accountable person or account is behind it, or what the agent may do. When one agent passes work to another, often at another company, existing OAuth mechanisms do not, by themselves, ensure that each hop only narrows what it received. A limit that the model enforces on itself is not a limit: the check has to sit outside the model, at a point the model cannot change (Section 9.6).¶
This document adds four things:¶
A chain. One HTTP header field carries a chain of delegation links (Section 4, Section 5). Each link can only narrow the scope, floors, and expiry of the link before it (Section 7). Widening is a separate, consent-visible operation outside this profile. A verifier checks every link, with keys from its own trust source (Section 13).¶
A floor on who is behind the agent. An Attestation Issuer answers each floor (Section 8) with a signed yes, a signed no, or a signed refusal, bound to the verifier's nonce and to an expiry, and never returns the value behind it (Section 11). For example, a mobile network operator can attest a voice-and-data line held for at least two years with no SIM swap in the last 90 days. An identity-document check can attest that the holder is at least 18 (Appendix A). The expiry limits how long a yes can be used.¶
A floor on what the agent may do. Each call has a class: r changes nothing; w changes only the caller's own things; x reaches beyond the caller, or cannot be repeated (Section 9). A link sets the highest class it allows and, optionally, a count per class (Section 10). For example, a link written "rwx" allows every class with no count. A link it delegates to can be "r+1w+1x": reads with no count, one w call, and one x call. It can never be more. In the flight example, a link that allows only r refuses the booking, whatever the model decides.¶
Who says what class a call has. The API Provider signs a menu that gives each call its class (Section 9.4). Until it does, the party that runs the agent can build its own menu, sign it, and be responsible for it. Without a trusted menu, the HTTP method decides, so a flight search sent as POST is x. A mechanical tool can draft a menu from API descriptions. One such tool, rwxmap [RWXMAP], was run over 5465 labelled calls it was tuned on, with each vendor left out in turn. It gave the right class for 78.4% of calls, a tighter class than needed for 17.5%, and a looser class for 4.2% (Section 16). The same menu can later give MCP tool annotations [MCP], such as readOnlyHint and destructiveHint. This document does not define that mapping.¶
This document does not:¶
define a credential format, a signature scheme, or an agent identity system. It works with related work and does not replace it (Section 15).¶
prove that a human is present, or that one person backs one agent. A long-held line is costly to get, but it is not unique. A genuine identity document limits identities to the documents a holder has (Section 17, Appendix A).¶
define a unique identifier for the person. This is on purpose, for privacy. A deployment that needs one site to recognize a returning document can use a per-site tag, as zkagent [ZKAGENT] does, and no other site can link that tag. A deployment can also add its own limits on how often one line or one document backs a chain.¶
revoke. A short expiry is the only limit on the life of a link.¶
stop replay (Section 11.2), share counts across verifiers (Section 10.4), or hide queries from the Attestation Issuer (Section 14).¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
This document defines the following terms:¶
Delegated agent authorization, read across the drafts active in this space, separates into three distinct layers:¶
This document occupies layer (a), plus floors. The chain model of Section 7 and the floor axes of Section 8 bound the space of what an agent could invoke coarsely: an axis such as actionClass or xBudget constrains a category of action or a count of calls across an entire chain, not the arguments of any single call. A floor is evaluated once, when a link is issued or verified, and it does not change per invocation.¶
This document does not perform per-call argument binding, and it does not produce a receipt. [RFC9396] defines Rich Authorization Requests, which bind authorization detail at grant time. [I-D.das-agentic-tool-binding] binds a tool name and its arguments per call, after a model has already chosen what to invoke. [I-D.schrock-ep-authorization-receipts] addresses layer (c) with a pre-execution approval bundle and terminal consumption evidence, without itself establishing that the action occurred. [I-D.schrock-action-evidence-boundary] complements it with an executor-side model for exact-action matching, local authorization, atomic consumption, and outcome reconciliation at invocation, including a requirement to enumerate every path that bypasses it. The ordering runs: authorization detail is bound at grant time; floors of the kind this document specifies bound the space in between as a chain is attenuated hop to hop; and a per-call digest is bound after a specific choice has been made.¶
One structural problem is shared across this layering and is not solved by any one layer alone: a parent credential remains valid after it has been attenuated into a narrower child, so a holder of the parent retains the broader authority the child was only ever a narrowing of. This problem sits in the composition boundary between layers, not inside any single token profile. [I-D.asor-wimse-agent-delegation-chain] Section 9.1 lists mitigations for it (a short token lifetime, holder binding, and status lists). This document shares the same problem and offers no token-profile fix for it either; a deployment that needs to close it needs a mechanism this document does not supply.¶
This document defines an HTTP field, Agent-Delegation. It carries a delegation chain from a Delegate to a Relying Service, or to the next Delegate in the chain. It is a Structured Field [RFC9651]: a List whose members are Byte Sequences, each member one opaque, encoded link, ordered from the root link (index zero) to the leaf link the immediate sender presents.¶
Agent-Delegation: :BASE64-ENCODED-L0:, :BASE64-ENCODED-L1:¶
The credential format in use sets the encoding inside each link (Section 11); this document does not mandate one. A verifier MUST be able to parse each Byte Sequence into the fields that Section 7 and Section 11 require, before it evaluates the chain. The immediate sender covers Agent-Delegation with an RFC 9421 [RFC9421] signature, which binds the field to the request it accompanies. This document defines no new signature mechanism.¶
Any intermediary that can read HTTP headers can read Agent-Delegation, and this profile does not encrypt it. A link MUST NOT carry personal data of the Delegator, the Delegate, or any subject the floor condition concerns. See Section 14.¶
This section profiles the RFC 9421 [RFC9421] signature parameters of its Section 2.3 for a request that carries an Agent-Delegation field. It defines no new signature mechanism.¶
A sender MUST include the keyid, alg,
created, expires, and nonce parameters, and
MUST include the Agent-Delegation field among the signature's covered
components.¶
A sender MUST set the tag parameter to the string
"agent-delegation", so that a verifier can identify a signature made
under this profile. Tag values are application-specific and need no
registry. RFC 9421 Section 7.2.7 states the risk that unrelated
applications pick the same tag; this profile adds nothing to it.¶
RFC 9421 leaves enforcement of expires to the application.
This profile requires it: a verifier MUST reject a request whose
signature expires value is earlier than the time of
verification. Three expiry values stay separate. A link carries its
own expiry. A floor attestation carries its own expiry
(Section 11). The expires parameter covers
the HTTP message alone.¶
A verifier that needs a fresh signature bound to a value of its own
choosing SHOULD send the Accept-Signature field with a nonce
parameter, as RFC 9421 Section 7.2.2 describes. This profile defines
no other way to deliver a nonce. The nonce parameter binds
one HTTP message. The nonce inside a floor attestation
(Section 11) binds one attestation to one verifier's
question. They are two nonces at two layers, and implementers MUST NOT
treat them as one.¶
RFC 9421 signs a message. It has no notion of delegated authority, sub-principals, or attenuation of scope. The chain model of Section 7 therefore extends RFC 9421 rather than restating it.¶
A scope is a set of zero or more capability strings. A capability string is OPAQUE to this profile: this document defines no syntax, no internal structure, and no namespace for it. Any structure a deployment gives it, for example a resource:action convention, is a deployment convention and MUST NOT be interpreted by a verifier implementing this profile.¶
Comparison is by exact set containment. A scope S1 is contained in a scope S2 if and only if every member of S1 is octet-for-octet identical to some member of S2.¶
A verifier MUST NOT apply any of the following when comparing scopes, because none is defined by this profile:¶
The presented bytes are compared as presented. A verifier that applies a matching rule this profile does not define can accept a chain another conforming verifier rejects, which makes attenuation unverifiable.¶
The empty set is a valid scope, is contained in every scope, and is not by itself an error. Wildcard, hierarchical, and pattern-based scope semantics are out of scope for this version of the profile and are future work.¶
A delegation chain is an ordered sequence of links L(0), L(1), ..., L(n), where L(0) is the root delegation and L(n) is the link presented to the Relying Service. For every link L(i) with i > 0, relative to its parent L(i-1), all of the following MUST hold:¶
Scope containment. The scope granted by L(i) MUST be a subset of the scope granted by L(i-1), per Section 6. L(i) MUST NOT name a capability that L(i-1) did not itself hold.¶
Floor non-relaxation. The floor carried by L(i) MUST be at least as tight as the floor carried by L(i-1) on every axis defined in Section 8. L(i) MUST NOT be looser than L(i-1) on any axis, whether by omitting an axis L(i-1) constrained, by widening an ordered axis's value, or by substituting a different value on an equality axis that L(i-1) did not itself permit.¶
Expiry non-extension. The expiry carried by L(i) MUST NOT be later than the expiry carried by L(i-1).¶
These three rules apply to every link in the chain, not only to the leaf link. A verifier MUST validate rules 1 through 3 for every adjacent pair (L(i-1), L(i)) for i = 1 to n, in addition to validating L(0) against the root Attestation Issuer's published floor and the Delegator's own authority. A verifier MUST reject the entire chain if any single link violates any single rule; a partial acceptance of a chain up to the point of violation is not conforming.¶
A verifier MUST support a configurable maximum chain depth and MUST reject a chain whose length exceeds that configured maximum, prior to evaluating any link's content. This bounds the verification cost of a chain and limits the depth to which authorization can be re-delegated across administrative domains in a single deployment.¶
Widening a scope, a floor, or an expiry beyond what a chain currently carries is out of scope for this profile. Where a deployment needs to widen an existing grant, that MUST be performed as a distinct, explicit, consent-visible operation outside this delegation mechanism, producing a new root delegation rather than a modification of an existing chain.¶
An axis is one named condition a floor can constrain, for example how long a line has existed. A floor is a set of axis/value pairs. This document defines a closed set of axes for this version of the profile:¶
| Axis | Type | Comparator |
|---|---|---|
| subjectClass | enum | equals |
| tenureMin | duration | min |
| credentialAgeMin | duration | min |
| ageMin | non-negative integer (years) | min |
| actionClass | ordered list: r < w < x | rank; semantics in Section 9 |
| classSource | ordered list: method < declared | rank; semantics in Section 9 |
| rBudget | non-negative integer | max; semantics in Section 10 |
| wBudget | non-negative integer | max; semantics in Section 10 |
| xBudget | non-negative integer | max; semantics in Section 10 |
An axis name that is not in this table MUST cause a verifier to reject the floor, and therefore the link that carries it. An unknown or misspelled axis name is always a hard rejection; it MUST NOT be silently ignored or dropped from the comparison. This applies equally to a published floor and to a requested floor presented in a delegation link.¶
subjectClass has two values. interactive means the
subject is a service a person uses directly; for a telecom
issuer, a line with voice and data service. machine
means the subject is a device-only service; for a telecom
issuer, a machine-to-machine line. A child link's subjectClass
MUST equal its parent's.¶
ageMin is an age threshold stated in whole years, for example 18. It asks one question: is the subject at least that age. An Attestation Issuer answers only yes or no; it MUST NOT return the subject's age or date of birth. ageMin does not use the duration grammar of Section 8.1. An Attestation Issuer publishes the thresholds it supports; if a floor asks for a threshold that is not on that list, the issuer MUST refuse, per Section 11.1.¶
For a min-compared axis (tenureMin, credentialAgeMin, ageMin), a larger value is tighter than a smaller one. For a max-compared axis (rBudget, wBudget, xBudget), a smaller value is tighter than a larger one. For a rank-compared axis (actionClass, classSource), a lower position in the axis's ordered list is tighter than a higher one.¶
min, max, and rank are the comparisons defined in [I-D.asor-wimse-agent-delegation-chain] Section 4.2, with the same tightening rules as its Section 4.3. equals is that draft's one_of with a single value. This document names its own closed set of axes rather than a registry.¶
A link can leave an axis out in two different ways. First, within a chain: if the parent link L(i-1) constrains an axis and the child link L(i) leaves it out, that is a loosening; the chain MUST be rejected, per rule 2 of Section 7. Second, if no link in the chain up to and including the link under evaluation constrains an axis, the issuer's published floor for that axis applies unchanged. This second case is inheritance, not loosening, and it is not the same as the first case.¶
On every ordered axis, a requested floor value MUST be at least as tight, by that axis's comparator, as the matching published or parent floor value. If it is not, the request MUST be refused. A floor value MUST NOT be widened implicitly, as a side effect of anything this profile defines; as Section 7 states, widening is always a separate, explicit, consent-visible operation, outside this profile's scope.¶
A duration-typed axis value MUST match exactly one of two forms, P<n>D or P<n>Y, where <n> is a non-negative integer. <n> MUST be between 0 and 2^53 - 1 inclusive. No other ISO 8601 duration form is accepted. A duration given in months MUST be rejected, because a month's length in days depends on the calendar and is ambiguous; accepting it would make the tightening comparison above undefined. For comparison, 1Y equals exactly 365D. A value outside the permitted range, or matching neither form, MUST be rejected.¶
This section defines actionClass, the highest class of call a link allows, and classSource, where a verifier gets a call's class from.¶
actionClass is an ordered list, r, w, and x, ranked r < w < x by the rank comparator of Section 8. A link's actionClass is a ceiling on the whole link, not a statement about one call. The values are defined by effect:¶
Whether a call can be undone is not the test. A child link's actionClass MUST NOT be higher than its parent's.¶
classSource is an ordered list, method and declared, ranked method < declared by the rank comparator of Section 8. With method, the verifier uses the HTTP method alone (Section 9.3). With declared, the verifier also uses a signed menu (Section 9.4); where the menu verifies and names the call, its value is used instead of the method default. method trusts no one but the verifier, so it is the tighter value.¶
A child link's classSource MUST NOT go from method to declared; the only permitted move is declared down to method. Where no link in the chain sets classSource, method applies, the second omitted-axis case of Section 8; where a parent sets it and a child omits it, the chain MUST be rejected, the first case.¶
Where classSource is method, the method gives a starting class, not the definition of the class: GET, HEAD, and OPTIONS default to r; PUT, DELETE, and PATCH default to w; POST, and any other method, default to x. This matches how most APIs use HTTP methods, and it can be wrong either way: some PUT, DELETE, or PATCH calls are really x, and some POST calls are really r or w. A GET that changes something breaks the safe-method rule of [RFC9110]. The API Provider's declared menu, below, corrects a wrong starting class.¶
Where classSource is declared, the class is the value the menu declares, provided it verifies per Section 9.4 and names the call. If the menu is absent, its signature does not verify, the verifier has no trusted key for the menu's signer, or its iss does not match the request's target, the verifier MUST use the method default. A verifier MUST NOT combine or take the greater of the two; a valid, matching menu entry replaces the method default, it is not compared against it. No menu at all is handled the same way as a menu that fails to verify. A verifier MUST NOT build or change a menu while it admits a request; a menu is created and signed before it is used.¶
An enforcement boundary is a role in an effect-capable path, not a component: the first admission point on that path whose successful admission is necessary for the protected effect to complete through that path. Any component can occupy this role for a given path, including a harness or orchestration layer, provided that component actually refuses or permits the request rather than merely observing or advising on it; a component that only coordinates or advises does not classify or admit, whatever else it does. Different paths to the same effect can realize the role at different components or layers, and no single component need be common to all of them. The verifier, as used throughout this document, is the component occupying the enforcement-boundary role for a given effect-capable path; the two terms refer to the same role, not two different things.¶
Boundary identification under this section is performed separately for each effect-capable path. A deployment with more than one path to the same protected effect identifies a qualifying enforcement boundary on each path individually; identifying one on a single path says nothing about any other.¶
Closure is a separate, system-wide requirement: every effect-capable path MUST cross an enforcement boundary qualifying under the definition above before the protected effect can complete. This is checked by the bypass test: for a given protected effect, if any path capable of producing that effect can do so without crossing a qualifying enforcement boundary, closure fails for that effect, regardless of how many other paths to it are correctly guarded. This closure requirement is consistent with, and distinct from, the complete-mediation condition of [I-D.schrock-action-evidence-boundary]. That document requires a deployment to document every direct, break-glass, administrator, alternate-protocol, and system-of-record path that bypasses its implementation, and forbids a claim of complete mediation unless the protected system rejects all material alternate paths or subjects them to an equivalent boundary. That condition governs what one deployment may claim about the system it protects. This document's closure requirement governs admission itself: it is an invariant over the complete set of effect-capable paths to a given protected effect, checked by the bypass test, however many components realize the enforcement-boundary role across them, and independent of any conformance claim.¶
An agent that declines to attempt an action it expects to be refused does not, by that restraint, satisfy this section: satisfaction requires an enforcement boundary that would refuse the request had it actually arrived, not an agent's choice not to send it. Admission is an act of an enforcement boundary refusing or permitting a request, not an act of a harness or an agent abiding by a floor it was merely told about.¶
This section defines rBudget, wBudget, and xBudget, the budget axes of Section 8: what each limits (Section 10.1), the limit and count and how a request is admitted (Section 10.2), how the count is keyed (Section 10.3), and the scope of the limits (Section 10.4).¶
rBudget, wBudget, and xBudget are each a non-negative integer, compared with the max comparator of Section 8. Each limits one actionClass value, per Section 9.3: rBudget bounds r-class calls, wBudget bounds w-class calls, xBudget bounds x-class calls, and a call never spends against the other two. A child link MUST NOT carry a higher value than its parent's for any of the three; raising one violates rule 2 of Section 7.¶
Where no link sets a given budget and the issuer's published floor sets none either, that class has no count limit: actionClass alone governs, so a link classed x with no xBudget allows unlimited x-class calls. Where the published floor does set a budget, that value applies (the link-to-published-floor case of Section 8); omission is inheritance, never widening. Where a parent link sets a budget and a child omits it, the chain MUST be rejected (the link-to-link case, rule 2 of Section 7).¶
A short label such as "rw+2x" means actionClass x, xBudget 2, no rBudget or wBudget: no limit on r or w, at most 2 x calls. The label is shorthand for talking about a link, not a wire format.¶
"Limit" and "count" are kept apart. The LIMIT is a budget value carried in the chain (Section 10.1), carried on the wire like any floor value. The COUNT is the number already spent for a chain; it is verifier-held state, keyed by the chain identifier of Section 10.3, one per budget per chain, never carried in a link. A verifier MUST NOT treat any link field as the count.¶
A request is classed per Section 9.3 and admitted only if the count remaining for that class is at least one, where a limit applies; with no limit, the actionClass rule of Section 9.1 alone governs. Where a limit applies, the count MUST be decremented in the same admission decision, with no window in which two concurrent requests on the same chain both see the same remaining count. At zero, the request MUST be rejected with the uniform outcome Section 13 requires for any failed check, without disclosing the remaining count. The count can only live in verifier-held state because anything on the wire can be sent again (Section 11.2).¶
The chain identifier MUST be derived by the verifier itself from the chain, and MUST NOT be taken from the request; a verifier that accepts a caller-supplied identifier does not conform, whatever else it checks. It is the SHA-256 [RFC6234] digest of the signature value of L(0), the root link, in the encoding the credential format uses on the wire (Section 4, Section 11): the same bytes a verifier already extracts and verifies as L(0)'s signature under item 1 of Section 11.¶
Every link, from L(0) to L(n), shares the identifier derived from L(0)'s signature. Re-delegation produces a new child link L(i) with its own signature but does not change L(0), so it does not change the chain identifier; re-delegation MUST NOT create new budgets, and a verifier MUST NOT derive the identifier from any link other than L(0).¶
Counts are held per verifier, not shared across verifiers: N verifiers with no shared state can together admit up to N times a budget before any one reaches zero. Sharing a count across verifiers is out of scope; this limit is stated here, not hidden.¶
Each budget is cumulative per chain and per class, not per operation, because a per-operation cap is defeated by splitting one action into many smaller ones, each under its own cap. rBudget limits how many reads an agent makes, not how much data one read returns; how much a read returns is up to the API Provider, for example through page size.¶
This document makes no claim about what budget size is practical; choosing a value is left to the deployment.¶
This document does not mandate a specific credential format. A JSON Web Token, a CBOR Object Signing and Encryption (COSE) structure, or another signed structure MAY be used, if it meets every property in this section. A conforming attestation MUST:¶
be signed by the Attestation Issuer over the exact bytes presented to the verifier. The verifier MUST verify the signature over those exact bytes and MUST NOT re-serialize the object first, or instead;¶
carry the verifier's nonce, echoed back exactly;¶
carry an expiry. A missing or non-numeric expiry MUST be treated as already expired; it MUST NOT be treated as "no deadline";¶
carry only a yes/no result of the floor evaluation, and MUST NOT carry the value behind that result. An attestation that carries a raw value, such as a date of birth or a tenure count, instead of or next to the result, does not conform to this profile;¶
be rejected by the verifier if its serialization has a duplicate top-level key. A first-wins or last-wins rule for duplicate keys lets the issuer and the verifier disagree about which value was signed; this profile requires rejection instead of a resolution rule.¶
Where an Attestation Issuer cannot or will not answer a floor, it MUST return a signed refusal, not silence, an unsigned error, or a bare protocol failure. The refusal MUST be signed by the Attestation Issuer, MUST carry the verifier's nonce, and MUST carry an expiry, under the same rules as an attestation, so it cannot be forged in the issuer's name or stripped without detection. A verifier MUST verify a refusal along a path independent of the one it uses for an accepted attestation, so a defect in one path does not admit a forged instance of the other.¶
If a floor names an axis the Attestation Issuer cannot answer, the issuer MUST return a signed refusal and MUST NOT skip that axis and answer on the rest; for example, a telecom issuer cannot answer ageMin, and an identity-document issuer cannot answer tenureMin. If the issuer holds incomplete data for an axis, it MUST return a signed refusal and MUST NOT round the answer to yes or no.¶
A signed yes, a signed no, and a signed refusal are three different results: a signed no is an attestation, under item 4 above, whose result is false; a signed refusal means the issuer cannot or will not answer at all. A verifier MUST NOT read a signed refusal as a signed no.¶
The nonce ties a signed response to the request that carried it; it does not, by itself, stop that same response from being presented again. The same response verifies again for as long as it has not expired. Stopping replay, for example by tracking nonces already used within their validity window, is the verifier's own job and is outside the scope of this document. Implementers MUST NOT assume that nonce binding, by itself, gives replay protection.¶
A Delegate that presents a delegation chain is identified to a Relying Service by an identifier whose only required property, in this profile, is: the identifier MUST be unlinkable across distinct Relying Services and MUST remain stable across separate interactions with the same Relying Service. This document does not specify how such an identifier is derived, what inputs contribute to it, its length, or its encoding; those are deployment choices left to the entity that issues or constructs the identifier.¶
This requirement pulls against related work. [I-D.klrc-aiagent-auth], Section 6, wants an agent identifier that is stable for the life of a workload identity, so that audit records can be linked over time. An identifier stable enough for that audit at one Relying Service is also a strong link across Relying Services if it is reused there, or can be derived in a reused form. This document does not resolve this tension. It is an open item for the Working Group.¶
A verifier processing a request that carries an Agent-Delegation field MUST perform the following steps, in the order given. A failure at any step MUST produce one uniform rejection outcome, indistinguishable from the outcome of a failure at any other step, so that the specific reason for rejection does not become available to an unauthenticated party as an oracle (see Section 17).¶
Parse the Agent-Delegation Structured Field List into its ordered member links.¶
Compare the number of links against the verifier's configured maximum chain depth; reject if the chain exceeds it.¶
Verify the RFC 9421 HTTP Message Signature applied by the immediate sender over the request, including the Agent-Delegation field among its covered components, per the profile of Section 5.¶
Verify each link's own signature, per Section 11, over its exact presented bytes.¶
Verify the root attestation, L(0), against the Attestation Issuer's public key, where that key is obtained from a configured trust source and MUST NOT be obtained from the message itself.¶
Verify the three attenuation rules of Section 7 for every adjacent pair of links in the chain.¶
Verify L(0) against the root Attestation Issuer's published floor and the Delegator's own authority, per Section 7. Reject any floor, published or carried in a link, that names an axis not listed in Section 8. Where no link up to L(n) constrains an axis, apply the published floor for that axis.¶
Verify that every floor attestation carried by the chain echoes, exactly, the nonce the verifier issued for this exchange, and reject if any does not.¶
Verify that no link in the chain has an expiry that has passed, evaluated at the time of verification.¶
Verify that every floor attestation carried by the chain has a yes result; reject if any is a signed no or a signed refusal, per Section 11.1.¶
Classify the request's actionClass per Section 9.3, using the declared menu of Section 9.4 where the chain's classSource is declared. Admit the request only if its class is not higher than the actionClass that applies to L(n) (Section 9.1) and, where a budget applies to that class, the count for that class is at least one; decrement that count in the same admission decision, per Section 10.2. Per Section 9.6, this step is performed by whichever component occupies the enforcement-boundary role for the effect-capable path the request is on, not by a component that only coordinates or advises.¶
Only after every preceding step has succeeded, apply the Relying Service's authorization policy to the resulting, fully verified chain.¶
A verifier that implements only a subset of these steps, or that implements them out of order such that a later step's success can mask an earlier step's failure, does not conform to this profile.¶
This profile is built around boolean-only disclosure: a floor attestation states whether a subject meets a condition and MUST NOT carry the underlying value. This bound holds throughout the chain; an intermediate Delegate that re-derives or forwards a raw value defeats the disclosure minimization this profile exists to provide, even where every attenuation rule in Section 7 is otherwise satisfied.¶
The agent identifier requirement in Section 12 requires unlinkability across Relying Services specifically so that two Relying Services receiving requests from the same underlying agent cannot, from the identifier alone, determine that the requests originated from the same principal.¶
This profile does not eliminate the Attestation Issuer's own visibility into the queries it answers. The Attestation Issuer necessarily observes that some verifier asked whether some subject meets some floor, even though the verifier itself learns only a boolean result. Where the query pattern itself is sensitive, this residual, issuer-side query log is a real and unsolved limitation of this profile, not an oversight to be corrected by better protocol design; deployments that need to eliminate it need a mechanism outside this document's scope.¶
This section records the status of known implementations of this profile, per [RFC7942]. The information is believed to be accurate at the time of writing, has not been independently verified, and is not an endorsement of this profile by any party. Both implementations below are single-author and open source. Neither has had external review or been adopted by any organization, and no Attestation Issuer has reviewed or validated this profile.¶
justabit proof-of-concept. A zero-dependency Node.js proof-of-concept is available at https://github.com/hamr0/justabit. Its suites passed on 2026-09-14, checked by exit code. They cover:¶
camara/v2/poc: Ed25519 signatures with a pinned algorithm, nonce binding, expiry, rejection of a raw value next to the result, duplicate top-level key rejection, and the signed refusal (Section 11, Section 11.1);¶
ietf/v3/poc: a closed axis set, the duration grammar, and tightening only (Section 8);¶
ietf/v3/poc: classification by method default or by a signed menu, path-template matching and origin binding of menus, tightening of actionClass and classSource, the two omitted-axis cases, a budget count keyed by the chain identifier the verifier derives, and three Verifier Placement cases, one of them a bypass path as a negative control (Section 9, Section 10.3).¶
The code has not caught up with this revision in four ways, and these parts of the text are not tested:¶
It does not implement RFC 9421 message signatures or any HTTP transport, the Agent-Delegation header, a delegation chain of more than two links, or the agent identifier of Section 12.¶
rwxmap. rwxmap [RWXMAP] is a work-in-progress tool that drafts a menu from OpenAPI descriptions with fixed rules and no language model. Step 1 classes GET, HEAD, and OPTIONS calls, and POST calls whose lead verb is a read verb, as r. Step 2 starts PUT, DELETE, and PATCH calls at w and raises a call to x when its verb acts on something live or a noun names another party; it marks a w call that no rule vouched for, so a person can check it. Step 3 leaves every other POST call at x. rwxmap signs nothing and does not yet produce MCP tool annotations. At rwxmap commit 56b3310, run on 2026-09-14 and reproduced from a clean copy, it gave these results over 5465 calls from 332 vendors, with each vendor left out in turn:¶
| Result against the truth labels | Calls | Share |
|---|---|---|
| right class | 4282 | 78.4% |
| tighter class than needed | 955 | 17.5% |
| looser class than needed (leak) | 228 | 4.2% |
Of the 228 leaks, 17 are GET calls that the method default keeps at r, and 179 are among the 1998 calls rwxmap marked for a person to check. Three limits apply to these figures. The rules were tuned on these same calls, so they are not a clean test, and there is no clean-test figure today. The truth labels were written by language-model agents that read each call blind, not by people. The tool's goal is about 80% right, 20% tighter, and 1-2% looser; the measured 4.2% is above that goal.¶
A verifier that checks only the last link defeats this profile. A Delegate can present a well-formed last link while an earlier link widened scope, loosened a floor, or extended an expiry. Conformance requires every step of Section 13 for every link; there is no mode that checks only the last link.¶
Nonce binding does not stop replay. Per Section 11.2, the same attestation verifies again until it expires. A deployment needs its own replay defense, such as tracking used nonces; this document does not supply one.¶
Cost-based floors do not create uniqueness. A tenure or credential-age floor makes a fresh qualifying credential slower and more costly to get. It does not show that a unique person holds the account, or that one subject does not back many agents. A deployment that needs uniqueness needs a mechanism this profile does not provide.¶
This profile defines no revocation. Once issued, an attestation or a link stays valid until its expiry. The only mitigation this profile offers is a short expiry.¶
The trust source is a central point. Every verifier depends on the trust source that gives it an Attestation Issuer's public key, per step 5 of Section 13. A compromise or outage of that trust source affects every verification that depends on it.¶
Rejection MUST be uniform. Per Section 13, a verifier MUST NOT return outcomes that differ by which check failed, such as an expired link, a bad signature, a loosened floor, or a chain that is too deep. A verifier that shows which check failed lets an unauthenticated party probe the edges of a valid chain without ever holding one.¶
The Attestation Issuer's query log is still a disclosure channel. The verifier learns only a yes, a no, or a refusal, but the issuer sees every query. A party that can read or compel disclosure of that log learns what the wire protocol withholds.¶
Budgets hold only if the verifier derives the chain identifier. Per Section 10.3, the verifier derives the chain identifier from L(0)'s signature. A verifier that takes the identifier from the request lets the presenter reset rBudget, wBudget, or xBudget by sending a new value on the next request; V11 in Appendix B tests this failure. Counts are held per verifier, per Section 10.4; a deployment that needs one shared count across verifiers needs a mechanism this document does not supply.¶
A declared menu can lower a class. A menu can declare a call below its method default, for example a POST as r. The origin match and the configured-key rule of Section 9.4 stop a menu signed for one API from being applied to another; a verifier that skips either check does not conform to this document. A menu signed by the party that runs the agent is that party's responsibility: it limits only the verifiers that party configures, and it never makes an API Provider's verifier admit more.¶
The method default can be too loose for GET. The method default classes every GET, HEAD, and OPTIONS call as r. Some GET calls change something, contrary to the safe-method rule of [RFC9110]; a link limited to r admits them unless a declared menu gives them a higher class.¶
This document registers the following entry in the "Hypertext Transfer Protocol (HTTP) Field Name Registry" defined by [RFC9110]:¶
| Field Name | Status | Structured Type | Reference |
|---|---|---|---|
| Agent-Delegation | permanent | List | This document, Section 4 |
No other IANA actions are required by this document.¶
This appendix is non-normative. Directions 1 to 3 could fill the Attestation Issuer role. None of them makes an attestation that meets Section 11 today, so each one states what it lacks. Direction 4 is harness experience. This profile does not require, prefer, or depend on any of them.¶
Direction 1: a mobile network operator. The operator answers floors about a line: subjectClass is the service type (voice and data, or machine to machine), tenureMin is the subscription tenure, and credentialAgeMin is the time since the last SIM swap. Many qualifying lines cost money over time, but cost does not make a line unique. CAMARA reviewed a companion proposal [CAMARA-PROPOSAL] in August and September 2026 and asked for changes; the author answered them. The signing part is now a separate proposal [CAMARA-SIGNING]. On 2026-09-15 both are open, and neither is accepted or adopted. The two documents meet only at the header of Section 4. Lacks: CAMARA API responses are not signed.¶
Direction 2: 8een. The holder's wallet makes a zero-knowledge age proof over an ISO/IEC 18013-5 [ISO18013-5] mobile document, bound to the site's nonce, with the scheme of [I-D.google-cfrg-libzk]. 8een [ZK8EEN] checks the proof against an issuer trust list and returns one bit. A one-time nonce and the credential expiry limit its use. Its own limit: "No proof from a real phone has ever reached this verifier." Lacks: no issuer signs the result, because the verifier checks the proof itself. This document does not define that model.¶
Direction 3: zkagent. zkagent [ZKAGENT] reads a passport or ID card chip on the holder's phone, checks the chip signature (passive authentication, [ICAO9303]), and returns one bit. It is not zero-knowledge. Tier A sends the bit with no signature. Tier B signs the bit with a phone key and adds a per-site tag; the site binds that key the first time it sees it. Passive authentication does not bind the person who presents the document, and a chip without chip authentication can be cloned. Lacks: tier A has no signature, and the tier B key does not come from a configured trust source (Section 13).¶
For Directions 2 and 3, subjectClass is interactive and ageMin applies; tenureMin and credentialAgeMin do not. They limit identities to the documents a holder has. They do not give one identity per person.¶
Direction 4: harness experience. bareguard https://github.com/hamr0/bareguard, bareloop https://github.com/hamr0/bareloop, and bareagent https://github.com/hamr0/bareagent are single-author, open-source experiments. They do not implement this profile, so Section 16 does not list them. Each puts the point of control at a different component: a single gate, an outer arbiter, and a policy chokepoint. What they share is a component that refuses or permits, not a location. This supports the role model of Section 9.6.¶
This appendix is non-normative. Where a vector and a normative section differ, the normative section governs. Each vector below is a negative control: a verifier that skips one check accepts a chain or admits a request that a conforming verifier rejects or refuses. A suite of only accepted cases cannot tell a conforming verifier from one that accepts everything.¶
In V4 and V6, every link has subjectClass interactive, and each link's expiry is earlier than its parent's, so rule 3 of Section 7 holds.¶
| Vector | Input | Expected outcome | A verifier that gets it wrong |
|---|---|---|---|
| V4 | L(0): scope booking:read, tenureMin P2Y, credentialAgeMin P180D. L(1): scope booking:read, tenureMin P2Y, no credentialAgeMin. | reject: L(1) leaves out an axis its parent set (rule 2 of Section 7) | compares only the axes both links carry, or treats the omission as inheritance from the published floor |
| V6 | L(0): scope booking:read. L(1): scope booking:read, booking:create. L(2): scope booking:read. The floors are the same on every link. | reject: L(1) names booking:create, which L(0) did not hold (rule 1) | checks only the last pair of links, not every pair as step 6 of Section 13 requires |
| V9 | One link: actionClass r, classSource declared. Request: POST /check to https://api.example.com. Menu: signed by a key the verifier trusts for that API, declares "POST /check" as r, iss https://attacker.example.com. | refuse: the iss does not match the request target's origin, so the menu fails; the method default for POST is x, which is higher than r (Section 9.4) | checks the menu's signature but does not compare iss with the request target's origin |
| V11 | One link: actionClass x, xBudget 2. Four x-class requests on this chain; the fourth carries a chain identifier the caller chose. | admit, admit, refuse, refuse: the verifier derives the chain identifier from L(0), so the fourth request spends from the same count, which is 0 (Section 10.3) | takes the chain identifier from the request, so the fourth request gets a new count of 2 and is admitted |
The full set, V1 to V11, with the accepted cases, is in the Test Vectors appendix of draft-hamr-oauth-agent-delegation-01 (https://datatracker.ietf.org/doc/html/draft-hamr-oauth-agent-delegation-01). There, V10 and V11 use writeBudget 2; in this document that is xBudget 2.¶
These vectors do not test link signatures, nonce binding, expiry against the clock, or the header encoding. The author derived them by hand from the rules. The justabit proof-of-concept (Section 16) runs V9 as written, a two-request form of V11 with the single writeBudget of -01, and the V4 rule on classSource and writeBudget but not on credentialAgeMin. No code runs V6.¶
This section is non-normative. It will be removed before this document leaves individual submission status.¶
Shortened the whole document. Cut the worked example and the changes since -00, folded the open reconciliation item into Section 12, and cut Appendix B to four negative controls, citing the full set in -01.¶
Rewrote Section 9.6. An enforcement boundary is now a role on an effect-capable path, identified for each path, rather than a component fixed at the resource or credential boundary. Added closure, a system-wide requirement that every effect-capable path cross a qualifying boundary, with its bypass test, and stated how that differs from the complete-mediation condition of [I-D.schrock-action-evidence-boundary]. Section 13 follows the same model.¶
Corrected the layering of [I-D.schrock-ep-authorization-receipts] in Section 3: that draft gives a pre-execution approval bundle and terminal consumption evidence, and does not itself establish that the action occurred. Layer (c) is relabelled "what was approved and consumed, once". Added [I-D.schrock-action-evidence-boundary] alongside it.¶
Dropped the floor axis registry, which the registry of [I-D.asor-wimse-agent-delegation-chain] already covers, and dropped the axes accountClass and partialPolicy. Added ageMin (Section 8).¶
Defined r, w, and x by effect (Section 9); PATCH now defaults to w. Replaced the single writeBudget with one budget per class (Section 10), where no number means no limit.¶
Renamed the Resource Owner role API Provider, and stated that the party running the agent may sign its own menu (Section 9.4).¶
Rewrote Appendix A. Each direction now states what it does today and what it lacks against Section 11; the earlier claim that all of them fill the Attestation Issuer role was wrong.¶
Added measured figures from a mechanical classifier trial to Section 16, in place of an unsupported accuracy claim.¶
Named Sangam Das and Jijie Wei (varwof) in the Acknowledgments.¶
The author thanks:¶
Iman Schrock for clarifying the receipts/AEB execution boundary and bypass relationship.¶
The in-process enforcement-placement case was raised by Jijie Wei (varwof). The enforcement-boundary definition and the bypass formulation are due to Sangam Das. The per-path identification and system-wide closure split, and the framing of the boundary as a role rather than a component, were settled with both on the WIMSE and OAuth lists in September 2026.¶