AI Research Library
Typed Actions and the Deterministic Authority Kernel
Rajesh Ranjan Mahapatra
A model may propose what should happen; only a small, typed, state-aware kernel should decide whether that effect is permitted now, bind it to authority, and prove what happened next.
© 2026 Rajesh Ranjan Mahapatra. 34 min read
Typed Actions and the Deterministic Authority Kernel
A model may propose what should happen; only a small, typed, state-aware kernel should decide whether that effect is permitted now, bind it to authority, and prove what happened next.
TLDR
- Model output should be treated as a proposal, never as authority. Fluent reasoning can select an action, but it must not supply the identity, permission or trusted state that makes the action executable.
- A deterministic authority kernel validates a typed proposal, derives the represented subject and actor from trusted channels, checks policy and current world state, then issues a narrow, expiring capability bound to one immutable action.
- The practical unit is a three-object protocol: proposal, decision receipt and effect receipt. This separation makes approval exact, retries safe, denials testable and post-incident reconstruction possible without preserving hidden reasoning.
- The kernel cannot repair bad policy, prove that a recommendation is wise or observe effects a downstream system cannot expose. It reduces authority errors only when every consequential path is mediated and state can be checked.
- Change the architecture review question from “Which tools may the agent call?” to “Which typed effects can this principal authorise, against which live state, and what evidence proves each effect occurred exactly once?”
The refund that was valid in every field
A relationship manager asks an assistant to reverse a £75 service fee after a documented complaint. The model retrieves the case, finds the ledger entry, produces the correct reason code and emits a perfectly valid tool call. Every required JSON field is present. The amount is an integer. The account identifier matches the expected pattern. The tool schema passes.
But one retrieved note contains an old customer identifier. The model selects that account instead of the account in the authenticated session. The service credential attached to the agent can reach both records, so the downstream API accepts the call. Nothing looks like a software failure: authentication succeeded, the payload was valid and the service returned 200 OK. The wrong customer received the reversal.
The defect was not that the model produced malformed syntax. It was that the system allowed model-controlled text to choose facts that belonged to the authority boundary. A valid action shape had been mistaken for a valid action.
The repair is not another instruction telling the model to be careful with account identifiers. The repair is to make the account scope come from signed user entitlement, to express the model output as a proposal, and to require an independent component to intersect that proposal with live policy and live state before any effect occurs.
The core answer is to split cognition from authority. Let the model interpret evidence, compare alternatives and propose a business action. Put a small deterministic mechanism between that proposal and every consequential resource. The mechanism must be non-bypassable, derive identity outside model-editable content, fail closed, bind approval to the exact proposal, recheck state beside the authoritative resource and emit evidence about both the decision and the effect.
flowchart TD A["The refund that was valid in<br/>every field"] --> B["On this page"] B --> C["Part I A valid shape is not<br/>authority"] C --> D["Part II The authority kernel,<br/>cut open"] D -. "re-test" .-> A classDef first fill:#0a1424,stroke:#D95545,stroke-width:2px,color:#e6f3ff classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff class A first class B,C,D step
Part IIThe authority kernel, cut open
The term “kernel” is borrowed carefully. NIST defines a reference monitor as a mechanism that is always invoked, tamperproof and small enough to analyse and test. A security kernel implements that concept inside a trusted computing base. An application authority kernel seeks the same properties for consequential business effects, but it operates across distributed services rather than inside one operating system.
The analogy matters because it suggests three tests. Can any tool path bypass the decision? Can the model, orchestration layer or retrieved content alter the enforcement code or trusted inputs? Is the mechanism small and deterministic enough to test exhaustively over its action vocabulary? The analogy breaks because a distributed application cannot obtain operating-system-level completeness by naming a policy service. Every downstream mutation endpoint must still enforce locally, and the authoritative state may change between decision and execution.
The proposal contract
A proposal contains only facts the model is allowed to choose or infer. For a fee reversal, those may be the action verb, target business resource, amount, controlled reason code, evidence references, expected state version and idempotency key. It should not contain the authenticated employee identity, the deployed agent identity, the set of entitled accounts, the effective policy or a Boolean named approved.
Closing the schema against unknown fields matters. Otherwise a model can add attractive but meaningless authority claims such as role: admin, user_confirmed: true or policy_exception: urgent. The fields may be ignored by a correct implementation, yet their presence can mislead logging, reviewers or later adapters. Trusted facts should be absent from the model’s writable type, not merely discouraged by prose.
A deterministic decision function
Let K be the deterministic kernel function, P the canonical typed proposal, T the trusted actor and subject context, St the authoritative state at decision time t, and Π the versioned policy. The result C is a narrow capability. K issues it only when every gate joined by the logical “and” symbol ∧ holds:
Valid(P) checks structure and controlled vocabularies. Authorised intersects the actor role, represented subject, purpose, action, resource and policy. Preconditions checks business facts such as “fee exists and is unreversed”. Fresh requires the proposal and any approval to refer to the current relevant state. Evidence requires the decision’s mandatory source references and human approval where policy demands it.
If any term becomes false, the result is deny, not a lower-confidence allow. If the policy version changes, the same inputs may yield a different decision by design. Within one policy version and trusted state snapshot, the result should be repeatable. Deterministic here means that identical trusted inputs produce the same decision, not that the software is free from defects or that the external world stops moving.
The three-object action protocol
The most useful implementation distinction is among the proposal, the decision receipt and the effect receipt. They are different records with different writers, trust levels and lifetimes.
| Object | Written by | What it may assert | What it must never stand in for |
|---|---|---|---|
| Typed proposal | Model or deterministic planner | Requested verb, resource, bounded parameters, evidence references, expected state, idempotency key | Identity, entitlement, approval, policy result or proof of execution |
| Decision receipt | Authority kernel | Allow or deny, reason code, trusted principals, purpose, policy version, proposal digest, state binding, expiry | Proof that the downstream effect occurred |
| Effect receipt | Resource owner or independent observer | Accepted, denied, applied or unknown; operation identifier; before and after state; readback evidence | A claim that the original business recommendation was wise |
A decision receipt makes review possible without exposing private chain-of-thought. It records what the system observed, which rules it applied and why it allowed or denied the proposal. An effect receipt closes a different gap. It answers whether the resource changed, not whether the API call looked successful.
The capability is a sealed answer, not a broad credential
When the kernel allows an action, it should issue a short-lived capability bound to the canonical proposal digest. The payload normally includes actor identity and deployed version, represented subject, purpose, action, resource, parameter limits, policy version, state version, expiry, audience and idempotency key. The executor verifies the signature and the binding before using it.
This capability may be represented by a signed token, a macaroon-like credential with caveats, an opaque handle to server-side state or a request evaluated directly at the resource. The representation is secondary. The invariant is that the model cannot mint, widen or retarget it. Approval authorises an immutable proposal, not an intention described in natural language.
A relationship manager clicking “approve refund” is not enough if the model can later alter the account, amount or reason code. The review event must sign or otherwise bind the digest of what was displayed. If any material parameter changes, a new proposal and new approval are required.
Delegation must narrow at every hop
Agent systems usually contain at least three acting identities: the human or organisation whose interests are represented, the deployed agent workload that proposes an operation and the service that finally executes it. Conflating them produces an audit record that says only “the agent did it”. That phrase hides the central authorisation question. The employee may be entitled to reverse a fee; the agent version may be approved to propose reversals; the fee service may be permitted to execute a valid capability. Those are three different grants.
OAuth Token Exchange distinguishes the subject of a token from an acting party and can preserve a delegation chain. That is useful plumbing, but exchange alone does not prove that authority became narrower. The application must still compare the incoming grant with the requested child grant. A system that turns a customer-service token into a portfolio-wide service credential has represented delegation while violating attenuation.
Write Cchild ⊑ Cparent when the child capability grants no more authority than its parent across resource set, action set, parameter ranges, purpose, audience, time window and acceptable state. The symbol ⊑ means “is no wider than”. Narrowing the amount from £100 to £75, the resource from a portfolio to one account or the lifetime from ten minutes to one request preserves the relation. Adding another account, purpose or mutation breaks it. The relation is partial because two capabilities can be incomparable: one may allow a larger amount for a shorter time.
The authority kernel should check attenuation mechanically whenever work passes to a sub-agent, tool broker or executor. It should also bind the parent decision identifier into the child receipt. Delegation is safe only when every hand-off can preserve provenance while reducing, never inventing, authority. In the running case, the employee’s portfolio entitlement narrows to one fee event, one £75 reversal, one reason code, one account state and one execution audience.
Keep the trusted base smaller than the agent runtime
The trusted base should contain the proposal canonicaliser, principal resolver, policy evaluator, state adapter, capability issuer and receipt linkage needed to decide one action. The model, prompt, retrieved documents, episodic memory, planner, general orchestration graph and user-interface prose remain outside it. They may supply proposals and evidence references. They cannot decide which identity is authenticated, which policy applies or whether the decision was allowed.
This boundary is operationally important. Agent runtimes change frequently, accept untrusted content and contain recovery logic that is difficult to reason about exhaustively. A small action vocabulary can be tested differently. For each verb, the kernel can close the schema, enumerate reason codes, define principal sources, name state preconditions and generate adversarial combinations. The decision core can then be released independently from prompt or model changes.
Policy selection must also be deterministic. The model may propose reverse_fee; a registry maps that verb to the applicable policy namespace and resource adapter. The model must not choose “legacy policy”, omit a disfavoured check or route the same mutation through a more permissive tool. Unknown actions, unresolved principals, missing state, unsupported evidence and policy-version mismatches should produce named denials rather than fallback behaviour.
Small does not mean centralised into one network service. Complete mediation can be realised by a shared library, signed capability, sidecar or resource-local policy check. The decisive property is that every material mutation path invokes equivalent enforcement. Batch jobs, administrator tools, browser automation and repair scripts belong in the inventory. Any exceptional path should carry separate identity, narrow operational purpose and evidence, because an undocumented bypass silently expands the trusted base to the whole estate.
Part IVA production-shaped worked case
Consider a synthetic commercial-banking assistant that prepares customer fee reversals. The scenario is designed to expose the mechanism; it is not a reported deployment. The authenticated employee is employee:RM-247. The attested workload is agent://rm-assist/refund-proposer@3.2.1. The employee may act only for a signed portfolio and only for the purpose complaint_remediation.
The model reads a complaint, a fee ledger entry and the current policy summary. It proposes reverse_fee on account:ACCT-100 for £75 using reason code SERVICE_FAILURE. It cites the complaint and fee event, states that it observed account version 12 and supplies a stable idempotency key tied to the case and fee.
The proposal does not contain the employee identity or entitlement. The host derives both from the authenticated session. It does not contain the agent role. The platform derives that from workload attestation and deployment metadata. It does not contain an “approved” flag. Under the illustrative policy, reversals up to £100 are permitted without a second person when the fee exists, the reason is controlled, the employee is entitled and the account is not restricted.
The kernel validates the proposal and computes its canonical digest. It intersects RM-247 with the trusted account entitlement, confirms the deployed agent version is allowed to propose this action, checks purpose, verifies fee FEE-9 exists for £75 and is unreversed, and confirms the account remains at version 12. It issues a capability that expires in two minutes and is bound to the proposal digest, account, amount, reason, actor, subject, purpose, policy version, state version and idempotency key.
The fee service validates the capability and repeats the local checks. It applies the reversal, increments the account state to version 13 and stores an effect receipt under the idempotency key. The network response is then lost. The agent reports UNKNOWN, not failure. Reconciliation queries the fee service, obtains the effect receipt and confirms that the reversal occurred once. A retry returns the stored receipt rather than applying a second reversal.
Now vary the case. If the model adds subject_id: employee:ADMIN, structural validation fails because identity is not a model-controlled field. If it targets ACCT-200, policy denies the proposal because that account is outside the employee’s trusted entitlement. If account version 13 appears after capability issuance, resource-local enforcement denies execution and requires a new decision. These negative paths show where authority actually lives. They matter more than a happy path that proves only that well-formed input can succeed.
Choose the control depth by consequence, authority and observability
Not every model output needs a full capability protocol. Excess control creates queues and tempts teams to bypass the system. The control depth should follow the effect, not the apparent sophistication of the model. Assess consequence, reversibility, delegated authority, sensitivity to live state, duplicate risk and the resource’s ability to expose an outcome.
| Question | If no | If yes |
|---|---|---|
| Can the operation change external state or disclose non-public data? | Use schema validation, ordinary service authorisation and rate limits. | Require a typed proposal mediated outside the model. |
| Does the agent act for a person, customer, business role or another agent? | Workload identity may be sufficient for low-risk own-authority reads. | Bind subject, actor and purpose separately; never accept identity from model text. |
| Could live state invalidate a previously correct decision? | Normal token expiry may be sufficient. | Bind a state version or digest and recheck beside the resource. |
| Could a retry duplicate a business effect? | Record the call normally. | Require a client intent key, durable fingerprint and result replay. |
| Would harm remain after technical rollback? | Automated action may be acceptable at validated thresholds. | Add exact preview, approval, narrow execution window and possibly dual control. |
| Can the resource provide authoritative readback? | Do not claim a verified outcome; retain human or compensating control. | Emit an effect receipt and reconcile unknown responses. |
Test the boundary, not only the policy syntax
A release gate should include user substitution, agent-version substitution, scope inflation, cross-resource use, stale entitlement, changed state, missing approval, changed proposal after approval, capability expiry, key reuse with different parameters, response loss after commit and a bypass attempt against every mutation endpoint. Each test needs a named denial or recovery result.
Cedar’s verification-guided development is instructive here. Its authors combine a formal model, mechanised proofs, differential random testing and property-based testing, and report that these methods found subtle defects in the validator and implementation. An application kernel rarely warrants the same formal investment on day one, but it should inherit the principle: keep the decision core small, specify invariants, generate hostile requests and compare implementations against a clear model.
stateDiagram-v2 [*] --> Claim state "Thought experiment one: change<br/>only ambient authority" as Claim state "The strongest objection:<br/>existing IAM already does this" as Test state "The proposal contract" as Revision Claim --> Test: expose mechanism Test --> Decision: evidence survives Test --> Revision: rival survives Revision --> Claim: narrow or rebuild Decision --> [*]
Failure boundary and negative control
The authority kernel solves a narrower problem than “safe agents”. It prevents a model from turning an untrusted proposal into authority outside explicitly encoded limits. It can also make stale decisions, duplicate effects and ambiguous outcomes visible. It does not establish that the model’s recommendation is accurate, fair, useful or lawful.
Bad policy remains executable policy. If the institution permits an unfair action, encodes the wrong threshold or grants an employee an excessive portfolio, a perfectly implemented kernel may faithfully allow harm. The kernel needs policy governance, representative evaluation, human contestability and incident response around it. It is an enforcement mechanism, not a source of values.
It also fails when the mediation claim is false. A second API, batch job, administrator path or browser automation route that can mutate the same resource without the kernel defeats complete mediation. Computer-use agents are especially difficult because clicks and pixels often sit above the real effect boundary. The safer interception point is the underlying transaction or HTTP request, where the action can again be typed and checked.
Finally, the kernel cannot verify an outcome the resource cannot expose. A “send email” endpoint may acknowledge queueing while final delivery remains unknown. A payment gateway may accept a request while settlement occurs later. The effect receipt must state the observed boundary honestly: accepted, queued, applied, settled or unresolved. Calling all of them success creates false evidence.
A negative control: the public weather lookup
Take a tool that reads a public weather forecast, exposes no private data, changes no state and has no delegated human authority. A schema, ordinary service authentication, egress control and rate limit may be enough. Adding proposal digests, one-use capabilities and effect receipts would increase latency and complexity without changing the material risk.
This negative control matters because it prevents the authority kernel from becoming ritual architecture. The mechanism earns its cost only where a model-selected operation can spend authority, reveal protected information or create a consequential state transition. Pure computation, public reads and private drafts can often remain outside the full protocol.
Protocols and frameworks do not remove the boundary
Tool protocols can describe schemas and transport calls. Current MCP specifications also define authorisation flows and warn that tools represent arbitrary code execution, while acknowledging that the protocol itself cannot enforce all security principles. This is the correct boundary. MCP, A2A, a graph orchestrator or a cloud agent runtime can carry the action protocol, but the application still owns business identity, purpose, policy, preconditions, idempotency and outcome verification.
The same rule applies to model-level prompt-injection defences. Training an instruction hierarchy or using another model to screen tool results may reduce attacks. It cannot create deterministic authority. Architecture-level defences such as CaMeL are compelling precisely because they assume the underlying model may remain susceptible and place enforceable control around it.
Executable artefact: a small authority kernel
The following Python programme uses only the standard library and synthetic state. It demonstrates the exact claim. The model-controlled proposal cannot supply identity. Trusted context carries the actor, subject, purpose and account entitlement. The kernel validates, authorises and signs a narrow capability. The resource rechecks state, applies one mutation, records an effect receipt and safely reconciles a lost response.
The positive test commits a £75 reversal, deliberately loses the response, reads the effect back and proves that a retry does not duplicate it. Negative tests reject a model-supplied identity, a cross-account action and execution after state drift. The HMAC key is intentionally synthetic and is not a production key-management pattern.
"""Synthetic authority-kernel demonstration for Paper 117.
No external packages, private endpoints or real credentials are used.
Expected result: one authorised fee reversal is applied exactly once; attempts to
supply identity, cross account scope, or execute against changed state are denied.
"""
from __future__ import annotations
from dataclasses import dataclass, asdict
from datetime import datetime, timedelta, timezone
from hashlib import sha256
import hmac
import json
from typing import Any
from uuid import uuid4
POLICY_VERSION = "refund-policy/7"
KERNEL_KEY = b"synthetic-demo-key-not-for-production"
ALLOWED_REASONS = {"SERVICE_FAILURE", "DUPLICATE_FEE"}
class ContractError(ValueError):
"""The model proposal did not match the typed action contract."""
@dataclass(frozen=True)
class TrustedContext:
actor_id: str
actor_version: str
subject_id: str
purpose: str
entitled_accounts: frozenset[str]
@dataclass(frozen=True)
class Capability:
payload: dict[str, Any]
signature: str
@dataclass(frozen=True)
class DecisionReceipt:
decision_id: str
decision: str
reason: str
proposal_digest: str
policy_version: str
capability: Capability | None = None
@dataclass(frozen=True)
class EffectReceipt:
operation_id: str
status: str
decision_id: str
proposal_digest: str
resource: str
before: dict[str, Any]
after: dict[str, Any]
executed_at: str
replayed: bool = False
class SyntheticLedger:
"""Authoritative state plus an idempotency/effect ledger."""
def __init__(self) -> None:
self.accounts: dict[str, dict[str, Any]] = {
"account:ACCT-100": {
"state_version": 12,
"fees": {
"FEE-9": {
"amount_gbp": 75,
"reversed": False,
"reversal_count": 0,
}
},
},
"account:ACCT-200": {
"state_version": 4,
"fees": {
"FEE-3": {
"amount_gbp": 40,
"reversed": False,
"reversal_count": 0,
}
},
},
}
self.effects_by_idempotency_key: dict[str, EffectReceipt] = {}
self.fingerprint_by_idempotency_key: dict[str, str] = {}
TOP_LEVEL_FIELDS = {
"action",
"resource",
"parameters",
"evidence_refs",
"expected_state_version",
"idempotency_key",
}
PARAMETER_FIELDS = {"fee_event_id", "amount_gbp", "reason_code"}
def canonical_json(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def digest(value: Any) -> str:
return sha256(canonical_json(value).encode("utf-8")).hexdigest()
def validate_proposal(raw: dict[str, Any]) -> dict[str, Any]:
"""Structural validation. It proves shape, not permission."""
unknown = set(raw) - TOP_LEVEL_FIELDS
missing = TOP_LEVEL_FIELDS - set(raw)
if unknown:
raise ContractError(f"unknown model-controlled fields: {sorted(unknown)}")
if missing:
raise ContractError(f"missing required fields: {sorted(missing)}")
if raw["action"] != "reverse_fee":
raise ContractError("unsupported action")
if not isinstance(raw["resource"], str) or not raw["resource"].startswith("account:"):
raise ContractError("resource must be an account identifier")
params = raw["parameters"]
if not isinstance(params, dict) or set(params) != PARAMETER_FIELDS:
raise ContractError("parameters must contain fee_event_id, amount_gbp and reason_code only")
if not isinstance(params["amount_gbp"], int) or not 0 < params["amount_gbp"] <= 500:
raise ContractError("amount_gbp must be an integer from 1 to 500")
if params["reason_code"] not in ALLOWED_REASONS:
raise ContractError("reason_code is not in the controlled vocabulary")
if not isinstance(raw["evidence_refs"], list) or len(raw["evidence_refs"]) < 2:
raise ContractError("at least two evidence references are required")
if not isinstance(raw["expected_state_version"], int):
raise ContractError("expected_state_version must be an integer")
if not isinstance(raw["idempotency_key"], str) or len(raw["idempotency_key"]) < 8:
raise ContractError("idempotency_key is too short")
return raw
def sign_payload(payload: dict[str, Any]) -> str:
return hmac.new(KERNEL_KEY, canonical_json(payload).encode("utf-8"), sha256).hexdigest()
def authorise(
raw_proposal: dict[str, Any],
trusted: TrustedContext,
ledger: SyntheticLedger,
now: datetime,
) -> DecisionReceipt:
"""Deterministically derive authority from trusted context, policy and state."""
try:
proposal = validate_proposal(raw_proposal)
except ContractError as exc:
return DecisionReceipt(
decision_id=str(uuid4()),
decision="DENY",
reason=f"CONTRACT_INVALID: {exc}",
proposal_digest=digest(raw_proposal),
policy_version=POLICY_VERSION,
)
proposal_digest = digest(proposal)
resource = proposal["resource"]
decision_id = str(uuid4())
if trusted.actor_version != "3.2.1":
reason = "AGENT_VERSION_DENIED"
elif trusted.purpose != "complaint_remediation":
reason = "PURPOSE_DENIED"
elif resource.removeprefix("account:") not in trusted.entitled_accounts:
reason = "SUBJECT_NOT_ENTITLED"
elif resource not in ledger.accounts:
reason = "RESOURCE_NOT_FOUND"
else:
account = ledger.accounts[resource]
params = proposal["parameters"]
fee = account["fees"].get(params["fee_event_id"])
if account["state_version"] != proposal["expected_state_version"]:
reason = "CASE_STATE_CHANGED"
elif fee is None:
reason = "FEE_NOT_FOUND"
elif fee["reversed"]:
reason = "ALREADY_REVERSED"
elif fee["amount_gbp"] != params["amount_gbp"]:
reason = "AMOUNT_MISMATCH"
else:
reason = "ALLOW"
if reason != "ALLOW":
return DecisionReceipt(
decision_id=decision_id,
decision="DENY",
reason=reason,
proposal_digest=proposal_digest,
policy_version=POLICY_VERSION,
)
capability_payload = {
"decision_id": decision_id,
"proposal_digest": proposal_digest,
"actor_id": trusted.actor_id,
"actor_version": trusted.actor_version,
"subject_id": trusted.subject_id,
"purpose": trusted.purpose,
"action": proposal["action"],
"resource": resource,
"parameters": proposal["parameters"],
"state_version": proposal["expected_state_version"],
"policy_version": POLICY_VERSION,
"idempotency_key": proposal["idempotency_key"],
"expires_at": (now + timedelta(minutes=2)).isoformat(),
}
capability = Capability(capability_payload, sign_payload(capability_payload))
return DecisionReceipt(
decision_id=decision_id,
decision="ALLOW",
reason="POLICY_SATISFIED",
proposal_digest=proposal_digest,
policy_version=POLICY_VERSION,
capability=capability,
)
def execute(
decision: DecisionReceipt,
ledger: SyntheticLedger,
now: datetime,
*,
lose_response_after_commit: bool = False,
) -> EffectReceipt | dict[str, str]:
"""Resource-local enforcement, idempotent mutation and effect evidence."""
if decision.decision != "ALLOW" or decision.capability is None:
return {"status": "DENIED", "reason": "NO_EXECUTION_CAPABILITY"}
cap = decision.capability
if not hmac.compare_digest(cap.signature, sign_payload(cap.payload)):
return {"status": "DENIED", "reason": "CAPABILITY_SIGNATURE_INVALID"}
if datetime.fromisoformat(cap.payload["expires_at"]) < now:
return {"status": "DENIED", "reason": "CAPABILITY_EXPIRED"}
key = cap.payload["idempotency_key"]
fingerprint = cap.payload["proposal_digest"]
prior = ledger.effects_by_idempotency_key.get(key)
if prior is not None:
if ledger.fingerprint_by_idempotency_key[key] != fingerprint:
return {"status": "DENIED", "reason": "IDEMPOTENCY_KEY_REUSE_MISMATCH"}
return EffectReceipt(**{**asdict(prior), "replayed": True})
resource = cap.payload["resource"]
account = ledger.accounts[resource]
if account["state_version"] != cap.payload["state_version"]:
return {"status": "DENIED", "reason": "CASE_STATE_CHANGED"}
params = cap.payload["parameters"]
fee = account["fees"][params["fee_event_id"]]
if fee["reversed"]:
return {"status": "DENIED", "reason": "ALREADY_REVERSED"}
before = {
"state_version": account["state_version"],
"reversed": fee["reversed"],
"reversal_count": fee["reversal_count"],
}
fee["reversed"] = True
fee["reversal_count"] += 1
account["state_version"] += 1
after = {
"state_version": account["state_version"],
"reversed": fee["reversed"],
"reversal_count": fee["reversal_count"],
}
receipt = EffectReceipt(
operation_id=str(uuid4()),
status="APPLIED",
decision_id=cap.payload["decision_id"],
proposal_digest=fingerprint,
resource=resource,
before=before,
after=after,
executed_at=now.isoformat(),
)
ledger.effects_by_idempotency_key[key] = receipt
ledger.fingerprint_by_idempotency_key[key] = fingerprint
if lose_response_after_commit:
return {"status": "UNKNOWN", "reason": "RESPONSE_LOST_AFTER_COMMIT"}
return receipt
def readback(idempotency_key: str, ledger: SyntheticLedger) -> EffectReceipt | None:
"""Independent reconciliation after a timeout or lost response."""
return ledger.effects_by_idempotency_key.get(idempotency_key)
def base_proposal() -> dict[str, Any]:
return {
"action": "reverse_fee",
"resource": "account:ACCT-100",
"parameters": {
"fee_event_id": "FEE-9",
"amount_gbp": 75,
"reason_code": "SERVICE_FAILURE",
},
"evidence_refs": ["case:CASE-7", "ledger:FEE-9"],
"expected_state_version": 12,
"idempotency_key": "CASE-7:FEE-9:REVERSE",
}
def run_demo() -> None:
now = datetime(2030, 1, 15, 12, 0, tzinfo=timezone.utc)
trusted = TrustedContext(
actor_id="agent://rm-assist/refund-proposer",
actor_version="3.2.1",
subject_id="employee:RM-247",
purpose="complaint_remediation",
entitled_accounts=frozenset({"ACCT-100"}),
)
# Positive case: authorised action commits, but the network response is lost.
ledger = SyntheticLedger()
proposal = base_proposal()
decision = authorise(proposal, trusted, ledger, now)
assert decision.decision == "ALLOW"
first_attempt = execute(decision, ledger, now, lose_response_after_commit=True)
assert first_attempt["status"] == "UNKNOWN"
observed = readback(proposal["idempotency_key"], ledger)
assert observed is not None and observed.status == "APPLIED"
# Safe retry: the stored effect is replayed and the fee is not reversed twice.
retry = execute(decision, ledger, now + timedelta(seconds=5))
assert isinstance(retry, EffectReceipt) and retry.replayed
assert ledger.accounts["account:ACCT-100"]["fees"]["FEE-9"]["reversal_count"] == 1
# Negative case 1: the model tries to supply trusted identity in its payload.
injected_identity = {**base_proposal(), "subject_id": "employee:ADMIN"}
identity_decision = authorise(injected_identity, trusted, SyntheticLedger(), now)
assert identity_decision.decision == "DENY"
assert identity_decision.reason.startswith("CONTRACT_INVALID")
# Negative case 2: a validly shaped proposal targets an account outside entitlement.
cross_account = base_proposal()
cross_account["resource"] = "account:ACCT-200"
cross_account["parameters"] = {
"fee_event_id": "FEE-3",
"amount_gbp": 40,
"reason_code": "SERVICE_FAILURE",
}
cross_account["expected_state_version"] = 4
cross_account["idempotency_key"] = "CASE-7:FEE-3:REVERSE"
cross_decision = authorise(cross_account, trusted, SyntheticLedger(), now)
assert cross_decision.decision == "DENY"
assert cross_decision.reason == "SUBJECT_NOT_ENTITLED"
# Negative case 3: state changes after capability issuance and before execution.
drift_ledger = SyntheticLedger()
drift_decision = authorise(base_proposal(), trusted, drift_ledger, now)
drift_ledger.accounts["account:ACCT-100"]["state_version"] += 1
drift_result = execute(drift_decision, drift_ledger, now + timedelta(seconds=10))
assert drift_result == {"status": "DENIED", "reason": "CASE_STATE_CHANGED"}
print("positive decision:", decision.decision)
print("lost response:", first_attempt)
print("readback:", observed.status, observed.after)
print("safe retry replayed:", retry.replayed)
print("identity injection:", identity_decision.reason)
print("cross-account attempt:", cross_decision.reason)
print("state-drift execution:", drift_result["reason"])
if __name__ == "__main__":
run_demo()
Expected output and the invariants it proves
positive decision: ALLOW
lost response: {'status': 'UNKNOWN', 'reason': 'RESPONSE_LOST_AFTER_COMMIT'}
readback: APPLIED {'state_version': 13, 'reversed': True, 'reversal_count': 1}
safe retry replayed: True
identity injection: CONTRACT_INVALID: unknown model-controlled fields: ['subject_id']
cross-account attempt: SUBJECT_NOT_ENTITLED
state-drift execution: CASE_STATE_CHANGED
The programme proves only its own synthetic invariants. It does not prove the business policy correct, the cryptographic construction suitable for production, the executor non-bypassable or the state store durable under arbitrary failure. Those become implementation and assurance work.
Sources and evidence boundary
Primary research
- Jerome H. Saltzer and Michael D. Schroeder, The Protection of Information in Computer Systems. Foundational protection principles and capability versus access-control architectures.
- Norm Hardy, The Confused Deputy. The canonical account of a privileged deputy misusing ambient authority.
- Arnar Birgisson and colleagues, Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud. Delegable, attenuated credentials with contextual caveats.
- Joseph W. Cutler and colleagues, Cedar: A New Language for Expressive, Fast, Safe, and Analyzable Authorization, plus Craig Disselkoen and colleagues, How We Built Cedar: A Verification-Guided Approach.
- Edoardo Debenedetti and colleagues, AgentDojo, and Defeating Prompt Injections by Design. Benchmark and architecture-level capability defence for tool-using agents.
Standards and official specifications
- NIST, reference monitor definition and SP 800-207A for granular application and service identity policy.
- IETF, RFC 8693: OAuth 2.0 Token Exchange and RFC 9396: OAuth 2.0 Rich Authorization Requests.
- SPIFFE, Workload API specification for workload identities and verifiable identity documents.
- JSON Schema, Draft 2020-12, used here only for structural-validation semantics.
- IETF, RFC 9110, idempotent methods, and the MCP tools specification with its security guidance.
Practitioner source
- Amazon Builders’ Library, Making retries safe with idempotent APIs. Used for the implementation pattern around client intent, duplicate recognition and result replay, not as evidence for a general scientific claim.
sequenceDiagram participant Q as Question participant M as Mechanism participant E as Evidence participant D as Decision Q->>M: The refund that was valid in every field M->>E: Thought experiment one: change only ambient authority E-->>M: compare rival explanations E->>D: Move autonomy into proposals, not permissions D-->>Q: revise scope or act
Move autonomy into proposals, not permissions
An agent becomes dangerous when its ability to form a plausible sentence is allowed to spend ambient authority. The durable design move is simple to state and demanding to implement: keep the model free to propose, while making permission deterministic, narrow, current and independently evidenced.
The typed proposal preserves model usefulness. The authority kernel derives principals and purpose from trusted channels, evaluates versioned policy and live state, and issues a capability bound to one immutable action. Resource-local enforcement keeps the final veto beside authoritative state. Decision and effect receipts preserve what was allowed and what actually happened. Idempotency and readback keep a lost response from becoming either a duplicate or an invisible omission.
This changes the architecture review. Do not approve an agent because its tool list looks narrow or its prompt says it must ask before acting. Select one material operation and demonstrate a denied identity substitution, a denied cross-resource proposal, a denied stale-state capability, an exact approval binding, a response lost after commit, a safe retry and a verified readback. If any effect path bypasses that chain, the authority boundary is still inside the model.
The decision is therefore not whether to trust the model. It is where trust is allowed to become power. Place that transition in the smallest mechanism the system can make non-bypassable, inspectable and testable. Everything before it is a proposal. Everything after it must leave evidence.