AI Research Library

The Three Principals and the Confused Deputy

Rajesh Ranjan Mahapatra

A consequential action is defensible only when the system preserves who requested it, which agent proposed the exact effect and which workload executed it, then binds all three to one narrow, current authority decision.

© 2026 Rajesh Ranjan Mahapatra. 32 min read

The Three Principals and the Confused Deputy

A consequential action is defensible only when the system preserves who requested it, which agent proposed the exact effect and which workload executed it, then binds all three to one narrow, current authority decision.

TLDR

  1. Authentication can prove every participant is genuine while the system still uses the wrong participant's authority. The confused deputy is a binding failure, not merely a credential failure.
  2. Preserve three roles separately: requester, proposer and executor. Bind them to an immutable action, target, purpose, policy version, current state and expiry.
  3. A model may construct a proposal; trusted policy code mints authority; the resource-local executor makes the final state and replay checks.
  4. Token exchange, workload identity, proof of possession and fine-grained scopes help, but none alone proves the executed effect is the one requested and authorised.
  5. Release only after cross-principal substitution, proposal mutation, wrong-audience, stale-entitlement and replay tests all fail closed.

Opening case

A refund agent receives two instructions that look identical at the tool boundary. Both say: return £390 to account 123. Both arrive through an authenticated employee session. Both are proposed by the approved agent build. Both reach the payments service over mutual TLS. Every credential is valid.

In the first path, an entitled operations analyst requested the refund after reviewing a duplicated fee. In the second, a retrieved document instructed the agent to substitute a different account while retaining the analyst's session context. The payments service sees the same well-attested caller in both cases. If it asks only, “May this workload call the refund endpoint?”, it will accept both.

The missing fact is not another identity. It is the relationship among identities and the action. The service must know who requested the purpose, which principal constructed the exact proposal and which principal is about to create the effect. It must also know that the proposal is still inside the requester's authority and unchanged since any approval.

This is the modern form of the confused deputy. The deputy has real authority. The request may contain a perfectly valid object name, account number or tool argument. The failure occurs when the system silently substitutes the deputy's authority for the requester's authority. Better model instructions cannot repair that substitution. The repair is a deterministic chain of request, proposal, authority and effect receipts.

Part I

The argument in one passChart · flow
flowchart LR
  A["Opening case"] --> B["On this page"]
  B --> C["The deputy can be honest and<br/>still wrong"]
  C --> D["Bind request, proposal and<br/>execution"]
  D -. "re-test" .-> A
  classDef first fill:#0a1424,stroke:#7B4CD8,stroke-width:2px,color:#e6f3ff
  classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff
  class A first
  class B,C,D step
A structural route through The Three Principals and the Confused Deputy, from Opening case to Source ledger. Reading use: follow the relation, then test it against the surrounding evidence and limits.

The deputy can be honest and still wrong

Norm Hardy's original example involved a compiler on a commercial time-sharing system. Users could name a file for debugging output. The compiler also possessed authority to write statistics in a protected system directory. A user supplied the billing file as the debugging destination, and the operating system checked the compiler's authority rather than the user's authority. The compiler did exactly what its interface allowed and destroyed billing data. Hardy called it a confused deputy. [1]

The important structure is easy to miss. The user supplied a designation, the name of an object. The operating system supplied authority from the compiler's ambient privileges. The name did not carry proof that the user could exercise that authority. A capability-style repair couples designation with a right that the caller can actually convey. Later work on object capabilities made the broader claim precise: systems that avoid ambient authority provide stronger support for least privilege and for avoiding confused-deputy paths. [2] [3]

Where authority is silently substituted An insecure path carries only a resource name from the requester and inserts the deputy's broad authority at execution. A bound path carries a narrow action capability tied to the requester and target. Ambient-authority path Requester names target designation only Deputy broad privilege Protected object authority appears here without caller binding Bound-authority path Requester conveys limit Target + operation + bounded authority Permitted object only
Figure 1. The fault is the silent authority insertion. The top path is the classic confused deputy: the caller supplies a name while the deputy supplies unrelated ambient power. The lower path couples designation and bounded authority. Conceptual figure based on Hardy's mechanism; no measured quantities.

Agentic systems recreate this structure because language models are exceptionally good at turning text into designators. A model can produce a customer number, file path, recipient, SQL predicate, function name or tool argument. The surrounding runtime often holds broad credentials because it needs to serve many users and workflows. The model therefore proposes what to touch while the runtime silently contributes why it may be touched.

Prompt injection is one trigger, but an attacker is not required. A stale portfolio assignment, entity-resolution error, queue mix-up or ambiguous pronoun can produce the same cross-principal substitution. The model may be compliant with its prompt. The executor may be compliant with IAM. The action is still unauthorised because the wrong authority was applied to the right-looking request.

Thought experiment: the same call

Hold everything constant except the origin of the target account. In world A, the account identifier comes from a signed request receipt created after the analyst selected a case. In world B, the identifier comes from text inside a retrieved attachment. The model, tool arguments, agent identity, network path and endpoint are otherwise identical.

An identity-only design produces the same decision in both worlds because it sees the same caller. A delegation-aware design produces different decisions because only world A can show that the target lies inside the requester's authority. The causal feature is not whether the account string is accurate; it is whether the string is bound to the authority under which the action is attempted.

Authentication is necessary, then the real question begins

Workload identity solves a genuine problem. A short-lived cryptographic identity can prove which deployed workload is calling. SPIFFE, for example, defines a SPIFFE ID and a verifiable identity document, or SVID, that a workload presents to a resource or caller. [14] That evidence lets a service distinguish the approved refund agent from an unknown container.

It does not prove that the refund agent may act for Maya, on account 123, to return £390, because of a duplicated fee, under the current policy. The same attested workload can make both authorised and unauthorised calls. Identity narrows the set of possible callers. Delegation must narrow the set of effects each caller may produce for another principal.

Authority binding: absent ← → exact
Caller identity: unknown ↑ ↓ attested
Figure 2. Authentication and authority integrity are independent axes. The lower-left quadrant explains why a perfectly authenticated agent can remain unsafe. Illustrative decision matrix, not empirical data.

A serious negative control

Not every service call needs a three-principal envelope. Consider a telemetry writer with one fixed, append-only sink. The caller cannot choose the destination, operation or retention policy. The service acts only under its own authority, and every accepted message has the same bounded effect. Preserving a user identity may help investigation, but it does not change the authorisation decision.

This negative control matters because it locates the mechanism. A confused deputy requires a deputy with authority that a less-authorised influence can steer towards a protected effect. Remove either feature and the risk collapses. If an operation has no caller-selected target, no delegated business purpose and no material side effect, a simpler workload-identity check may be the correct design.

The three-principal pattern becomes valuable when an action is user-specific, cross-tenant, state-changing, externally visible, difficult to reverse or subject to independent approval. Applying it indiscriminately creates ceremony without protection. Applying it only at login leaves the consequential boundary exposed.

Part II

Bind request, proposal and execution

The cleanest model names three roles rather than three technology types. A person, service or agent may occupy a role, and one entity may occupy more than one role in a low-risk path. The roles stay distinct in the evidence because they answer different questions.

The requester originates the authorised purpose and constrains what may be attempted. It might be an employee, customer, scheduler, case workflow or upstream service. Its receipt must come from trusted application state, not from a model-generated field such as requested_by.

The proposer constructs the concrete candidate action. In an agentic workflow this is usually the model-bearing runtime, identified by workload identity and deployed version. The proposer may interpret evidence and choose parameters. It does not thereby acquire the requester's entitlement or the executor's technical power.

The executor is the workload that creates or attempts the effect at the authoritative resource. It may be a tool adapter, payments service, case API or database procedure. Its own credential proves which software is acting. Its local checks decide whether the action is still valid against current state.

An approver can be a fourth principal, but approval is not a substitute for any of the three roles. The approver contributes a signed decision over an immutable proposal. The requester may lack approval authority. The approver may lack execution credentials. Keeping those mandates separate prevents one broad status called “approved” from swallowing distinct decision rights.

The three-principal identity braid Requester, proposer and executor identities remain separate while a request receipt becomes a proposal, a signed decision receipt and an effect receipt. Requester Proposer Executor Request receipt purpose, target envelope, entitlement, expiry Immutable proposal operation, parameters, evidence and state digest Decision receipt binds requester + proposer + executor to proposal hash, audience, policy and time Effect receipt state before and after, postcondition, nonce
Figure 3. The three-principal identity braid. The roles remain separately attributable while the decision receipt binds them to one immutable proposal. The effect receipt closes the chain with observed resource state. Practitioner synthesis; logical relationships, not measured flow volumes.

The formal contract

A useful implementation has four typed objects. The request receipt records the requester's authenticated identity, purpose, resource envelope, upper bounds, entitlement version, expiry and nonce. The proposal records the proposer's identity and version, the exact operation and parameters, a digest of the request, relevant evidence and the resource state against which the proposal was formed.

A trusted policy service evaluates both objects. If policy allows the action, it issues a short-lived decision receipt, or capability envelope, addressed to one executor. The executor verifies that receipt, rechecks current resource state, enforces replay protection and emits an effect receipt. The model can create the proposal object. It cannot sign the request receipt, choose the authenticated requester, mint the decision receipt or declare its own effect successful.

Aeffective = Arequester(Q) ∩ Aproposer(P) ∩ Adecision(C) ∩ Aexecutor(Snow)

Here, Q is the trusted request receipt, P is the immutable proposal, C is the signed decision receipt and S_now is the resource's current authoritative state. Each term removes possible actions. The requester constrains purpose and business entitlement. The proposer allow-list constrains which agent build may formulate the action. The decision receipt constrains the exact operation, target, amount, audience and validity window. The executor applies local preconditions and one-time-use rules.

The intersection matters more than any individual permission. A user may be entitled to refund account 123 but not operate the payments API. The agent may call the API but not select accounts outside the request receipt. The executor may move money technically but only under a receipt addressed to its workload identity. No principal's authority is complete by itself.

Delegation should narrow authority at every hop

A delegation chain is safe only when each transition preserves or reduces the set of permitted effects. The requester may authorise a refund up to £500 for one account and one case. The proposer can select £390 inside that envelope, but it cannot add a second account, change the purpose to a goodwill payment or extend the validity period. If policy permits no more than £350, it must reject the £390 proposal and require a revised proposal with a new digest; it must not silently authorise different material parameters under the old object. The executor can still refuse because the case is closed or the account state changed. No downstream principal may recover power removed upstream.

This gives the architecture a useful monotonicity test. Let E(Q) be the effects allowed by the trusted request, E(P) the effect described by the proposal, E(C) the effects authorised by the decision receipt and E(X) the effect the executor will actually attempt. A valid chain satisfies E(X) ⊆ E(C) ⊆ E(P) ⊆ E(Q). The proposal is normally one concrete effect, so its set is small. A broad scope such as refund:write is not evidence of this relation because it says nothing about account, amount, reason or decision time.

The test also reveals a common architectural inversion. A gateway first authenticates the user, then exchanges the user token for a service token that is broader because the internal service needs a convenient role. The service token is cryptographically sound, yet the chain has expanded authority at the point where attribution becomes harder. A safer exchange carries the subject and actor separately, addresses the token to one resource and adds a business decision object that cannot exceed the original request.

Attenuation is not merely about scopes. Time, evidence and state must narrow too. A request valid for ten minutes should not become a thirty-minute execution right. A proposal based on one version of an account should not remain valid after a material state change. Evidence that supports a duplicate-fee refund should not authorise a different payment category. The delegation envelope is the product of identity, purpose, object, operation, limits, time and state, not a string attached to an access token.

Anatomy of a decision receipt A layered signed envelope contains principal bindings, exact proposal identity, authority constraints, freshness and evidence references. PRINCIPAL BINDINGS requested_by · proposed_by · executor_audience plus approver identity when policy requires it EXACT PROPOSAL proposal_hash · operation · resource · parameter bounds request_hash · evidence_digest · state_version AUTHORITY AND TIME purpose · policy_version · issued_at · expires_at one-time nonce · revocation handle · required assurance VERIFICATION EVIDENCE signature · issuer · key identifier · trace correlation effect receipt link after attempted execution tamper evident
Figure 4. A decision receipt describes the decision, not merely the caller. The exact field names are implementation choices. The invariant is that identity, action, authority, freshness and evidence travel as one signed contract outside model-editable text. Illustrative schema.

Standards already offer several of these pieces. OAuth 2.0 Token Exchange distinguishes impersonation from delegation. In delegation, the actor retains a separate identity while representing a subject; the subject_token and optional actor_token express those roles, and a JWT act claim can carry the current actor. [6] Rich Authorization Requests add structured authorization_details for fine-grained requirements rather than relying only on flat scopes. [7]

Those mechanisms are useful transport semantics, not the whole business contract. RFC 8693 explicitly leaves token validity details and local policy to implementations. A token exchange does not automatically invalidate its input tokens, create one-time business semantics or link renewal and revocation across every derived token. Its nested historical actors are informational for access control; only top-level claims and the current actor are considered. That boundary reinforces the need for an application-level decision and effect ledger.

Thought experiment: one changed digit

An approver reviews a proposal to refund £390 and clicks approve. Before execution, the agent changes the amount to £890. Keep the approver identity, session, policy, account, agent build and executor unchanged. Vary only whether approval binds to a human-readable summary or to the canonical proposal hash.

With summary approval, the executor may see a genuine click and a genuine agent yet apply the wrong effect. With hash-bound approval, the changed digit produces a different digest, so the old authority is unusable. A human decision becomes an enforceable control only when the approved object and executed object are provably the same.

Proposal digest protects approval across time A proposal is hashed, approved and later compared at execution. A changed amount causes a hash mismatch and denial. Proposal £390 Approval sign H(P) Mutation £890 Execute compare approved digest = a91… new digest = 7d4… DENY: hash mismatch
Figure 5. Approval survives time only through object identity. A mutation after approval invalidates the receipt, even though every participating identity remains genuine. Synthetic values for illustration.
Failure boundary

The braid cannot prove that the requester understood the consequences, that the model's interpretation was correct, that evidence was truthful or that the resource's implementation is safe. It proves a narrower claim: the attempted effect was attributable to named principals and stayed within a signed, current authority decision. Outcome verification and domain assurance remain separate controls.

Part III

Break the chain before release

Identity architecture is often reviewed as a diagram of issuers, tokens, gateways and trust domains. That review can confirm that credentials are minted and validated. It cannot show that the system refuses to borrow one principal's authority for another principal's proposal. The decisive evidence comes from controlled failures.

A release test should vary one binding at a time while holding the rest of the path constant. Substitute a resource outside the requester's entitlement. Use an unapproved proposer build. Address the capability to another executor. Change one material proposal field after approval. Advance the resource state. Revoke the user entitlement. Present the same capability twice. Each case should fail at a named enforcement point before the protected effect.

The propagation lab

The executable artefact below uses synthetic identities and HMAC-signed receipts to make the mechanism visible. It is deliberately small and uses only the Python standard library. It is not a production token profile. In a real system, keys, issuers, algorithms, validation, revocation and secure storage require established infrastructure and independent review.

The insecure baseline authenticates the agent and trusts its arguments. It therefore accepts a cross-account proposal. The secure path verifies the request receipt, proposer, exact proposal hash, executor audience, current entitlement version, current resource state and one-time nonce. The positive case is allowed; every mutation is denied for a specific reason.

Measured results from the synthetic propagation lab, comparing an identity-only baseline with the three-principal path under controlled failure injection.
Injected variationIdentity-only baselineThree-principal pathRequired interpretation
Valid request and unchanged proposalAllowAllowPositive control
Account substituted by proposalAllowDenyRequester-resource binding works
Unapproved proposer buildDeny only if caller check differsDenyProposal provenance is enforced
Amount changed after capability issueAllowDenyApproval binds to exact object
Capability presented to wrong executorAllow if token is broadDenyAudience is narrow
User entitlement version changesAllow until session refreshDenyBusiness authority is current
Resource state advancesAllowDenyLocal state veto works
Same capability used twiceAllowDenyBusiness replay protection works
Figure 6. Failure injection separates caller identity from decision integrity. Results are measured from the included synthetic Python harness. “Identity-only baseline” denotes the code path that validates the approved agent identity but trusts model-supplied action arguments.
case actual verdict ------------------------------------------------------------------------ insecure_cross_account ALLOW PASS secure_happy_path ALLOW PASS secure_cross_account DENY:RESOURCE_SUBSTITUTED PASS secure_wrong_proposer DENY:PROPOSER_DENIED PASS secure_proposal_mutation DENY:PROPOSAL_MUTATED PASS secure_wrong_executor DENY:WRONG_EXECUTOR_AUDIENCE PASS secure_stale_entitlement DENY:ENTITLEMENT_STALE PASS secure_state_change DENY:RESOURCE_STATE_CHANGED PASS secure_replay DENY:REPLAY_DETECTED PASS
Open the complete runnable Python lab

Assumptions: synthetic data, fixed demonstration time, HMAC only to expose receipt boundaries. Save as three_principal_propagation_lab.py and run with Python 3.11 or later. The script asserts that every expected result passes.

#!/usr/bin/env python3
"""Synthetic three-principal propagation lab.

Educational only. It demonstrates request, proposal and execution binding with
HMAC-signed receipts. It is not a production token service or cryptographic
profile.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass, replace
import hashlib
import hmac
import json
from typing import Callable

NOW = 1_700_000_000
IDENTITY_KEY = b"demo-identity-key"
AUTHORITY_KEY = b"demo-authority-key"
POLICY_VERSION = "refund-policy-17"
APPROVED_PROPOSER = "agent:refund/v3"
APPROVED_EXECUTOR = "service:payments/v2"


def canonical(value: object) -> bytes:
    """Return a stable byte representation for hashing and signing."""
    return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()


def digest(value: object) -> str:
    return hashlib.sha256(canonical(value)).hexdigest()


def sign(value: object, key: bytes) -> str:
    return hmac.new(key, canonical(value), hashlib.sha256).hexdigest()


def verify(value: object, signature: str, key: bytes) -> bool:
    return hmac.compare_digest(sign(value, key), signature)


@dataclass(frozen=True)
class RequestReceipt:
    requester: str
    account: str
    operation: str
    max_amount_gbp: int
    purpose: str
    entitlement_version: int
    expires_at: int
    nonce: str
    signature: str = ""

    def unsigned(self) -> dict[str, object]:
        value = asdict(self)
        value.pop("signature")
        return value


@dataclass(frozen=True)
class Proposal:
    proposal_id: str
    proposer: str
    request_hash: str
    operation: str
    account: str
    amount_gbp: int
    beneficiary: str
    state_version: int

    def hash(self) -> str:
        return digest(asdict(self))


@dataclass(frozen=True)
class Capability:
    requester: str
    proposer: str
    executor: str
    proposal_hash: str
    operation: str
    account: str
    max_amount_gbp: int
    state_version: int
    policy_version: str
    expires_at: int
    nonce: str
    signature: str = ""

    def unsigned(self) -> dict[str, object]:
        value = asdict(self)
        value.pop("signature")
        return value


class Denied(Exception):
    """A reason-coded denial from policy or execution."""


def make_request(**values: object) -> RequestReceipt:
    receipt = RequestReceipt(**values)
    return replace(receipt, signature=sign(receipt.unsigned(), IDENTITY_KEY))


def issue_capability(
    request: RequestReceipt,
    proposal: Proposal,
    *,
    live_entitlement_version: int,
    executor: str = APPROVED_EXECUTOR,
) -> Capability:
    """Trusted policy code binds the three principals to one exact proposal."""
    if not verify(request.unsigned(), request.signature, IDENTITY_KEY):
        raise Denied("REQUEST_SIGNATURE_INVALID")
    if request.expires_at <= NOW:
        raise Denied("REQUEST_EXPIRED")
    if request.entitlement_version != live_entitlement_version:
        raise Denied("ENTITLEMENT_STALE")
    if proposal.request_hash != digest(request.unsigned()):
        raise Denied("REQUEST_PROPOSAL_MISBOUND")
    if proposal.proposer != APPROVED_PROPOSER:
        raise Denied("PROPOSER_DENIED")
    if proposal.operation != request.operation:
        raise Denied("OPERATION_INFLATED")
    if proposal.account != request.account:
        raise Denied("RESOURCE_SUBSTITUTED")
    if proposal.amount_gbp > request.max_amount_gbp:
        raise Denied("AMOUNT_EXCEEDS_REQUEST")

    capability = Capability(
        requester=request.requester,
        proposer=proposal.proposer,
        executor=executor,
        proposal_hash=proposal.hash(),
        operation=proposal.operation,
        account=proposal.account,
        max_amount_gbp=request.max_amount_gbp,
        state_version=proposal.state_version,
        policy_version=POLICY_VERSION,
        expires_at=NOW + 90,
        nonce=f"cap:{proposal.proposal_id}",
    )
    return replace(capability, signature=sign(capability.unsigned(), AUTHORITY_KEY))


class PaymentsExecutor:
    def __init__(self, principal: str = APPROVED_EXECUTOR) -> None:
        self.principal = principal
        self.used_nonces: set[str] = set()

    def execute(
        self,
        proposal: Proposal,
        capability: Capability,
        *,
        live_state_version: int,
    ) -> str:
        """The resource-local executor performs the final veto and replay check."""
        if not verify(capability.unsigned(), capability.signature, AUTHORITY_KEY):
            raise Denied("CAPABILITY_SIGNATURE_INVALID")
        if capability.executor != self.principal:
            raise Denied("WRONG_EXECUTOR_AUDIENCE")
        if capability.expires_at <= NOW:
            raise Denied("CAPABILITY_EXPIRED")
        if capability.proposal_hash != proposal.hash():
            raise Denied("PROPOSAL_MUTATED")
        if capability.state_version != live_state_version:
            raise Denied("RESOURCE_STATE_CHANGED")
        if capability.nonce in self.used_nonces:
            raise Denied("REPLAY_DETECTED")
        if proposal.account != capability.account:
            raise Denied("RESOURCE_MISBOUND")
        if proposal.amount_gbp > capability.max_amount_gbp:
            raise Denied("AMOUNT_OUT_OF_BOUNDS")

        self.used_nonces.add(capability.nonce)
        return "EXECUTED"


def insecure_execute(proposal: Proposal) -> str:
    """Confused-deputy baseline: trust the agent credential and its arguments."""
    if proposal.proposer != APPROVED_PROPOSER:
        raise Denied("AGENT_NOT_AUTHENTICATED")
    return "EXECUTED"


def outcome(label: str, fn: Callable[[], object], expected: str) -> tuple[str, str, str]:
    try:
        fn()
        actual = "ALLOW"
    except Denied as exc:
        actual = f"DENY:{exc}"
    verdict = "PASS" if actual == expected else "FAIL"
    return label, actual, verdict


def run_lab() -> list[tuple[str, str, str]]:
    request = make_request(
        requester="analyst:maya",
        account="acct-123",
        operation="refund",
        max_amount_gbp=420,
        purpose="correct duplicated fee",
        entitlement_version=7,
        expires_at=NOW + 300,
        nonce="req-001",
    )
    request_hash = digest(request.unsigned())
    proposal = Proposal(
        proposal_id="p-001",
        proposer=APPROVED_PROPOSER,
        request_hash=request_hash,
        operation="refund",
        account="acct-123",
        amount_gbp=390,
        beneficiary="customer-wallet-9",
        state_version=12,
    )
    cross_account = replace(proposal, proposal_id="p-002", account="acct-999")
    wrong_proposer = replace(proposal, proposal_id="p-003", proposer="agent:unknown/v1")

    capability = issue_capability(
        request, proposal, live_entitlement_version=7
    )
    secure_executor = PaymentsExecutor()
    wrong_executor = PaymentsExecutor("service:archive/v1")

    results = [
        outcome(
            "insecure_cross_account",
            lambda: insecure_execute(cross_account),
            "ALLOW",
        ),
        outcome(
            "secure_happy_path",
            lambda: secure_executor.execute(
                proposal, capability, live_state_version=12
            ),
            "ALLOW",
        ),
        outcome(
            "secure_cross_account",
            lambda: issue_capability(
                request, cross_account, live_entitlement_version=7
            ),
            "DENY:RESOURCE_SUBSTITUTED",
        ),
        outcome(
            "secure_wrong_proposer",
            lambda: issue_capability(
                request, wrong_proposer, live_entitlement_version=7
            ),
            "DENY:PROPOSER_DENIED",
        ),
        outcome(
            "secure_proposal_mutation",
            lambda: PaymentsExecutor().execute(
                replace(proposal, amount_gbp=419),
                capability,
                live_state_version=12,
            ),
            "DENY:PROPOSAL_MUTATED",
        ),
        outcome(
            "secure_wrong_executor",
            lambda: wrong_executor.execute(
                proposal, capability, live_state_version=12
            ),
            "DENY:WRONG_EXECUTOR_AUDIENCE",
        ),
        outcome(
            "secure_stale_entitlement",
            lambda: issue_capability(
                request, proposal, live_entitlement_version=8
            ),
            "DENY:ENTITLEMENT_STALE",
        ),
        outcome(
            "secure_state_change",
            lambda: PaymentsExecutor().execute(
                proposal, capability, live_state_version=13
            ),
            "DENY:RESOURCE_STATE_CHANGED",
        ),
        outcome(
            "secure_replay",
            lambda: secure_executor.execute(
                proposal, capability, live_state_version=12
            ),
            "DENY:REPLAY_DETECTED",
        ),
    ]
    return results


if __name__ == "__main__":
    rows = run_lab()
    print(f"{'case':30} {'actual':31} verdict")
    print("-" * 72)
    for label, actual, verdict in rows:
        print(f"{label:30} {actual:31} {verdict}")
    assert all(verdict == "PASS" for _, _, verdict in rows)

Why sender-constrained tokens are still insufficient

Bearer-token theft is one route to an unauthorised effect. Sender-constrained tokens reduce that route. DPoP binds an OAuth token to proof of possession at the application layer, and mutual TLS can bind a token to a client certificate. [8] [9] Resource Indicators let a client identify the intended protected resource so the resulting token can be audience-restricted. [10]

These controls answer, “Is the presenter the holder for whom this token was issued?” and “Is this the intended service?” They do not answer, “Did the entitled requester ask for this exact refund?” A legitimate sender can still be confused. Proof of possession stops another process from replaying its token; it does not stop the correct process from using valid power for a poisoned target.

Likewise, one-time business execution cannot be inferred from token validity alone. A token may be fresh and sender-constrained while the refund has already occurred. The resource service needs a durable idempotency record or transaction nonce tied to the business operation. Security-token replay and duplicate business effect are related but distinct failure modes.

Worked scenario: a production-shaped refund

Worked scenario

This is a synthetic composite designed to expose the authority mechanism. It is not a client deployment or a claim about a particular bank.

Maya, an operations analyst, opens case C-721. The channel authenticates her and resolves a current entitlement that permits refunds on account 123 up to £420 for fee-correction cases. Trusted application code issues a request receipt with that account, purpose, limit, entitlement version, expiry and nonce. The receipt never enters the model as editable text.

Agent build refund/v3 reads the case evidence and proposes a £390 refund to the customer's registered wallet. Its proposal records the request hash, evidence digest, case-state version and exact parameters. A deterministic policy service verifies Maya's entitlement, the agent build, the operation class and the amount. Because the amount is below the configured dual-control threshold, it issues a 90-second decision receipt addressed only to payments-executor/v2.

The executor validates the signature, audience and proposal hash. It then consults the live account state. If the fee has already been reversed, the wallet has changed, the case is under legal hold or the capability nonce has been consumed, it denies locally. If all preconditions hold, it posts the refund and emits an effect receipt containing the state transition and idempotency record.

Production-shaped three-principal refund path A vertical sequence from analyst request through agent proposal, policy decision, payment executor checks and effect receipt, with denial branches for substitutions and stale state. Maya requester Signed request receipt account 123 · refund ≤ £420 · case C-721 Agent refund/v3 proposes £390 · registered wallet · state v12 wrong account DENY Policy issues decision receipt proposal hash · payments-executor/v2 policy 17 · expires in 90 seconds Payments executor checks live state audience · hash · state · nonce · postcondition state changed DENY Effect receipt: executed once
Figure 7. The authority control plane and resource-local veto meet at execution. The example uses synthetic names and values. Each denial branch preserves a reason-coded failure before funds move.

Now inject the earlier retrieved instruction that substitutes account 999. The model can still generate the proposal, but policy compares it with the trusted request receipt and returns RESOURCE_SUBSTITUTED. Inject a valid capability into an archive service and audience validation rejects it. Change the amount after issue and the proposal hash fails. Reuse the receipt and the executor's durable nonce record returns REPLAY_DETECTED.

The model remains valuable because it interprets evidence and composes a candidate action. The control does not demand deterministic language generation. It demands deterministic authority. Stochastic proposal and deterministic permission are compatible precisely because they meet at a typed boundary.

Principal continuity through queues and retries

Delegation often breaks after the first synchronous call. A front-end receives an authenticated requester, creates a job and places it on a queue. Minutes later, a worker wakes with only a case identifier and its own service credential. The worker is genuine, but the requester's purpose and limits have become informal metadata. If it reconstructs authority from the current case record or a free-text task description, the architecture has reintroduced ambient authority at the asynchronous boundary.

The queued unit should therefore carry immutable references to the request receipt and proposal, plus a correlation identifier that cannot itself authorise anything. A retry may reuse the same proposal only while its decision receipt remains current and unconsumed. A replan must create a new proposal digest. A human amendment must create a new request or approval receipt rather than editing the old evidence in place. Continuity means preserving the chain of decisions, not preserving one broad credential across time.

Fan-out requires the same discipline. Suppose one approved case produces a notification, a ledger update and a payment. Each effect needs its own executor audience and postcondition, even when all three derive from one request. The notification service must not receive payment authority, and the payment worker must not infer approval from the fact that the notification succeeded. Parent and child receipts can share a case lineage while retaining separate nonces, limits and effect receipts.

This design makes recovery more precise. After a timeout, the orchestrator asks which effect receipt exists for which child operation. It does not simply replay the whole workflow under the agent's standing role. That distinction prevents a harmless retry mechanism from becoming a confused deputy with batch privileges.

Signatures preserve history, not current permission

A signed decision receipt proves that a trusted issuer made a decision over particular bytes. It does not prove that the decision remains safe to execute. Between issue and use, the requester may lose entitlement, an approver may withdraw consent, the resource may enter a blocked state or another transaction may consume the available balance. This is a time-of-check to time-of-use problem expressed at a business boundary.

The executor therefore needs two categories of checks. Invariant checks verify facts that must match the signed receipt exactly: issuer, signature, proposer, audience, proposal digest, operation, limits, expiry and nonce. Currentness checks compare live authoritative state with assumptions named by the receipt: entitlement version, case status, account version, policy epoch, revocation handle and required approvals. Mixing the categories is dangerous. A valid signature cannot waive a failed currentness check, while a fresh database read cannot legitimise a mutated proposal.

There are three defensible responses when current state differs. The executor can deny and require a new proposal, which is safest for material changes. It can re-evaluate a narrowly defined predicate locally, such as “available refundable amount remains at least £390”, when policy permits that form of late binding. Or it can route to human review when the difference is meaningful but not mechanically classifiable. Quietly executing under the old receipt is not a fourth option.

Postcondition verification closes the remaining gap. An accepted command and a successful transport response do not necessarily prove the intended effect. The executor should record the authoritative state transition, link it to the one-time nonce and test a domain postcondition, such as one refund ledger entry for £390 against the named charge. If the outcome is unknown after a timeout, the system must read back before retrying. Otherwise a recovery loop can convert one authorised intent into two effects.

Operate the boundary as a measured control

Production monitoring should join issuance and execution rather than count API calls alone. Track decision receipts issued, consumed, expired unused, revoked and denied. Break denials down by proposer version, requester type, operation, resource, policy version and reason code. Sudden increases in resource substitution or stale entitlement may reveal prompt attacks, data drift or broken workflow state.

Sample allowed actions against the request and resource state that existed at the decision time. Sample denied actions to detect false blocks and systematic boundary probing. Rehearse emergency withdrawal of an agent version and business revocation of a user's entitlement. OpenID's Continuous Access Evaluation Profile defines event types through which cooperating systems can attenuate access when sessions, users, devices or applications change, but implementation still has to connect those signals to the decision and execution stores. [15]

Do not return the complete policy expression to the model. Reason codes such as REQUESTER_NOT_ENTITLED, PROPOSER_DENIED, PROPOSAL_MUTATED, WRONG_EXECUTOR_AUDIENCE, RESOURCE_STATE_CHANGED and REPLAY_DETECTED are sufficient for operations and evaluation without exposing every rule.

Part IV

How the claim changes under pressureChart · state
stateDiagram-v2
  [*] --> Claim
  state "Thought experiment, the same call" as Claim
  state "Authentication is necessary,<br/>then the real question begins" as Test
  state "A serious negative control" as Revision
  Claim --> Test: expose mechanism
  Test --> Decision: evidence survives
  Test --> Revision: rival survives
  Revision --> Claim: narrow or rebuild
  Decision --> [*]
The revision loop begins with Thought experiment, the same call and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Standards are parts, not proof

A vendor-neutral design should reuse standards for identity and token semantics while keeping the business decision object stable. The mistake is to treat any one primitive as evidence that delegation is solved.

Workload identity establishes the proposer or executor as a named software principal. SPIFFE SVIDs are one portable mechanism. NIST SP 800-207A places granular application and service identity inside a broader zero-trust architecture for cloud-native and multi-cloud systems. [13] Neither specifies which customer, case or business purpose the workload may represent.

Attribute and relationship policy expresses who may do what to which object under which conditions. NIST's ABAC definition evaluates subject, object, requested operation and, where relevant, environment attributes against policy. [12] Zanzibar demonstrates a large-scale relationship-based authorisation model and the importance of consistency between object and access-control state. [5] These mechanisms can resolve entitlement, but they still need the correct subject, object and operation to arrive from trusted bindings.

Token exchange and structured authorisation preserve subject and actor semantics across a hop and express fine-grained requirements. They are especially useful when a user-facing token must become a narrow service token. OAuth's 2025 security best-current-practice document strengthens the wider token environment, including advice shaped by attacks on OAuth deployments. [11] Yet an access token remains only as strong as its audience, claims, policy evaluation and resource-server enforcement.

Attenuable capabilities show how delegated authority can become narrower as it moves. Macaroons use chained message authentication codes and contextual caveats to constrain how, by whom and under what conditions authority is exercised. [4] Their design supplies a valuable intuition for decision receipts: delegation should remove power, not add it. Enterprise revocation, live business state and resource postconditions still need explicit treatment.

Transaction context propagation is an active standards area. As of August 2026, the IETF OAuth working group has an active Internet-Draft for Transaction Tokens intended to preserve user identity, workload identity and authorisation context through an internal call chain. [16] A related individual draft proposes agent context using subject and actor fields. [17] These are promising directions, but Internet-Drafts are work in progress and do not remove the need to bind a concrete proposal or verify an effect.

Practitioner synthesis

Use standards to carry identities, audience, proof of possession, attributes and revocation signals. Use a domain decision receipt to bind those primitives to the exact action and current case state. Keep the final veto beside the system that owns the authoritative resource.

Impersonation is a design choice, not a default

Simply issuing a token that makes the agent indistinguishable from the user can be appropriate for a narrow read where the resource service needs only the user's entitlement and the actor adds no independent authority. RFC 8693 calls this impersonation and distinguishes it from delegation, where the actor retains a separate identity.

Impersonation becomes hazardous when the agent's behaviour, software version or tool access matters to the decision. It can erase which workload constructed the request, make policy unable to distinguish human and automated actions, and grant the agent every right inside the token's scope. For material effects, delegation usually provides the better evidence because the resource can evaluate both represented subject and current actor.

The principle is not “never impersonate”. It is “do not erase an actor whose identity changes the risk”. A policy may permit a human to view a customer record interactively while denying an autonomous bulk read by the same user through an agent. Subject identity alone cannot express that difference.

The three-principal release decision

Choose control strength by consequence and reversibility. A private draft may need only authenticated user and workload identities plus ownership checks. Sending an external message adds recipient and content binding. A temporary account restriction needs current case state and bounded duration. Releasing funds or closing a high-risk alert may require an independent approver, a one-use decision receipt and a resource-local recheck.

Consequence and reversibility determine delegation strength A two-dimensional field places low-consequence reversible work near simple identity checks and high-consequence hard-to-reverse work near exact proposal binding, dual control and one-time execution. Workload identity + ownership check Three-principal receipt + current state Exact proposal + approver + one-use local execution private draft external message temporary restriction funds release Harder to reverse → Greater consequence →
Figure 8. Use the weakest mechanism that still makes the consequence defensible. Positions are illustrative. Business owners must classify consequence, including harms that cannot be undone by technical rollback.

Release evidence for one material operation

  1. Show the requester identity came from trusted session or workflow state, not model text.
  2. Show the proposer identity and deployed version were independently attested.
  3. Show the decision receipt binds requester, proposer, executor audience and exact proposal hash.
  4. Show entitlement and policy were evaluated at a freshness appropriate to the consequence.
  5. Show the executor rechecked authoritative state and denied a changed-state case.
  6. Show a cross-resource substitution, wrong executor and post-approval mutation all failed.
  7. Show the same capability could not create the effect twice, and join the decision to an effect receipt.

If one trajectory cannot prove these seven facts, the identity chain is incomplete. An IAM diagram, successful OAuth exchange or persuasive generated explanation cannot fill the missing evidence. The test should run in continuous integration, during policy changes and in recurring adversarial exercises.

Conclusion

The action must carry its authority

The confused deputy survives because software architectures repeatedly separate designation from authority. A caller names a target. A model proposes parameters. A privileged runtime contributes access. The resource checks the runtime and mistakes technical ability for delegated permission.

The repair is not to make the model claim a requester more carefully. It is to remove identity and authority from model-editable text. Preserve the requester in a trusted receipt. Preserve the proposer through workload identity and proposal provenance. Address a narrow decision receipt to the executor. Bind it to the exact proposal, policy, purpose, state and time. Let the resource make the final veto and record the observed effect.

The architecture decision changes from “give the agent an identity” to “make every material action prove whose authority it carries”. That decision survives model changes, cloud changes and protocol changes because it is expressed at the business-effect boundary.

Do not release a consequential agent after the happy path alone. Require one allowed case and at least five denied mutations: requester-resource substitution, unapproved proposer, wrong executor, changed proposal and replay. Add stale entitlement and changed state for irreversible work. When those tests fail closed, the three principals remain separate even while they collaborate. When they do not, the deputy is still confused.

The evidence conversationChart · sequence
sequenceDiagram
  participant Q as Question
  participant M as Mechanism
  participant E as Evidence
  participant D as Decision
  Q->>M: Opening case
  M->>E: Thought experiment, the same call
  E-->>M: compare rival explanations
  E->>D: Source ledger
  D-->>Q: revise scope or act
Question, mechanism, evidence and decision exchange the burdens developed in The Three Principals and the Confused Deputy. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Source ledger

Primary research

  1. Norm Hardy, The Confused Deputy: (or why capabilities might have been invented), 1988. Mechanism and original compiler case.
  2. Jerome H. Saltzer and Michael D. Schroeder, The Protection of Information in Computer Systems, 1975. Least privilege, separation of privilege and complete mediation.
  3. Mark S. Miller, Ka-Ping Yee and Jonathan S. Shapiro, Capability Myths Demolished, 2003. Ambient authority and capability properties.
  4. Arnar Birgisson et al., Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud, 2014. Attenuable delegated authority.
  5. Ruoming Pang et al., Zanzibar: Google's Consistent, Global Authorization System, 2019. Relationship-based authorisation and consistency.

Official standards and guidance

  1. IETF, RFC 8693: OAuth 2.0 Token Exchange. Delegation, impersonation, subject and actor semantics.
  2. IETF, RFC 9396: OAuth 2.0 Rich Authorization Requests. Structured fine-grained authorisation details.
  3. IETF, RFC 9449: OAuth 2.0 Demonstrating Proof of Possession. Application-layer sender-constrained tokens.
  4. IETF, RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens. Certificate-bound sender-constrained tokens.
  5. IETF, RFC 8707: Resource Indicators for OAuth 2.0. Explicit target-resource signalling and audience restriction.
  6. IETF, RFC 9700: Best Current Practice for OAuth 2.0 Security, 2025.
  7. NIST, SP 800-162: Guide to Attribute Based Access Control. Subject, object, operation and environment attributes.
  8. NIST, SP 800-207A: A Zero Trust Architecture Model for Access Control in Cloud-Native Applications, 2023.
  9. SPIFFE, SPIFFE concepts and SVIDs. Portable workload identity primitives.
  10. OpenID Foundation, Continuous Access Evaluation Profile 1.0, final specification, 2025.

Current work in progress

  1. IETF OAuth Working Group, Transaction Tokens, active working-group Internet-Draft. Cited as an emerging direction, not an established standard.
  2. Ashay Raut, Transaction Tokens For Agents, active individual Internet-Draft. The document has no formal IETF standing and is treated only as work in progress.

Practitioner synthesis

The requester-proposer-executor braid, request/proposal/decision/effect receipt chain, release matrix and synthetic refund scenario are original architecture synthesis for this article. They are proposed decision instruments, not published comparative efficacy results.