<?xml version="1.0" encoding="utf-8"?>
<?xml-model href="rfc7991bis.rnc"?>
<rfc xmlns:xi="http://www.w3.org/2001/XInclude"
     docName="draft-saha-aadp-00"
     category="std"
     ipr="trust200902"
     submissionType="IETF"
     tocInclude="true"
     sortRefs="true"
     symRefs="true"
     version="3">

  <front>
    <title abbrev="AADP">The Agent Action Decision Protocol (AADP): Per-Action Authorization for AI Agents</title>
    <seriesInfo name="Internet-Draft" value="draft-saha-aadp-00"/>
    <author fullname="Shamik Saha" initials="S." surname="Saha">
      <organization>Independent</organization>
      <address>
        <postal>
          <city>Amsterdam</city>
          <country>Netherlands</country>
        </postal>
        <email>shamik.saha.rcciit@gmail.com</email>
      </address>
    </author>
    <date year="2026" month="August" day="17"/>
    <area>sec</area>
    <keyword>AI agents</keyword>
    <keyword>authorization</keyword>
    <keyword>policy</keyword>
    <keyword>guardrails</keyword>

    <abstract>
      <t>Existing agent-security work concentrates on identity: who an agent
      is, what credentials it holds, and which tools it may reach. This
      document addresses the complementary question: whether a specific
      proposed action, with specific argument values, may be performed now,
      and under what conditions. It defines the Agent Action Decision
      Protocol (AADP), a two-phase wire contract between a Policy Decision Point (PDP)
      that authorizes agent actions and the Policy Enforcement Points (PEPs)
      that perform them. AADP specifies verdicts with machine-readable
      reasons, obligations that fail closed, budget reservation semantics,
      approval and idempotency behavior, evidence requirements, and a set of
      evaluation invariants that any conformant decision point must observe
      -- including the rule that an irreversible action is never executed
      autonomously. The protocol is transport-agnostic and is designed so
      that decision points and enforcement points can be implemented
      independently, in different languages, by different parties.</t>
    </abstract>
  </front>

  <middle>

    <section anchor="intro">
      <name>Introduction</name>
      <t>AI agents increasingly perform consequential actions: moving money,
      modifying data, sending communications, executing code, and
      provisioning infrastructure. The frameworks that host these agents
      typically decide what an agent can do by controlling which tools are
      exposed to it. Recent analysis has shown that this conflates two
      distinct questions: capability (whether an agent can invoke a tool)
      and authorization (whether this call, with these argument values,
      should be performed now) <xref target="SCOPEGATE"/>. A tool that is
      legitimately exposed for one purpose can be invoked for another; an
      agent authorized to make one payment can attempt a second; and a
      generic capability such as an HTTP client can reach effects its name
      does not declare.</t>

      <t>Parallel work standardizes agent identity and credential flows
      <xref target="I-D.klrc-aiagent-auth"/>
      <xref target="I-D.ni-wimse-ai-agent-identity"/> and hardens the
      authorization of protocol connections <xref target="MCP"/>. That work
      answers who the agent is and what it may reach; the WIMSE
      applicability work states explicitly that whether an agent inherits
      its user's permissions is out of its scope. It deliberately leaves
      per-action concerns -- budget enforcement, obligation tracking,
      approval lifecycle -- as deployment-specific. AADP specifies exactly
      that remainder: a small, implementable contract for per-action
      authorization decisions, designed to compose with identity-layer
      standards rather than replace them.</t>
      <t>Closer to this document,
      <xref target="I-D.liu-agent-operation-authorization"/> authorizes
      individual operations delegated from a human principal, carrying
      policy as Rego, signed confirmation evidence, and a delegation chain
      across multiple agents. It addresses a different remainder: consent
      and delegation lineage. It does not constrain cumulative spend, and
      the approval it records is a binary confirmation rather than a
      lifecycle with expiry and re-evaluation. AADP is complementary to it:
      the state that must persist between calls -- budgets shared across
      differently-named tools, reservations that outlive a decision,
      approvals that can expire -- is what this document specifies, and it
      cannot be expressed by evaluating one call at a time however the
      policy is written.</t>

      <t>The Policy Decision Point / Policy Enforcement Point split, and the
      concept of obligations attached to a permission, originate in XACML
      <xref target="XACML"/>. AADP inherits that vocabulary deliberately and
      differs in what it standardizes: agent-action semantics that stateless
      policy evaluation cannot express -- stateful budget reservation shared
      across differently-named tools, autonomy tiers with reversibility as a
      precondition, a two-phase exchange separated by a durability boundary,
      and typed evidence sufficient to re-derive every verdict.</t>

      <section anchor="scope">
        <name>Scope</name>
        <t>AADP specifies: the request an enforcement point sends to ask
        permission; the answers a decision point may return; the obligation
        that accompanies a permission; the report that must follow an
        attempted action; the ordered evaluation invariants a conformant
        decision point must observe; and idempotency, approval, and failure
        semantics.</t>
        <t>AADP does not specify a policy language, a storage engine, an
        approval user interface, or a transport. Policy composition -- tiers,
        effect labels, budgets, parameter bounds -- is internal to the
        decision point. Only the verdict and its reason cross the wire.</t>
      </section>

      <section anchor="reqlang">
        <name>Requirements Language</name>
        <t>The key words "<bcp14>MUST</bcp14>", "<bcp14>MUST NOT</bcp14>",
        "<bcp14>REQUIRED</bcp14>", "<bcp14>SHALL</bcp14>", "<bcp14>SHALL
        NOT</bcp14>", "<bcp14>SHOULD</bcp14>", "<bcp14>SHOULD NOT</bcp14>",
        "<bcp14>RECOMMENDED</bcp14>", "<bcp14>NOT RECOMMENDED</bcp14>",
        "<bcp14>MAY</bcp14>", and "<bcp14>OPTIONAL</bcp14>" in this document
        are to be interpreted as described in BCP 14
        <xref target="RFC2119"/> <xref target="RFC8174"/> when, and only
        when, they appear in all capitals, as shown here.</t>
      </section>
    </section>

    <section anchor="roles">
      <name>Roles</name>
      <dl>
        <dt>Policy Decision Point (PDP):</dt>
        <dd>Evaluates a proposed action and returns a verdict. Owns policy,
        budget state, approval state, and the evidence record. There is
        exactly one logical PDP per governed domain.</dd>
        <dt>Policy Enforcement Point (PEP):</dt>
        <dd>Sits wherever actions actually happen -- an agent-framework tool
        wrapper, a protocol proxy, an API gateway filter, a container or
        micro-VM supervisor, a workflow engine. A PEP <bcp14>MUST NOT</bcp14>
        perform a governed action without a permit, and <bcp14>MUST</bcp14>
        report the outcome of every permit it receives.</dd>
      </dl>
      <t>A deployment <bcp14>MAY</bcp14> have many PEPs consulting one PDP.
      Every PEP is bound by the same contract.</t>
    </section>

    <section anchor="model">
      <name>Protocol Model</name>

      <section anchor="tiers">
        <name>Autonomy Tiers</name>
        <t>Tiers are ordered integers; higher means less autonomy.</t>
        <table anchor="tiertable">
          <name>Autonomy Tiers</name>
          <thead>
            <tr><th>Tier</th><th>Name</th><th>Meaning</th></tr>
          </thead>
          <tbody>
            <tr><td>0</td><td>observe</td><td>read-only; no external effect</td></tr>
            <tr><td>1</td><td>auto</td><td>may execute automatically; MUST be reversible</td></tr>
            <tr><td>2</td><td>auto_capped</td><td>may execute automatically within budgets</td></tr>
            <tr><td>3</td><td>confirm</td><td>requires human approval before execution</td></tr>
          </tbody>
        </table>
        <t>A verdict carries both the nominal tier (configured for the
        action) and the effective tier (after escalation). When they differ,
        the action was escalated, and the reason code states why. PDPs
        <bcp14>MUST</bcp14> report both.</t>
      </section>

      <section anchor="verdicts">
        <name>Verdicts</name>
        <table anchor="verdicttable">
          <name>Verdicts and PEP Obligations</name>
          <thead>
            <tr><th>Verdict</th><th>Meaning</th><th>PEP obligation</th></tr>
          </thead>
          <tbody>
            <tr><td>permit</td><td>the action may proceed now</td>
                <td>execute, then report exactly once</td></tr>
            <tr><td>deny</td><td>the action must not proceed</td>
                <td>do not act; no report</td></tr>
            <tr><td>propose</td><td>a human must approve first</td>
                <td>do not act; surface the approval id</td></tr>
            <tr><td>dry_run</td><td>rehearsal only; no real effect</td>
                <td>do not act; MAY simulate</td></tr>
            <tr><td>observe</td><td>read classified as no-effect</td>
                <td>act freely; no report required</td></tr>
            <tr><td>replay</td><td>this request id was already decided</td>
                <td>honour the original verdict; do not act again</td></tr>
          </tbody>
        </table>
      </section>

      <section anchor="reasons">
        <name>Reason Codes</name>
        <t>Every verdict <bcp14>MUST</bcp14> carry exactly one
        machine-readable reason. Conformant PDPs <bcp14>MUST</bcp14> use
        the identifiers in <xref target="reasontable"/> where they apply,
        and <bcp14>MAY</bcp14> define additional codes under a vendor
        prefix ("x-" followed by a vendor token and a dot).</t>
        <table anchor="reasontable">
          <name>Reason Codes</name>
          <thead>
            <tr><th>Code</th><th>Meaning</th></tr>
          </thead>
          <tbody>
            <tr><td>passed</td><td>all checks satisfied</td></tr>
            <tr><td>kill_switch</td><td>the global stop is engaged</td></tr>
            <tr><td>default_deny</td><td>no policy exists for this action type</td></tr>
            <tr><td>bounds</td><td>a parameter violated its declared bounds</td></tr>
            <tr><td>cap_daily_rate</td><td>a rate budget is exhausted</td></tr>
            <tr><td>cap_eur_day</td><td>a daily value budget is exhausted</td></tr>
            <tr><td>cap_eur_month</td><td>a monthly value budget is exhausted</td></tr>
            <tr><td>dry_run</td><td>the action is in rehearsal mode</td></tr>
            <tr><td>tier_confirm</td><td>policy requires human approval</td></tr>
            <tr><td>effect_floor</td><td>an effect class imposes a stricter tier than the action's own</td></tr>
            <tr><td>no_compensating_command</td><td>the action is irreversible and cannot auto-execute</td></tr>
            <tr><td>observe</td><td>the action is a governed read</td></tr>
            <tr><td>malformed</td><td>the request could not be parsed or validated</td></tr>
          </tbody>
        </table>
      </section>
    </section>

    <section anchor="exchange">
      <name>The Two-Phase Exchange</name>
      <t>AADP is a two-phase protocol. The phases are separated by a
      durability boundary because the act between them may be slow, remote,
      or fatal.</t>
      <artwork align="center"><![CDATA[
 PEP                              PDP
  |  decide(request)               |
  |------------------------------->|  evaluate, reserve budget,
  |                                |  record intent, COMMIT
  |<-------------------------------|
  |  permit(permit_id, obligations)|
  |                                |
 [ PEP performs the action ]       |
  |                                |
  |  report(permit_id, outcome)    |
  |------------------------------->|  record linked result, COMMIT
  |<-------------------------------|
]]></artwork>
      <t>A PEP that receives a permit <bcp14>MUST</bcp14> send exactly one
      report, whatever happened -- success, failure, timeout, or refusal to
      act. A PDP <bcp14>MUST</bcp14> treat a permit with no report as an
      unresolved intent and <bcp14>MUST NOT</bcp14> release its budget
      reservation implicitly.</t>
      <t>An unresolved intent therefore holds budget that no action ever
      spent, and a caller that requests permits and never reports can
      exhaust a budget at the cost of one decide call per permit, leaving
      no effect at the sink to detect. Reservations <bcp14>MUST</bcp14>
      therefore be reclaimable explicitly rather than only implicitly: a
      PDP that issues a permit carrying "execute_within"
      (<xref target="obligations"/>) <bcp14>MUST</bcp14>, once that
      deadline has passed with no report, append an expiry entry to the
      evidence record and release that permit's reservation. The release
      is an audited event, not a silent timeout, and the permit is void
      from that point. A PDP <bcp14>SHOULD</bcp14> attach "execute_within"
      to every permit that reserves budget; one that does not
      <bcp14>MUST</bcp14> document how reservations are otherwise
      reclaimed.</t>
      <t>Combining the two phases into a single call is not conformant: a
      decision point that waits for the act holds state across an operation
      it does not control.</t>
    </section>

    <section anchor="messages">
      <name>Messages</name>
      <t>All messages are JSON objects <xref target="RFC8259"/>. Timestamps
      are <xref target="RFC3339"/> in UTC. Monetary values are decimal
      strings (for example, "12.50"), never floating-point numbers.</t>

      <section anchor="decidereq">
        <name>Decide Request</name>
        <sourcecode type="json"><![CDATA[
{
  "protocol": "aadp/0.1",
  "request_id": "0a5f2c3e-1b7e-4f2a-9c1d-6b0f9e2a77c1",
  "action_type": "payments.transfer",
  "params": { "payee": "acme-gmbh", "amount_eur": "40.00" },
  "source": "llm",
  "rationale": "user asked to settle invoice 8841",
  "session_id": "sess-91f3",
  "cost_eur": "40.00",
  "created_at": "2026-08-17T09:14:02Z",
  "parent_id": null
}
]]></sourcecode>
        <t>Field requirements: "protocol", "request_id" (a UUID; the
        idempotency key, see <xref target="idem"/>), "action_type" (an
        opaque identifier; the PDP's policy key), "params" (a JSON object,
        possibly empty, treated as untrusted input), "source" (one of
        "scheduler", "rule", "llm", "ui", "undo", "system"), "rationale"
        (human-readable justification, recorded as evidence), and
        "created_at" are <bcp14>REQUIRED</bcp14>. "session_id" and
        "cost_eur" are <bcp14>RECOMMENDED</bcp14>; "cost_eur" is
        <bcp14>REQUIRED</bcp14> when value budgets apply. "parent_id"
        identifies the permit being reversed when "source" is "undo".
        "approval_ref" carries the "approval_id" of a granted approval when
        a PEP itself resumes a previously proposed action
        (<xref target="approvals"/>); it is <bcp14>REQUIRED</bcp14> in that
        case and <bcp14>MUST</bcp14> be absent otherwise. A PDP
        <bcp14>MUST</bcp14> verify that an "approval_ref" refers to an
        approval that is granted, unexpired, unconsumed, and raised for an
        action equivalent to the one now requested; if it does not, the
        request is evaluated as though no approval had been supplied.</t>
        <t>"source" is informational. A conformant PDP <bcp14>MUST
        NOT</bcp14> let it change the verdict: governance binds to the
        action, not to who proposed it.</t>
        <t>Evaluation is total with respect to its input. A PDP that
        receives a request it cannot parse or validate -- a missing
        required field, a value of the wrong type, a malformed identifier,
        an unparseable body -- <bcp14>MUST</bcp14> return a "deny" verdict
        with reason "malformed", and <bcp14>MUST NOT</bcp14> signal the
        failure only as a transport-level or runtime error. Fail-closed
        behavior is a property of the decision point; a PDP that raises
        instead of deciding delegates that property to each enforcement
        point, where it holds only by convention. Where the supplied
        "request_id" is itself well-formed the response
        <bcp14>SHOULD</bcp14> echo it; where it is not, the response
        <bcp14>MUST NOT</bcp14> invent one. A single reserved sentinel value
        denoting "no valid identifier was supplied" is not an invented
        identifier and <bcp14>MAY</bcp14> be used for this purpose.</t>
        <t>The same totality applies to a quantity a check requires. A
        budget or cap that cannot be evaluated because the request resolves
        no amount where one is required -- absent, unparseable, or
        non-finite -- <bcp14>MUST</bcp14> deny with the reason of the check
        that required it, and <bcp14>MUST NOT</bcp14> proceed against an
        assumed zero. An amount the PDP cannot determine is not a zero
        amount; treating it as one silently disables the control.</t>
        <t>This requirement governs a request that reached the decision
        function: one the PDP received and attempted to evaluate. A "deny"
        verdict with reason "malformed" is an evaluation outcome, and is
        honored as any other verdict is, independent of the enforcement
        point's failure-mode configuration (<xref target="failure"/>). A
        request that fails before the decision function -- rejected by the
        transport or by intermediary infrastructure the PDP does not
        control, such as a body that is not well-formed for the binding in
        use -- is a protocol failure rather than a verdict; its transport-
        level treatment is described in <xref target="http"/>, and it is
        subject to <xref target="failure"/> with the constraint stated
        there.</t>
        <t>This requirement covers invalid input, not internal failure. A
        PDP that cannot evaluate because its own policy store, evidence
        record or budget ledger is unavailable <bcp14>SHOULD NOT</bcp14>
        report that condition as a routine denial, since that makes a
        defect indistinguishable from a policy outcome; such a PDP is
        unreachable for this request, and <xref target="failure"/> governs
        what the enforcement point does next.</t>
      </section>

      <section anchor="decideresp">
        <name>Decide Response</name>
        <sourcecode type="json"><![CDATA[
{
  "protocol": "aadp/0.1",
  "request_id": "0a5f2c3e-1b7e-4f2a-9c1d-6b0f9e2a77c1",
  "verdict": "permit",
  "reason": "passed",
  "detail": "",
  "nominal_tier": 1,
  "effective_tier": 1,
  "permit_id": "pmt-h8Qk2mAoT7xY",
  "obligations": [
    { "type": "report_result", "required": true },
    { "type": "undo_available_until",
      "value": "2026-08-17T09:29:02Z" }
  ],
  "approval_id": null,
  "evidence_id": "aud-88213"
}
]]></sourcecode>
        <t>"permit_id" is present if and only if the verdict is "permit".
        "approval_id" is present if and only if the verdict is "propose".
        "evidence_id" references the entry in the PDP's evidence record and
        <bcp14>SHOULD</bcp14> be present for every verdict.</t>
        <t>Where no policy resolved -- a request denied with reason
        "malformed", or an unknown action denied with reason
        "default_deny" -- no policy assigned a tier. The tier fields
        <bcp14>MUST</bcp14> then carry the most restrictive tier
        ("confirm"), and the reason code, not the tier, is authoritative
        for such a verdict. A recipient <bcp14>MUST NOT</bcp14> infer that
        an action was contemplated from a tier that no policy assigned.</t>
      </section>

      <section anchor="reportreq">
        <name>Report Request and Response</name>
        <sourcecode type="json"><![CDATA[
{
  "protocol": "aadp/0.1",
  "permit_id": "pmt-h8Qk2mAoT7xY",
  "outcome": "success",
  "payload": { "confirmation": "TX-77120" },
  "error": null,
  "completed_at": "2026-08-17T09:14:03Z"
}
]]></sourcecode>
        <t>"outcome" <bcp14>MUST</bcp14> be one of "success", "failure",
        "timeout", or "not_attempted". "not_attempted" is the correct value
        when a PEP declines to act, for example because it cannot satisfy
        an obligation (<xref target="obligations"/>).</t>
        <sourcecode type="json"><![CDATA[
{ "protocol": "aadp/0.1", "accepted": true,
  "evidence_id": "aud-88214" }
]]></sourcecode>
      </section>
    </section>

    <section anchor="obligations">
      <name>Obligations</name>
      <t>An obligation is a condition attached to a permit that the PEP
      <bcp14>MUST</bcp14> satisfy for the permission to be valid.
      Obligations are typed and extensible; they are the protocol's
      extension point for enforcement capabilities the PDP itself cannot
      provide.</t>
      <table anchor="obltable">
        <name>Obligation Types</name>
        <thead>
          <tr><th>Type</th><th>Meaning</th></tr>
        </thead>
        <tbody>
          <tr><td>report_result</td><td>the PEP MUST send exactly one report (always implied)</td></tr>
          <tr><td>undo_available_until</td><td>a reversal window; the PEP SHOULD retain what is needed to reverse</td></tr>
          <tr><td>execute_within</td><td>a deadline after which the permit is void</td></tr>
          <tr><td>isolate</td><td>the action MUST be performed inside an isolation boundary meeting the stated profile</td></tr>
        </tbody>
      </table>
      <t>Unknown obligations fail closed. A PEP that receives an obligation
      whose type it does not recognize <bcp14>MUST NOT</bcp14> perform the
      action, and <bcp14>MUST</bcp14> report "not_attempted" with an error
      naming the unsupported type. A PEP <bcp14>MUST NOT</bcp14> silently
      ignore an obligation: a policy author has to be able to assume that
      an issued obligation was either honoured or visibly refused.</t>
      <t>Where an obligation admits evidence of its discharge (for example,
      the identity of the isolation boundary used), the PEP
      <bcp14>SHOULD</bcp14> include that evidence in its report payload, so
      that the discharge is recorded alongside the decision.</t>
      <t>Vendor-specific obligations <bcp14>MUST</bcp14> use a vendor
      prefix as in <xref target="reasons"/>.</t>
    </section>

    <section anchor="idem">
      <name>Idempotency and Replay</name>
      <t>"request_id" is the idempotency key. A PDP that receives a decide
      request for a request_id it has already decided <bcp14>MUST</bcp14>
      return the original verdict with "verdict" and "reason" unchanged,
      and <bcp14>MUST NOT</bcp14> re-reserve any budget. Implementations
      <bcp14>MAY</bcp14> additionally set "replayed": true.</t>
      <t>A "permit_id" and an "approval_id" are capability identifiers:
      possession of one is sufficient to report an outcome for a permit, or
      to resume an approved action. They <bcp14>MUST</bcp14> therefore be
      unpredictable -- infeasible to guess from other issued identifiers --
      so that a party holding only a decide credential cannot forge a
      result, or a resumption, for an exchange it did not conduct. A
      sequential or otherwise enumerable identifier does not satisfy this
      requirement. Single-use bounds replay of a known identifier;
      unpredictability bounds forging of an unknown one. Where a deployment
      cannot guarantee unpredictable identifiers, the report and resume
      channels <bcp14>MUST</bcp14> instead bind each identifier to the
      authenticated identity that received it, and reject any report or
      resumption presented under a different identity.</t>
      <t>A "permit_id" is single-use. A second report for the same permit_id
      <bcp14>MUST</bcp14> be rejected with "accepted": false.</t>
      <t>Resuming an approved action (<xref target="approvals"/>) is a new
      decision with a new request_id; reusing the proposed request's id
      would return the "propose" verdict forever.</t>
    </section>

    <section anchor="approvals">
      <name>Approvals</name>
      <t>When the verdict is "propose", the PDP creates an approval record
      with a deadline and returns "approval_id". Approval state transitions
      are pending to approved, denied, or expired.</t>
      <t>A conformant PDP <bcp14>MUST</bcp14>: transition pending to
      approved atomically, rejecting a second approval and any approval
      after the deadline; record who approved and when; and re-evaluate the
      action on resumption. An approval authorizes the action, not the
      outcome: a kill switch engaged between approval and resumption
      <bcp14>MUST</bcp14> still deny.</t>
      <t>A granted approval is single-use: the PDP <bcp14>MUST</bcp14>
      mark it consumed when a resumed request is decided against it, so
      that one human decision cannot authorize repeated actions.</t>
      <t>Resumption may be driven from either side, and both forms are
      conformant provided the guarantees above hold. In PEP-driven
      resumption, the PEP issues a fresh decide request carrying
      "approval_ref". In PDP-driven resumption, granting the approval is
      itself the resumption: the PDP re-evaluates the recorded request
      under a fresh request_id and returns a decide response. A PDP
      <bcp14>MUST</bcp14> document which form it implements, and
      <bcp14>MUST NOT</bcp14> let either form bypass re-evaluation.</t>
      <t>Approval transport -- who is notified, and how -- is out of
      scope.</t>
    </section>

    <section anchor="invariants">
      <name>Evaluation Invariants</name>
      <t>This section is the normative core. Two conformant PDPs with the
      same policy <bcp14>MUST</bcp14> reach the same verdict for the same
      request. Implementations <bcp14>MAY</bcp14> optimize freely --
      caching, compiling, reordering pure checks -- provided these
      observable orderings hold.</t>
      <ol>
        <li>The kill switch is evaluated first, before policy lookup. An
        engaged kill switch <bcp14>MUST NOT</bcp14> be overridable by a
        policy entry.</li>
        <li>Unknown actions are denied. Absence of policy is a denial with
        reason "default_deny", never a permission.</li>
        <li>Irreversibility escalates. An action at tier "auto" with no
        registered means of reversal <bcp14>MUST NOT</bcp14> auto-execute;
        it escalates to "confirm" with reason
        "no_compensating_command".</li>
        <li>Bounds are validated before proposal. A human <bcp14>MUST
        NOT</bcp14> be asked to approve an action whose parameters already
        violate policy.</li>
        <li>Rehearsals do not spend. If an action is in dry-run, the PDP
        <bcp14>MUST NOT</bcp14> reserve any budget.</li>
        <li>Budget reservation is atomic and all-or-nothing. Where an
        action draws on several budgets, all <bcp14>MUST</bcp14> be checked
        before any is reserved, and a failure on any <bcp14>MUST</bcp14>
        leave every counter untouched. Concurrent requests <bcp14>MUST
        NOT</bcp14> be able to over-reserve a budget.</li>
        <li>Escalation is recorded, not hidden. When effective tier exceeds
        nominal tier, the reason <bcp14>MUST</bcp14> identify the
        escalating check.</li>
        <li>Reversal is governed. An undo is itself an action subject to
        the full pipeline. There is no privileged path.</li>
        <li>Intent precedes action. The evidence record of a permit
        <bcp14>MUST</bcp14> be durable before the permit is returned. A
        crash between phases <bcp14>MUST</bcp14> leave a recoverable,
        detectable state: an intent with no result.</li>
      </ol>
    </section>

    <section anchor="evidence">
      <name>Evidence</name>
      <t>A conformant PDP <bcp14>MUST</bcp14> maintain an append-only
      record in which each entry carries at minimum: the request, the
      verdict, the reason code, both tiers, a timestamp, and -- for results
      -- a link to the intent it resolves.</t>
      <t>Entries <bcp14>MUST NOT</bcp14> be updated or deleted. Corrections
      are appended, never applied in place. Implementations
      <bcp14>SHOULD</bcp14> enforce this structurally (storage-level
      constraints), not only by convention.</t>
      <t>The durability an intent must have before its permit is returned
      (invariant 9) means, at minimum, survival of the failure of the PDP
      process: a permit that was returned <bcp14>MUST</bcp14> be
      recoverable after the deciding process restarts. Whether the record
      also survives the failure of the host -- power loss, operating-system
      crash -- is a deployment property a PDP <bcp14>MUST</bcp14> document,
      since it determines whether an intent can be lost in a window the
      enforcement point cannot observe. Returning a permit whose intent is
      not durable to the documented level is non-conformant.</t>
      <t>The record <bcp14>MUST</bcp14> be sufficient to re-derive every
      verdict it contains, given the policy version in force at the time.
      A PDP <bcp14>MUST</bcp14> therefore identify, on every entry, the
      policy that produced it, and <bcp14>MUST</bcp14> retain that policy
      for as long as the entries referring to it are retained. Identifying
      the policy by a content digest of its normative content is
      <bcp14>RECOMMENDED</bcp14>, so that an unchanged policy keeps one
      identity and a reverted change is recognisable as a return to an
      earlier one rather than as a third state.</t>
      <t>This requirement exists because policy is usually stored mutably.
      A decision point that overwrites policy in place, and records only
      the verdict, has an evidence record that cannot be checked: it
      cannot show that a permit issued last week was correct under last
      week's rules, and an operator who loosens policy, acts, and restores
      it leaves nothing behind. Retaining superseded policy is what makes
      the rest of this section meaningful.</t>
    </section>

    <section anchor="failure">
      <name>Failure Semantics</name>
      <t>If a PEP cannot reach the PDP, its behavior is a deployment policy
      that <bcp14>MUST</bcp14> be configured explicitly, not defaulted
      silently:</t>
      <dl>
        <dt>fail_closed (RECOMMENDED):</dt>
        <dd>perform no governed action.</dd>
        <dt>fail_static:</dt>
        <dd>permit only actions on a locally cached list of statically
        decidable, unbudgeted, tier-0/1 permissions, and queue evidence for
        later delivery.</dd>
        <dt>fail_open:</dt>
        <dd>perform the action. Conformant implementations <bcp14>MUST
        NOT</bcp14> make this the default and <bcp14>SHOULD</bcp14> require
        it to be named in configuration.</dd>
      </dl>
      <t>A PEP operating under fail_static <bcp14>MUST</bcp14> deliver
      queued evidence when the PDP becomes reachable, and <bcp14>MUST
      NOT</bcp14> locally permit any action that would require a budget, an
      approval, or an obligation.</t>
      <t>These modes govern a PDP that could not be reached. A failure that
      indicates the request itself was not well-formed -- a transport or
      parse failure of the kind described in <xref target="decidereq"/> and
      <xref target="http"/> -- <bcp14>MUST</bcp14> be treated as fail_closed
      regardless of the configured mode: such a request <bcp14>MUST
      NOT</bcp14> be performed under fail_open or fail_static. A malformed
      request must not become a permitted action by way of a failure
      policy, just as it must not by way of the decision function.</t>
    </section>

    <section anchor="transport">
      <name>Transport Bindings</name>
      <t>The core protocol is transport-agnostic. Two bindings are
      defined.</t>
      <section anchor="http">
        <name>HTTP/JSON Binding</name>
        <table anchor="httptable">
          <name>HTTP/JSON Operations</name>
          <thead>
            <tr><th>Operation</th><th>Method and path</th></tr>
          </thead>
          <tbody>
            <tr><td>decide</td><td>POST /v1/decide</td></tr>
            <tr><td>report</td><td>POST /v1/report</td></tr>
            <tr><td>list approvals</td><td>GET /v1/approvals</td></tr>
            <tr><td>approve / deny</td><td>POST /v1/approvals/{id}/approve, POST /v1/approvals/{id}/deny</td></tr>
            <tr><td>kill switch</td><td>POST /v1/killswitch</td></tr>
            <tr><td>health</td><td>GET /v1/health</td></tr>
          </tbody>
        </table>
        <t>Authentication is bearer-token by default, with at minimum two
        roles: a decide role (decide, report) and an admin role (approve,
        deny, kill switch, policy reload). Separation of duties is a
        governance property and <bcp14>MUST NOT</bcp14> be collapsed into a
        single credential. Deployments <bcp14>MAY</bcp14> substitute
        identity-layer mechanisms such as those of
        <xref target="I-D.klrc-aiagent-auth"/>.</t>
        <t>HTTP status is 200 for any evaluated verdict, including "deny",
        and including a "deny" with reason "malformed" that the PDP
        produced from a request it received and evaluated (<xref
        target="decidereq"/>): a denial is a successful evaluation, not an
        error, and the verdict is carried in the response body. 4xx and 5xx
        are reserved for requests that failed before evaluation -- a body
        that did not parse for this binding, an unsupported content type, a
        rejected credential -- and for infrastructure failures. A PEP
        <bcp14>MUST</bcp14> treat a 4xx or 5xx under <xref
        target="failure"/>, under which a failure denoting a malformed
        request is not eligible for fail_open. A PDP that has begun
        evaluating a request <bcp14>MUST NOT</bcp14> report the outcome as a
        bare 4xx or 5xx with no verdict body; the distinction is whether the
        decision function was reached.</t>
      </section>
      <section anchor="uds">
        <name>Local Socket Binding</name>
        <t>Implementations <bcp14>SHOULD</bcp14> offer a Unix domain socket
        binding carrying the same messages, for enforcement points on the
        same host. Peer credentials <bcp14>MAY</bcp14> be used in place of
        bearer tokens.</t>
      </section>
    </section>

    <section anchor="conformance">
      <name>Conformance</name>
      <t>A conformant PDP returns a verdict for every request it accepts,
      including one it cannot parse (<xref target="decidereq"/>). It
      implements the messages of <xref target="messages"/>, the invariants of
      <xref target="invariants"/>, the idempotency rules of
      <xref target="idem"/>, the approval semantics of
      <xref target="approvals"/>, and the evidence requirements of
      <xref target="evidence"/>.</t>
      <t>A conformant PEP never performs a governed action without a
      permit; sends exactly one report per permit; fails closed on unknown
      obligations; and has an explicitly configured unreachability
      behavior.</t>
      <t>A minimal PEP <bcp14>MAY</bcp14> implement only decide and report
      and refuse any verdict other than "permit" and "deny". It
      <bcp14>MUST</bcp14> then treat "propose" and "dry_run" as refusals to
      act and report "not_attempted".</t>
    </section>

    <section anchor="versioning">
      <name>Versioning</name>
      <t>The "protocol" field carries "aadp/MAJOR.MINOR". Minor versions add
      optional fields, reason codes, and obligation types; a recipient
      <bcp14>MUST</bcp14> ignore unknown fields. Unknown obligations and
      unknown verdicts are the exception: both <bcp14>MUST</bcp14> fail
      closed. Major versions may change required semantics.</t>
    </section>

    <section anchor="iana">
      <name>IANA Considerations</name>
      <t>This document requests the creation of an "Agent Action Decision
      Protocol (AADP)" registry group with three registries, each operating
      under the Specification Required policy <xref target="RFC8126"/>.
      Names beginning with "x-" are reserved for vendor use and are not
      registered.</t>
      <t>Registration requests for all three registries use the same
      template: Name (the wire identifier), Description (one sentence),
      Change Controller, and Reference (the specification defining the
      semantics). For the Obligation Types registry, two further fields
      are required: Value Syntax, and Discharge Evidence (what a PEP
      <bcp14>MUST</bcp14> supply in a report to demonstrate the obligation
      was met). The designated expert should confirm that the semantics
      are implementable by a PEP without access to PDP-internal state, and
      that no existing entry already expresses them.</t>

      <section>
        <name>AADP Verdicts Registry</name>
        <t>Initial contents are the six verdicts defined in
        <xref target="verdicts"/>: "permit", "deny", "propose", "dry_run",
        "observe", and "replay". For each, the Reference is this document,
        the Change Controller is the IETF, and the Description is the
        corresponding "Meaning" cell of
        <xref target="verdicttable"/>.</t>
        <t>A new verdict changes what a conformant PEP must do on receipt.
        The designated expert <bcp14>MUST NOT</bcp14> approve a
        registration whose PEP obligation is not stated, and
        <bcp14>SHOULD</bcp14> reject a verdict whose behavior an existing
        verdict plus a reason code already expresses.</t>
      </section>
      <section>
        <name>AADP Reason Codes Registry</name>
        <t>Initial contents are the thirteen reason codes defined in
        <xref target="reasons"/>: "passed", "kill_switch", "default_deny",
        "bounds", "cap_daily_rate", "cap_eur_day", "cap_eur_month",
        "dry_run", "tier_confirm", "effect_floor",
        "no_compensating_command", "observe", and "malformed". For each, the Reference
        is this document, the Change Controller is the IETF, and the
        Description is the corresponding "Meaning" cell of
        <xref target="reasontable"/>.</t>
        <t>Reason codes are the audit vocabulary: a registration
        <bcp14>SHOULD</bcp14> state which verdicts it may accompany.</t>
      </section>
      <section>
        <name>AADP Obligation Types Registry</name>
        <t>Initial contents are the four obligation types defined in
        <xref target="obligations"/>: "report_result",
        "undo_available_until", "execute_within", and "isolate". For each,
        the Reference is this document and the Change Controller is the
        IETF.</t>
        <t>Because an unrecognized obligation type causes a conformant PEP
        to decline the action (<xref target="obligations"/>), registration
        in this registry is the only way an obligation becomes usable
        across independent implementations. The designated expert
        <bcp14>MUST</bcp14> verify that the Discharge Evidence field
        describes something a PEP can actually produce and a PDP can
        actually check.</t>
      </section>
    </section>

    <section anchor="security">
      <name>Security Considerations</name>
      <t>Parameters are untrusted input, commonly authored by a language
      model that may itself be under prompt-injection influence. PDPs
      <bcp14>MUST</bcp14> validate parameters against declared bounds and
      <bcp14>SHOULD</bcp14> reject unknown parameters rather than pass them
      through. The "rationale" field is model-authored text in the common
      case; it is evidence, never input to the decision.</t>
      <t>AADP mitigates a class of confused-deputy failures
      <xref target="SCOPEGATE"/> by evaluating argument values per call
      rather than gating tool exposure. It does not defend against a PEP
      that can act without consulting the PDP: a capability reachable
      outside the governed perimeter is outside this protocol's ability to
      detect. Deployments <bcp14>SHOULD</bcp14> verify that no unsupervised
      path to a governed capability exists.</t>
      <t>The interval between a permit and its execution is a
      time-of-check/time-of-use window. The "execute_within" obligation
      bounds it; deployments with strict requirements <bcp14>SHOULD</bcp14>
      issue it. A revoked or expired permit discovered at execution time is
      reported "not_attempted".</t>
      <t>The approval queue is a denial-of-service surface: an agent that
      can flood proposals can bury a human approver. Rate caps on
      proposal-generating action types, and approval TTLs, bound the
      queue.</t>
      <t>Budget reservation is a second denial-of-service surface, and a
      quieter one. A caller that requests permits and never reports
      consumes the reserved budget without performing any action, so
      nothing appears at the sink and nothing fails; legitimate work is
      simply denied for the rest of the window. The explicit reclamation
      required in <xref target="exchange"/> bounds the exposure to the
      "execute_within" deadline, which is why that obligation is
      <bcp14>RECOMMENDED</bcp14> on every budgeted permit rather than
      reserved for time-critical actions.</t>
      <t>The evidence record may contain sensitive parameter values.
      Implementations <bcp14>SHOULD</bcp14> support field-level redaction
      at write time and <bcp14>MUST NOT</bcp14> achieve redaction by
      mutating existing entries. Access to the evidence record
      <bcp14>SHOULD</bcp14> be governed no less strictly than the actions
      it records.</t>
      <t>Monetary values as decimal strings avoid floating-point rounding
      as an attack surface on budget arithmetic.</t>
    </section>

  </middle>

  <back>
    <references>
      <name>References</name>
      <references>
        <name>Normative References</name>
        <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
          <front>
            <title>Key words for use in RFCs to Indicate Requirement Levels</title>
            <author initials="S." surname="Bradner" fullname="S. Bradner"/>
            <date year="1997" month="March"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="2119"/>
        </reference>
        <reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
          <front>
            <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
            <author initials="B." surname="Leiba" fullname="B. Leiba"/>
            <date year="2017" month="May"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="8174"/>
        </reference>
        <reference anchor="RFC3339" target="https://www.rfc-editor.org/info/rfc3339">
          <front>
            <title>Date and Time on the Internet: Timestamps</title>
            <author initials="G." surname="Klyne" fullname="G. Klyne"/>
            <author initials="C." surname="Newman" fullname="C. Newman"/>
            <date year="2002" month="July"/>
          </front>
          <seriesInfo name="RFC" value="3339"/>
        </reference>
        <reference anchor="RFC8259" target="https://www.rfc-editor.org/info/rfc8259">
          <front>
            <title>The JavaScript Object Notation (JSON) Data Interchange Format</title>
            <author initials="T." surname="Bray" fullname="T. Bray" role="editor"/>
            <date year="2017" month="December"/>
          </front>
          <seriesInfo name="STD" value="90"/>
          <seriesInfo name="RFC" value="8259"/>
        </reference>
        <reference anchor="RFC8126" target="https://www.rfc-editor.org/info/rfc8126">
          <front>
            <title>Guidelines for Writing an IANA Considerations Section in RFCs</title>
            <author initials="M." surname="Cotton" fullname="M. Cotton"/>
            <author initials="B." surname="Leiba" fullname="B. Leiba"/>
            <author initials="T." surname="Narten" fullname="T. Narten"/>
            <date year="2017" month="June"/>
          </front>
          <seriesInfo name="BCP" value="26"/>
          <seriesInfo name="RFC" value="8126"/>
        </reference>
      </references>
      <references>
        <name>Informative References</name>
        <reference anchor="I-D.klrc-aiagent-auth" target="https://datatracker.ietf.org/doc/draft-klrc-aiagent-auth/">
          <front>
            <title>AI Agent Authentication and Authorization</title>
            <author><organization>IETF Individual Submission</organization></author>
            <date year="2026"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-klrc-aiagent-auth-03"/>
        </reference>
        <reference anchor="SCOPEGATE" target="https://arxiv.org/abs/2606.28679">
          <front>
            <title>Capability Gates Are Not Authorization: Confused-Deputy Failures in LLM Agent Frameworks</title>
            <author initials="D." surname="Mellafe Zuvic" fullname="David Mellafe Zuvic"/>
            <date year="2026" month="June"/>
          </front>
          <seriesInfo name="arXiv" value="2606.28679"/>
        </reference>
        <reference anchor="XACML" target="https://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html">
          <front>
            <title>eXtensible Access Control Markup Language (XACML) Version 3.0</title>
            <author><organization>OASIS</organization></author>
            <date year="2013" month="January"/>
          </front>
        </reference>
        <reference anchor="I-D.liu-agent-operation-authorization" target="https://datatracker.ietf.org/doc/draft-liu-agent-operation-authorization/">
          <front>
            <title>Agent Operation Authorization</title>
            <author><organization>IETF Individual Submission</organization></author>
            <date year="2026" month="March"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-liu-agent-operation-authorization-02"/>
        </reference>
        <reference anchor="I-D.ni-wimse-ai-agent-identity" target="https://datatracker.ietf.org/doc/draft-ni-wimse-ai-agent-identity/">
          <front>
            <title>WIMSE Applicability for AI Agents</title>
            <author><organization>IETF Individual Submission</organization></author>
            <date year="2026" month="February"/>
          </front>
          <seriesInfo name="Internet-Draft" value="draft-ni-wimse-ai-agent-identity-02"/>
        </reference>
        <reference anchor="MCP" target="https://modelcontextprotocol.io/specification">
          <front>
            <title>Model Context Protocol Specification</title>
            <author><organization>MCP Project</organization></author>
            <date year="2026"/>
          </front>
        </reference>
        <reference anchor="RFC7942" target="https://www.rfc-editor.org/info/rfc7942">
          <front>
            <title>Improving Awareness of Running Code: The Implementation Status Section</title>
            <author initials="Y." surname="Sheffer" fullname="Y. Sheffer"/>
            <author initials="A." surname="Farrel" fullname="A. Farrel"/>
            <date year="2016" month="February"/>
          </front>
          <seriesInfo name="BCP" value="205"/>
          <seriesInfo name="RFC" value="7942"/>
        </reference>
      </references>
    </references>

    <section anchor="example">
      <name>Worked Exchange (Tier 3)</name>
      <artwork><![CDATA[
-> decide  { request_id: A, action_type: "payments.transfer",
             cost_eur: "4000.00" }
<- propose { reason: "effect_floor", nominal_tier: 2,
             effective_tier: 3, approval_id: "apr-9Xt4Bs7Q" }

   [ human approves apr-77 ]

-> decide  { request_id: B, ..., approval_ref: "apr-9Xt4Bs7Q" }
<- permit  { permit_id: "pmt-Rc8Hn2Vw5Lp3",
             obligations: [ {type:"report_result"},
               {type:"execute_within",
                value:"2026-08-17T09:20:00Z"} ] }

   [ PEP performs the transfer ]

-> report  { permit_id: "pmt-Rc8Hn2Vw5Lp3", outcome: "success" }
<- accepted { evidence_id: "aud-88221" }
]]></artwork>
    </section>

    <section anchor="implstatus">
      <name>Implementation Status</name>
      <t>This section records the status of known implementations, per the
      practice described in <xref target="RFC7942"/>; it is to be removed
      before publication as an RFC.</t>
      <t>onedoor (Apache-2.0, Python, version 0.3.4 on PyPI at the time of
      writing) is the reference implementation of the PDP, with a test
      suite covering the invariants of <xref target="invariants"/> and
      running on Linux and Windows. Its decision service exposes the HTTP
      operations of <xref target="http"/>.</t>
      <t>Four enforcement points have been written against it, at differing
      levels of maturity. Two are packaged with the implementation: an
      in-process library binding, and a Model Context Protocol stdio proxy.
      Two are experimental examples rather than supported adapters: a
      LiteLLM gateway guardrail and a LangGraph tool wrapper with
      interrupt-based approval flow. The LiteLLM example is known to
      deviate from <xref target="exchange"/> in that it reports on issue of
      the permit rather than after the act; it is therefore not conformant
      as written, and is included here as evidence that the gateway hook
      point is viable, not as a conformant PEP.</t>
      <t>onedoor records policy provenance as required by
      <xref target="evidence"/>: the policy set is content-hashed, retained
      in an append-only table, and the hash is stamped on every evidence
      entry. This was added after the requirement was written. Its cost, from
      interleaved A/B runs on one host, is roughly 4 per cent of a
      decide-and-report cycle -- around 0.02 ms on a cycle of about 0.49 ms.
      Single-run figures at this scale are not distinguishable from run-to-run
      variation, which is itself of that order, so the figure is offered as an
      order of magnitude and not as a benchmark.</t>
      <t>Its evidence writes are split along the durability boundary this
      document draws: the intent row is always written synchronously before
      the permit is returned, while result rows may optionally be batched.
      Batching results costs nothing this document requires -- a lost result
      leaves an intent with no result, which is the recoverable state
      <xref target="invariants"/> already mandates -- and on one host it
      raised throughput from roughly 1,940 to 2,840 decide-and-report cycles
      per second, saturating at a batch of about 16. Exactly-once reporting
      is preserved by checking the pending batch as well as the record.</t>
      <t>onedoor does not yet implement the explicit reservation
      reclamation required by <xref target="exchange"/>: a permit that is
      never reported holds its budget until the accounting window rolls
      over. The requirement was added after probing the implementation,
      and the implementation has not caught up.</t>
      <t>Two further requirements of this revision are not yet met by the
      reference implementation, and are recorded here rather than hidden.
      Its permit identifiers are sequential rather than unpredictable
      (<xref target="idem"/>); a deployment relying on it therefore
      requires the identity-binding alternative that section permits.
      Its evidence is durable to process-crash but not to host-failure
      level (<xref target="evidence"/>), which it documents rather than
      strengthens. A conformant unpredictable-identifier scheme and a
      host-durable evidence mode are planned but unshipped.</t>
      <t>onedoor implements PDP-driven resumption
      (<xref target="approvals"/>): its approve operation re-evaluates the
      recorded request under a fresh request identifier and returns a
      decide response. It does not currently accept "approval_ref" on a
      decide request; PEP-driven resumption is therefore specified but not
      yet exercised by an implementation.</t>
    </section>

    <section anchor="ack">
      <name>Acknowledgments</name>
      <t>The PDP/PEP decomposition and the obligation concept derive from
      XACML. The capability-versus-authorization framing was independently
      articulated by David Mellafe Zuvic in the analysis cited as
      <xref target="SCOPEGATE"/>, whose limitations section names stateful
      side channels as future work; this document treats that remainder as
      part of its scope.</t>
    </section>
  </back>
</rfc>
