| Internet-Draft | AADP Bound Permits | September 2026 |
| Saha | Expires 2 April 2027 | [Page] |
The Agent Action Decision Protocol (AADP) decides, per action, whether an agent's concrete request may proceed now, and assumes that the decision is consumed by an enforcement point on the same secured channel that issued it. AADP identifies, as a planned extension, a permit that must be honoured across a trust boundary, and defines the "present_bound" obligation as its hook. This document specifies that extension. An action-bound permit is a permit signed by the decision point, bound to one recipient, one presenter key, one HTTP request and one decided action instance, and short-lived. It composes with mandate formats such as the Agent Authorization Envelope, which carry what a recipient can evaluate for itself; the bound permit carries a decision the recipient cannot recompute, because it depends on state held by the issuer: cumulative budgets, live reservations, approval lifecycle and escalation. The document adds scoped trust in issuers, a declared currentness mode, a mandate reference, a verification order with registered refusal reasons, and a confirmation the recipient signs. It builds on existing specifications for every mechanism it can, and defines no new signature format, token format or policy language.¶
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 2 April 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.¶
A consequential request from an agent that crosses into another organization's system raises three questions, and existing work answers two of them.¶
| Question | Answered by |
|---|---|
| Who is acting, and what may it reach? | Workload identity and access tokens (for example SPIFFE, mutual TLS, OAuth 2.0) |
| What has the agent been mandated to do, within what constraints, until when? | Mandate formats such as [I-D.kroehl-agentic-trust-aae], which the relying party evaluates itself |
| Was this exact request decided, under the sender's current authorization state, and is what arrived the request that was decided? | Inside one domain, [AADP]. Across a boundary, nothing. |
The third question is the gap. A mandate evaluation of the kind AAE specifies is stateless by design: AAE lists state carried across presentations and cumulative budgets as future work. An AADP evaluation is stateful by design: a verdict depends on cumulative spend, reservations, approvals that expire, prior executions and a kill switch. A recipient in another domain holds none of that state and cannot recompute the decision. It can only verify that a decision was made, by whom, about what, and that the request it received is the one decided.¶
That is all a bound permit does. It is an envelope for one decision, not a system for cross-domain authorization, and Section 1.3 says what it leaves to others.¶
A recipient that supports this profile acts on a request only when all three of the following hold:¶
This is a conjunction. Each layer can refuse; none can overrule another. The issuer's decision never widens what the mandate allows, and the mandate never substitutes for a decision the issuer did not make.¶
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.¶
Agent --proposal--> PEP --decide--> PDP (issuer)
| <--permit + bound permit--+
|
| request + Content-Digest + Signature
| (key in cnf) + bound permit
| + Idempotency-Key
v
+--------- trust boundary ----------+
v
REP -- verify permit, binding, scope,
| currentness
| -- evaluate referenced mandate (if any)
| -- apply local policy
| -- consume (iss, jti) atomically
v
Recipient system -- effect
|
v
signed recipient confirmation --> PEP --report--> PDP
The PEP never signs the decision and the PDP never signs the request. Each signature says one thing: the issuer's says this was decided; the presenter's says this is the request, and I am the party the decision named. A compromised PEP can still substitute a request, but not silently: the substituted request will not match the decided action, and the mismatch lands in the recipient's signed record as evidence (Section 15.3).¶
A bound permit is a JSON Web Token [RFC7519] in JWS compact serialization [RFC7515], explicitly typed as [RFC8725] recommends:¶
{ "alg": "EdDSA", "kid": "<issuer key id>",
"typ": "aadp-permit+jwt" }
¶
EdDSA over Ed25519 is RECOMMENDED. A recipient MUST reject a bound permit whose "typ" is not "aadp-permit+jwt", and MUST reject any header parameter listed in "crit" that it does not implement.¶
| Claim | Presence | Meaning |
|---|---|---|
| iss | REQUIRED | The issuer. Looked up in the recipient's issuer table (Section 6). |
| sub | REQUIRED | The presenter, as an identifier the recipient can associate with the key in "cnf" (for example, a SPIFFE ID). Informational: the binding is "cnf", not "sub". |
| aud | REQUIRED | The recipient, as a single value. A permit with more than one audience MUST be rejected. It equals the value of the permit's "present_bound" obligation. |
| jti | REQUIRED | An identifier for this bound permit, unique per issuer, minted by the issuer (see below). Also the idempotency key (Section 12). |
| iat, nbf, exp | REQUIRED | Issue time, not-before and expiry (Section 3.3). |
| cnf | REQUIRED across a trust boundary | The presenter's key [RFC7800], as "jkt", a JWK thumbprint [RFC7638] as used by [RFC9449] (Section 4.2). |
| authorization_details | REQUIRED | What was decided, as typed entries [RFC9396] (Section 3.4). |
| action_digest | REQUIRED | The digest of the action object A (Section 4.3). |
| verdict | REQUIRED | Always "permit". Present so that the object states its own meaning; no other AADP verdict produces a bound permit. |
| tier | OPTIONAL | The nominal and effective autonomy tiers from the AADP decide response. |
| policy_version | RECOMMENDED | The content digest of the policy in force at the decision, as recorded in the issuer's AADP evidence. |
| mandate | OPTIONAL | A mandate reference (Section 5). |
| currentness | REQUIRED | "time-bounded" or "status-checked" (Section 7). |
| status | REQUIRED if "currentness" is "status-checked" | Where the recipient checks status (Section 7.2). |
| evidence | OPTIONAL | References into the issuer's AADP evidence record, to the presenter's preceding evidence record (for example a stage receipt [STAGE-RECEIPTS]), and to earlier hops ("upstream", Section 11.2). Carried and echoed; never required for verification and never a source of authority. |
The "jti" is not the AADP "permit_id". [AADP] Section 7 forbids presenting "permit_id" to a target resource as a credential or as a key carrying meaning there, because "permit_id" is the identity of AADP's decision and report lifecycle inside the governed domain. A bound permit is, by design, presented outside that domain, so it carries an identity of its own. The issuer MUST mint "jti" independently of "permit_id", MUST NOT make one derivable from the other, and MUST record the mapping between them in its AADP evidence entry for the decision.¶
This profile is stricter than [AADP] Section 7, which recommends that a PEP derive a target's idempotency key deterministically from the permit, for example from "permit_id". Where the target is a bound-permit recipient, "jti" is that key (Section 12), and the independent minting required above replaces the derivation Section 7 recommends. Nothing else in [AADP] Section 7 changes.¶
A claim is REQUIRED only if the recipient's verification consumes it. "policy_version", "mandate" and "evidence" are carried so that both sides' records can name them, not because the recipient can dereference them. Making a recipient depend on fields it cannot use would condition adoption on infrastructure it does not have.¶
Each entry has a "type" naming a registered action type (Section 18) and the fields that type defines. The type's registration states, for each field, its meaning, its units and its comparison rule against an issuer's scope (Section 6). For example:¶
"authorization_details": [{
"type": "payments.transfer/1",
"payee": "acme-gmbh",
"amount": { "currency": "EUR", "max": "40.00" },
"reference": "invoice-8841"
}]
¶
A recipient MUST reject a permit carrying an "authorization_details" entry whose "type" it does not implement. Unknown types fail closed, as unknown obligations do in [AADP] Section 6.¶
{
"iss": "https://pdp.payer.example",
"sub": "spiffe://payer.example/pep/accounts-payable",
"aud": "https://payments.example.net",
"jti": "bp-4f0c2a91d7e3",
"iat": 1790152800, "nbf": 1790152800, "exp": 1790152860,
"cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" },
"authorization_details": [{
"type": "payments.transfer/1",
"payee": "acme-gmbh",
"amount": { "currency": "EUR", "max": "40.00" },
"reference": "invoice-8841"
}],
"action_digest": "sha-256=:9Bh7a3W0...=:",
"verdict": "permit",
"tier": { "nominal": 1, "effective": 1 },
"policy_version": "sha-256=:3f775k...=:",
"mandate": { "type": "aae", "id": "urn:uuid:7c1e...",
"digest": "sha256:ab41..." },
"currentness": "time-bounded",
"evidence": { "evidence_id": "aud-88213" }
}
¶
A bound permit is useless if the request it accompanies can be altered. Three bindings close that: the body to its bytes, the request to the presenter's key, and the bytes to the decided action.¶
The presenter MUST send a Content-Digest field [RFC9530] computed over the exact content bytes sent, using sha-256 or a stronger registered algorithm. The recipient MUST recompute it over the bytes it received, before any parsing, decompression it did not negotiate, or re-serialization. The parsed object is not necessarily the object that was sent; [STAGE-RECEIPTS] applies the same boundary rule to external calls.¶
The presenter MUST sign the request with HTTP Message Signatures [RFC9421], using the private key whose thumbprint is the permit's "cnf.jkt". The signature MUST cover, at minimum, the components "@method", "@authority", "@path", "@query", "content-digest", "idempotency-key" and "aadp-permit", where "aadp-permit" is the field carrying the bound permit (Section 18). Covering the permit field binds the signature to this permit; covering "content-digest" binds it to the body; the derived components bind it to the target. Action types MAY require further components in their registration. Volatile, hop-by-hop or intermediary-rewritten fields MUST NOT be covered.¶
The recipient MUST verify the signature under the key identified by "cnf.jkt", and MUST reject a request whose signature verifies under any other key. Without this binding a bound permit is a bearer token: whoever holds it can present it until it expires. Bearer permits MAY be used inside one governed domain, where the channel assumptions of [AADP] Section 1.2 hold, and MUST be declared as bearer in that deployment's documentation. They MUST NOT be accepted across a trust boundary.¶
Wire bytes and meaning are different things, and the permit binds both.¶
This digest is an instance binding. [I-D.kroehl-agentic-trust-aae] Section 2.2.2 binds a grant to an action type: the values that distinguish one instance from another (amounts, recipients, sequence numbers) are kept out of its action object and travel beside it, bounded by the grant's constraints, and a binding that commits to an action instance is outside that specification, which neither defines nor forbids it. The "action_digest" here is such an instance binding: its A carries the concrete values the decision was taken about. A recipient that also evaluates an AAE therefore derives two objects from the one request it received, each bound by its own digest under its own domain tag: the mandate says the kind of action is allowed within per-action bounds; the permit says this action was decided. Neither digest can stand in for the other. An action-type registration SHOULD state both derivations, so that they are taken from the same request fields.¶
Two requests can be identical in every respect AADP evaluates -- identity, arguments, budget, approval state -- and differ only in the authority under which they are caused: an account suspension under a fraud-response mandate, and the same suspension under an employee-support mandate. The decision may be right in both cases; what fails without a reference is re-derivability, because a reader of the record cannot say under which mandate the action was authorized. This document does not define a mandate. It defines a reference to one:¶
"mandate": { "type": "aae", "id": "<credential id>",
"digest": "<mandate digest>" }
¶
Where "mandate.type" is "aae" and the AAE is presented or retrievable:¶
Where the permit names a mandate the recipient cannot obtain or evaluate, the recipient MUST refuse ("mandate-unavailable") unless its local policy explicitly treats that action type as mandate-optional, and it MUST record which it did.¶
A trust store that merely lists issuers lets any trusted issuer authorize anything the recipient's local policy fails to stop. This profile replaces the list with a table of scopes. Each entry states:¶
issuer: https://pdp.payer.example
keys: jwks_uri or pinned keys
action types: [ payments.transfer/1 ]
limits: per type, per field, using the type's comparison rule
e.g. payments.transfer/1: amount.max <= EUR 1,000.00
not_after: 2027-01-01T00:00:00Z
currentness: minimum mode accepted from this issuer
¶
A bound permit proves that a decision was taken under a policy version and, where named, a mandate. It does not by itself prove that either was still in force when the request arrived. A policy superseded, or a mandate revoked, between decision and execution is invisible to the recipient. Short lifetimes bound that window in time; they do not close it. Inside one domain the PEP can ask again; across a boundary the recipient cannot.¶
This profile does not claim to close the window by default. It requires every permit to declare which of two modes it is in, and every recipient record to state which mode applied, so that the limitation is visible in each record instead of hidden in each deployment.¶
If a "status-checked" permit's status cannot be determined -- endpoint unreachable, response unparseable, statement stale -- the recipient MUST refuse ("status-unavailable"). A recipient MAY apply an explicit, locally configured, audited fail-open policy for action types whose risk classification permits it, and MUST record that it did.¶
The REP performs these steps in order, stopping at the first failure. Cheap structural checks come before cryptography, cryptography before state, and state is consumed last, so that a request refused for any other reason does not consume its permit.¶
| # | Step | Refusal reason |
|---|---|---|
| 1 | Permit present, parses, "typ" is "aadp-permit+jwt", "crit" understood | malformed |
| 2 | "iss" in the issuer table; signature verifies under a current key for that issuer | issuer-unknown, signature-invalid |
| 3 | "aud" identifies this recipient, as a single value | audience-mismatch |
| 4 | nbf <= now <= exp within the declared skew; the rules of Section 3.3 hold | expired, not-yet-valid, lifetime-invalid |
| 5 | Every "authorization_details" type implemented | unknown-authorization-type |
| 6 | Every entry within the issuer's scope | issuer-out-of-scope |
| 7 | Content-Digest recomputed over the received bytes matches | content-mismatch |
| 8 | An HTTP message signature is present, made with the key "cnf.jkt" names, verifies, and covers the required components | request-signature-missing, presenter-key-mismatch, request-signature-invalid, binding-incomplete |
| 9 | A derived from the request; its digest equals "action_digest" | action-mismatch |
| 10 | Currentness per "currentness" (Section 7) | stale-policy, mandate-revoked, status-unavailable |
| 11 | Referenced mandate evaluated (Section 5.3) | mandate-mismatch, mandate-denied, mandate-pending, mandate-unavailable |
| 12 | Recipient-local policy permits | local-policy |
| 13 | (iss, jti) consumed atomically (Section 12) | replayed, idempotency-conflict |
Only after step 13 does the recipient perform the effect.¶
A recipient's record states, per request, one of: "verified" (every step passed); "refused", with its reason code and the step at which it stopped; or "could-not-check", with the dependency that was unavailable. There is no partial verification: a verification in which a required check was not performed is "could-not-check".¶
Refusals are returned as HTTP status 403 (409 for "idempotency-conflict", 503 for "could-not-check") with a Problem Details body [RFC9457] whose "type" is the registered reason URI (Section 18). Reason codes say which check failed. They MUST NOT disclose the recipient's policy, limits or issuer table: "issuer-out-of-scope" names the entry and field that fell outside, not the bound it exceeded. The initial set is the reason column of Table 3, plus "chained-permit-unsupported" (Section 11).¶
Each side of a cross-domain action keeps its own record; without more, a dispute is two self-authored accounts. This profile asks for one object the recipient signs and both sides keep. On completing step 13 and attempting the effect, the recipient returns a recipient confirmation, a JWS signed by the recipient, in the "AADP-Recipient-Confirmation" response field:¶
{
"typ": "aadp-confirmation+jwt",
"iss": "https://payments.example.net",
"permit": { "iss": "https://pdp.payer.example",
"jti": "bp-4f0c2a91d7e3",
"digest": "sha-256=:<digest of the permit's compact
serialization>:" },
"request": { "content_digest": "sha-256=:...:",
"signature_base_digest": "sha-256=:<digest of the
RFC 9421 signature base>:" },
"action_digest": "sha-256=:...:",
"mandate_verdict": { "type": "aae", "core_digest": "sha256:..." },
"outcome": "effected | refused | effect-unknown",
"reason": "<reason code if refused>",
"recipient_action_id": "TX-77120",
"iat": 1790152803
}
¶
A recipient that refuses at any step SHOULD still return a confirmation with "outcome": "refused" and the reason. A refusal the recipient signs is evidence; a refusal it merely returns is an assertion.¶
A bound permit authorizes one request, to one audience, by one presenter. This version of the profile is single-hop:¶
A capability declared unsupported must be refused by the code, not merely left out of the text. Chained permits, in which an intermediary derives a permit from one it received, are out of scope for this version; any future design would be expected to build on existing delegation mechanisms such as [RFC8693].¶
Two patterns cover most multi-party cases without a chain. Both are informative: they use only what this document already defines.¶
When one piece of work is carried out as several requests -- an orchestrating agent that hands sub-tasks to sub-agents, or a batch of payments executed by several workers -- each request is decided by the issuer on its own, and the issuer issues one bound permit per request. Each permit names its own audience, its own presenter key in "cnf" (the sub-agent's or worker's key, not the orchestrator's), its own "authorization_details" and "action_digest", and its own "jti".¶
Budgets are then enforced where they live. Each sub-request reserves against the issuer's cumulative budgets under [AADP] Section 4, so the total across all sub-requests is bounded by the same budgets that bound any other set of actions, and each reservation is resolved by its own report. No party other than the issuer derives or narrows a permit, and no recipient has to verify more than one issuer signature.¶
When a recipient in one domain acts, in turn, towards a further domain -- A's request causes B to call C -- B takes its own decision and presents its own bound permit to C, as above. C may still want to know what caused B's request. B's issuer MAY include, in the permit's "evidence" claim, an "upstream" member: an array of references to earlier hops, each of the form { "iss", "jti", "digest" }, where "digest" is the digest of that hop's bound permit or of the recipient confirmation B returned for it (Section 10).¶
Without "cnf" and the request signature, a bound permit is a bearer token. Section 4.2 forbids bearer permits across a trust boundary. The single most likely deployment mistake is accepting the permit without verifying the request signature; conformance vectors for it are mandatory (Section 17).¶
A compromised issuer key forges permits until the recipient's table stops trusting it. Lifetimes are bounded (Section 3.3), and "status-checked" permits can be revoked at the status list. A compromised presenter key lets an attacker present permits already issued to that presenter, within their lifetimes, to their audiences, for their decided actions only.¶
A presenter that holds direct capability to the recipient can bypass this profile entirely; [AADP] Section 1.2 already disclaims that case. What this profile changes is the case in which the presenter uses the profile and substitutes a request: the substituted request fails "action-mismatch" or "content-mismatch", and the recipient's signed refusal records a decided action and a presented action that differ. That is not prevention. It turns a silent substitution into evidence that names both sides.¶
An upstream reference is written by the issuer of the permit that carries it and is not verified by this profile. A recipient that granted it authority would accept, from a compromised or careless issuer, a claim about another domain's decision that no one checked. Section 11.2 therefore makes it evidence only.¶
Clock skew is bounded and recorded (Section 3.3). A recipient MUST NOT extend "exp" by its skew allowance beyond the "execute_within" deadline.¶
A proxy that re-encodes the body, rewrites the path or strips unknown fields will cause refusals by design. Deployments with such intermediaries either terminate the profile at the intermediary, which then becomes the recipient with its own record, or configure it to pass covered components untouched.¶
A request that policy should not have permitted; a recipient that lies in its confirmation; issuer and recipient in collusion; a mandate that was wrongly granted. Each needs controls outside this document.¶
A bound permit discloses, to its recipient, the decided action, the presenter's identifier, the issuer, a policy version digest and optionally a mandate reference. It discloses no policy content and no other action. "authorization_details" SHOULD carry the minimum fields the action type needs. Recipient confirmations disclose the recipient's action identifier to the presenter. Digests of personal data SHOULD NOT be used as identifiers where the data is guessable.¶
Each vector is a refusal with a named reason, and each is paired with the valid case beside it, so that a verifier that refuses everything does not pass. Where a rule has an absent case and an empty case, both are vectors.¶
| # | Mutation | Expected |
|---|---|---|
| V01 | Body changed after signing | content-mismatch |
| V02 | Path changed after signing | request-signature-invalid |
| V03 | Presented to a different recipient | audience-mismatch |
| V04 | "aud" carries two values | audience-mismatch |
| V05 | Presented after "exp" | expired |
| V06 | "exp" later than "execute_within" | lifetime-invalid |
| V07 | Same permit presented twice, same body | stored result returned; recorded as a repeat |
| V08 | Same "jti", different body | idempotency-conflict |
| V09 | Request signed by a key other than "cnf.jkt" | presenter-key-mismatch |
| V10 | Request not signed at all (bearer presentation) | request-signature-missing |
| V11 | Signature omits "content-digest" from the covered components | binding-incomplete |
| V12 | Body re-serialized with the same meaning, different bytes | content-mismatch |
| V13 | Same bytes, A derived differently from what was decided | action-mismatch |
| V14 | "authorization_details" type unknown to the recipient | unknown-authorization-type |
| V15 | Issuer trusted, action type outside its scope | issuer-out-of-scope |
| V16 | Issuer trusted for the type, amount above its limit | issuer-out-of-scope |
| V17 | "status-checked", policy version since superseded | stale-policy |
| V18 | "status-checked", status endpoint unreachable | status-unavailable (could-not-check) |
| V19 | Mandate digest differs from the evaluated AAE | mandate-mismatch |
| V20 | Referenced AAE evaluates to PENDING | mandate-pending |
| V21 | Permit carries "parent" | chained-permit-unsupported |
| V22 | Recipient confirmation names a different request digest | detected by the presenter; the report records the discrepancy |
| V23 | Consume store unavailable | could-not-check, not a policy denial |
| V24 | Permit valid, local policy denies | local-policy, and the permit is not consumed |
This document requests the following registrations. Registration procedures and templates will be completed in a later revision.¶
The form a record-reading tool should use when a bound-permit request fails, stating what is and is not known:¶
Action: payments.transfer/1
First failure: content-mismatch (step 7)
Established: Permit bp-4f0c2a91d7e3 decided EUR 40.00 to
acme-gmbh. The recipient received a body whose
digest does not match. The recipient refused
before any effect, and signed the refusal.
Not established: Which component changed the body.
Whether the cause was an attacker or a defect.
Evidence: permit digest, request content digest,
signature base digest, recipient confirmation,
the sending stage's record
¶
[Note to the RFC Editor: please remove this section before publication, as described in RFC 7942.]¶
This section records the status of known implementations of this profile at the time of posting, following [RFC7942]. Listing an implementation here is not an endorsement.¶