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
- 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.
- Preserve three roles separately: requester, proposer and executor. Bind them to an immutable action, target, purpose, policy version, current state and expiry.
- A model may construct a proposal; trusted policy code mints authority; the resource-local executor makes the final state and replay checks.
- 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.
- 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
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
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]
Ambient-authority path
Bound-authority path
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.
Unknown caller, no binding
Deny. Neither the actor nor the authority relationship can be trusted.
Unknown caller, claimed binding
Deny. A well-formed action receipt is not useful if its signer or presenter is not trusted.
Valid caller, no binding
The dangerous quadrant. The workload is genuine, yet may spend its own ambient authority for another party's request.
Valid caller, exact binding
Eligible for execution, subject to live state, policy, replay and postcondition checks.
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 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.
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.
requested_by, proposed_by, executor_audience, and approver when required.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.
a91….H(P) = a91….7d4….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.
| Injected variation | Identity-only baseline | Three-principal path | Required interpretation |
|---|---|---|---|
| Valid request and unchanged proposal | Allow | Allow | Positive control |
| Account substituted by proposal | Allow | Deny | Requester-resource binding works |
| Unapproved proposer build | Deny only if caller check differs | Deny | Proposal provenance is enforced |
| Amount changed after capability issue | Allow | Deny | Approval binds to exact object |
| Capability presented to wrong executor | Allow if token is broad | Deny | Audience is narrow |
| User entitlement version changes | Allow until session refresh | Deny | Business authority is current |
| Resource state advances | Allow | Deny | Local state veto works |
| Same capability used twice | Allow | Deny | Business replay protection works |
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
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.
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
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 --> [*]
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.
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.
Release evidence for one material operation
- Show the requester identity came from trusted session or workflow state, not model text.
- Show the proposer identity and deployed version were independently attested.
- Show the decision receipt binds requester, proposer, executor audience and exact proposal hash.
- Show entitlement and policy were evaluated at a freshness appropriate to the consequence.
- Show the executor rechecked authoritative state and denied a changed-state case.
- Show a cross-resource substitution, wrong executor and post-approval mutation all failed.
- 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.
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
Source ledger
Primary research
- Norm Hardy, The Confused Deputy: (or why capabilities might have been invented), 1988. Mechanism and original compiler case.
- Jerome H. Saltzer and Michael D. Schroeder, The Protection of Information in Computer Systems, 1975. Least privilege, separation of privilege and complete mediation.
- Mark S. Miller, Ka-Ping Yee and Jonathan S. Shapiro, Capability Myths Demolished, 2003. Ambient authority and capability properties.
- Arnar Birgisson et al., Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud, 2014. Attenuable delegated authority.
- Ruoming Pang et al., Zanzibar: Google's Consistent, Global Authorization System, 2019. Relationship-based authorisation and consistency.
Official standards and guidance
- IETF, RFC 8693: OAuth 2.0 Token Exchange. Delegation, impersonation, subject and actor semantics.
- IETF, RFC 9396: OAuth 2.0 Rich Authorization Requests. Structured fine-grained authorisation details.
- IETF, RFC 9449: OAuth 2.0 Demonstrating Proof of Possession. Application-layer sender-constrained tokens.
- IETF, RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens. Certificate-bound sender-constrained tokens.
- IETF, RFC 8707: Resource Indicators for OAuth 2.0. Explicit target-resource signalling and audience restriction.
- IETF, RFC 9700: Best Current Practice for OAuth 2.0 Security, 2025.
- NIST, SP 800-162: Guide to Attribute Based Access Control. Subject, object, operation and environment attributes.
- NIST, SP 800-207A: A Zero Trust Architecture Model for Access Control in Cloud-Native Applications, 2023.
- SPIFFE, SPIFFE concepts and SVIDs. Portable workload identity primitives.
- OpenID Foundation, Continuous Access Evaluation Profile 1.0, final specification, 2025.
Current work in progress
- IETF OAuth Working Group, Transaction Tokens, active working-group Internet-Draft. Cited as an emerging direction, not an established standard.
- 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.