The migration that passed every API test

Imagine a bank that has built a service agent on a managed runtime. The model gathers evidence, drafts a recommendation and, after approval, calls an account tool. Twelve months later the bank decides to move the reasoning workload. The new provider supports the same model class, structured output and function calls. The team ports prompts, tool schemas and evaluation sets. Every isolated API test passes.

The migration still fails. The original runtime owns the conversation thread, the pending approval, the mapping between retrieved sources and generated claims, the record of which authority was delegated, and the retry state for a tool call that timed out after acceptance. The team can reproduce the words of the application, but not the institutional state from which an action was permitted. Its apparent portability was only surface compatibility.

The central mistake is treating the agent runtime as the unit that must be portable. A runtime is allowed to be specialised, opinionated and temporary. The unit that must survive is the institution's decision contract: who requested the work, what state was authoritative, which context was admissible, which action was proposed, what authority bounded it, what evidence supported the decision, and how an uncertain outcome will be reconciled.

Call the boundary around that contract the migration cut. On one side sit models, planners, agent frameworks and managed runtimes. On the other sit enterprise-owned control primitives and systems of record. A runtime may plan, classify, reason or negotiate. It may not become the sole keeper of identity, authority, business state, evidence or recovery. Vendor neutrality appears when a runtime can be removed at that cut without making an in-flight decision unintelligible or unauthorisable.

This produces a sharper architecture rule: own the invariant, rent the worker. The enterprise owns the meanings that must remain stable. Providers compete on the quality and economics of the workers that operate inside those meanings.

Part I

Draw the migration cut before choosing the fabric

The phrase “vendor-neutral” often produces the wrong design reflex. Teams create a wrapper around every model API, reduce every tool to a generic function, and expose only features common to all providers. This can make switching easier, but it can also flatten useful differences and create a lowest-common-denominator platform. More importantly, API abstraction says little about whether a consequential decision can be reconstructed after the runtime changes.

A defensible fabric begins with a different question: which facts and permissions must remain true even if every stochastic worker changes? The answer belongs in a common control plane. Runtime adapters then translate between native capabilities and an enterprise contract. A strong provider can still offer computer use, long context, private reasoning, specialised tools or remote agents. Those features are recorded as capabilities, not mistaken for institutional authority.

The common control plane sits below competing runtimes, not inside one of them

Figure 1. Common control plane over runtimes. Conceptual reference architecture. The upper layer may change by provider or use case. The lower contract remains enterprise-owned and governs every consequential crossing.

Thought experiment one: delete the runtime

Imagine stopping the current runtime at noon, with no export from its proprietary console. Do not replace it yet. Ask seven questions. Can the institution still identify the requestor, proposer and executing workload? Can it recover authoritative case state? Can it prove which source claims were current and permitted? Can it reconstruct the exact action contract? Can it show the grant that authorised the proposal? Can it join decision evidence to the observed effect? Can it resume or reconcile an uncertain action?

If any answer depends on the disappeared console, the fabric is not yet neutral at that primitive. Exportability after the fact is weaker than ownership during execution. A provider may offer excellent export functions, yet an in-flight approval or timeout can still depend on semantics understood only by that provider.

The migration cut separates replaceable cognition from durable institutional meaning

Figure 2. The migration cut. Conceptual cutaway. Portability does not require private reasoning formats to match. It requires the meaning needed to authorise, observe and recover an effect to exist outside them.

Portability is a bottleneck property

Suppose each primitive is scored 0, 1 or 2. A score of 0 means vendor-owned or unreconstructable. A score of 1 means exportable with manual interpretation. A score of 2 means enterprise-owned, versioned and conformance-tested. Averaging these scores is misleading. Six strong primitives do not compensate for one missing authority grant or one unrecoverable outcome.

P = min(I, S, C, T, A, E, R)

This is an intentionally severe decision rule, not a scientific law. It represents a practical fact: the weakest primitive governs whether a consequential case can cross the cut safely. In the illustrative profile below, evidence is well designed, but recovery remains provider-bound. The system's effective portability is therefore zero.

A single missing primitive collapses consequential portability

Figure 3. Portability as a bottleneck. Illustrative ordinal scores, not measured data. Averages hide the one primitive that can prevent a safe migration or replay.

Practitioner synthesis. API compatibility reduces engineering friction. The migration cut reduces institutional dependence. A mature design needs both, but only the second protects decision meaning.

Part II

Keep seven primitives outside the runtime

The seven primitives are not seven new products. They are seven meanings that must be explicit somewhere in the architecture. A small system may implement them in one service and one database. A large estate may distribute them across identity, workflow, policy, context and observability platforms. Vendor neutrality depends on their contracts, not on their deployment topology.

A proposal becomes an authorised effect only by crossing all seven tests

Figure 4. The seven-test trajectory. Conceptual sequence. Implementations may evaluate tests in parallel, but none is semantically interchangeable with another.

1. Identity binds three principals, not one service account

A consequential action normally involves at least three principals: the person or system that requested work, the agent that proposed an action, and the workload that executed it. Collapsing them into one technical identity creates a confused deputy. The tool sees a valid service credential but cannot tell whose intent it serves, which agent transformed that intent, or which workload actually crossed the effect boundary.

The portable identity record therefore binds all three. Human identity may come from the enterprise identity provider. Workload identity can use platform-neutral identifiers such as SPIFFE IDs and short-lived verifiable identity documents.8 Delegation can use token exchange and structured authorisation details rather than handing a broad bearer token to every sub-agent.6 Authentication names an actor; authority limits what that actor may do for this purpose, resource, case and time.

2. State names current reality, not remembered conversation

A provider thread is useful conversational history. It is not authoritative world state. The fabric needs a versioned case record whose changes can be compared, replayed and rejected when stale. That state may include case status, current owner, pending approvals, completed effects and expected next transitions. It should be addressable without asking a model to infer it from prose.

The distinction matters during races. An agent may prepare a refund while another process closes the case. If the proposal carries expected state version 17 but the system of record is now at 18, the deterministic boundary rejects or replans. Replaying a chat transcript would not reliably expose the conflict. State is what the organisation is prepared to treat as true; memory is what a worker retained about prior interaction.

3. Context is a set of governed claims, not a large prompt

Context must carry more than text. Each decisive claim needs a source identifier, digest, effective interval, retrieval time, permitted purpose, confidentiality class and supersession relation where relevant. The model may receive a readable rendering, but the control plane retains typed claims. That allows a validator to ask whether a policy was current, whether a customer document was permitted for this purpose, and whether the cited evidence actually belongs to the case.

This is where a context compiler earns its name. It converts documents, records and events into a decision-specific evidence package. MCP resources can standardise how a host obtains contextual material, but the current MCP architecture intentionally leaves security and consent enforcement with the host, and the protocol itself is stateless.1 The enterprise must still decide what a source authorises and for how long.

4. Actions describe effects, not just function signatures

JSON Schema or OpenAPI can express names, fields and types.5 A consequential action contract must also state preconditions, expected state version, resource scope, idempotency behaviour, reversibility, readback method, postconditions, error taxonomy and compensation where one genuinely exists. The runtime proposes against this canonical contract. An adapter translates the proposal into a native API, MCP tool call, message or legacy command.

The contract should distinguish a proposal from execution. A model can produce `set_account_limit(amount=1800)`. It does not thereby possess the right to invoke that operation. The action kernel validates semantic fields and state before an execution adapter receives a short-lived capability. Tool connectivity is not effect authority.

5. Authority narrows at every delegation

Authority is a grant bound to principals, purpose, action, resource, case, limits, expiry and required approval. It is not the same as tool discoverability, model instruction or network reachability. A parent agent may ask a specialist agent to calculate an amount, but the child does not inherit the parent's ability to execute a payment. A planner may know a tool exists without receiving a token that can call it.

OAuth token exchange provides a standard mechanism for delegation and impersonation, while Rich Authorization Requests can carry structured authorisation details.67 Those mechanisms carry grants. The institution still defines the business policy that decides whether a grant may be minted. A separate policy decision point, implemented with a policy engine or equivalent deterministic service, keeps that judgement outside the stochastic worker.18

Delegation should attenuate authority as work becomes more specialised

Figure 5. The authority cone. Conceptual authority flow. The colours and widths are illustrative. Each handoff reduces scope until the executor receives only the capability needed for the accepted effect.

Thought experiment two: perfect interoperability, failed authority

Consider two agents from different vendors. A2A carries a task and its artefacts flawlessly. The second agent discovers a tool through MCP and invokes it with schema-valid arguments. Every protocol conformance test passes. Now vary one feature: instead of exchanging a broad service token, the coordinator issues a case-bound, action-bound grant that expires in sixty seconds. In the first world, a compromised specialist can use ambient power beyond its task. In the second, it can return an analysis or execute one approved effect, depending on its grant.

The difference is not better interoperability. It is explicit authority. A protocol can transport a request securely while the request remains over-authorised. This is why protocol adoption cannot substitute for the authority kernel.

6. Evidence joins claims, decisions, actions and observed effects

Operational evidence should answer a reviewer’s questions without requiring access to a provider’s hidden reasoning. A decision receipt records the request, identities, state version, decisive claims, policy version, proposed action, grant and disposition. An effect receipt records what the effector accepted, its idempotency key, observed postcondition and external reference. A recovery receipt records reconciliation, compensation or escalation.

W3C Trace Context helps correlate calls across process boundaries, CloudEvents provides a common event envelope, and W3C PROV supplies a general model for entities, activities and agents involved in producing an artefact.101112 OpenTelemetry can carry spans and semantic attributes, but telemetry and evidence are not identical. Telemetry is optimised for operating systems; evidence must be retained according to the decision’s obligations. Prompt and tool content can also contain sensitive data, so content capture should be selective rather than assumed.16

7. Recovery treats an unknown outcome as a first-class state

A timeout does not mean failure. The external system may have accepted the action before the response was lost. Blind retry can duplicate a payment, notice, order or account change. The contract therefore declares idempotency, readback and compensation semantics before execution. The workflow enters `UNKNOWN_OUTCOME`, observes the external state, and only then decides whether to complete, retry, compensate or escalate.

Durable workflow engines can replay orchestration and resume after failure; saga patterns coordinate compensating transactions when a distributed transaction cannot be atomic.1715 Neither turns every real-world effect into a reversible one. Sending a disclosure, filing a report or communicating a price may be impossible to undo. Recovery means restoring a governed state of knowledge and control, not pretending history can always be erased.

Part III

Use standards as bridges, not as the fabric

The current standards landscape is rich enough to avoid inventing every wire format. MCP defines a host, client and server architecture for connecting model applications to resources, prompts and tools.1 A2A defines discovery, messages, tasks, status updates and artefacts between independent agent systems.3 OpenAPI types HTTP interfaces. OAuth and SPIFFE support delegated and workload identity. CloudEvents, Trace Context, PROV and OpenTelemetry support events, correlation, provenance and telemetry. Open Workflow Specification supplies a vendor-neutral workflow definition, while durable engines and saga patterns supply execution and recovery mechanisms.141715

These standards are complementary because they answer different questions. MCP asks how a host communicates with capability servers. A2A asks how independent agents exchange work.4 OAuth asks how a client obtains and presents delegated access. SPIFFE asks how a workload proves its identity. OpenTelemetry asks how operations are observed. None decides whether a particular customer case is at the correct version, whether this policy claim superseded another, whether a human approval applies to this amount, or whether a timed-out effect actually occurred.

No protocol covers the full decision-and-recovery contract

The comparison has seven control columns. Scroll the table horizontally on a narrow screen.

Standard or familyIdentityStateContextActionAuthorityEvidenceRecovery
MCP△ connection security○ external● resources● tools△ host-enforced△ protocol events○ external
A2A△ security schemes△ task lifecycle△ messages△ tasks and artefacts△ transport-level△ task history△ task states
OpenAPI and JSON Schema○ external○ external△ typed payload● interface schema△ security declaration○ external△ error schema
OAuth, SPIFFE and policy● principals○ external△ purpose claims△ scopes● grants△ token record○ external
CloudEvents, Trace Context, PROV and OTel△ attributes△ observed changes△ source attributes△ operation spans△ policy attributes● correlation and provenance△ failure signals
Workflow and saga patterns△ execution identity● workflow state△ workflow inputs△ activities○ external△ history● retry and compensation
Figure 6. Protocol coverage matrix. Practitioner synthesis based on the cited specifications. Symbols show primary, partial or external coverage, not certification or security strength.

The right compositional rule is simple: adopt standards at the seams, own semantics at the centre. Use MCP for tool and resource interoperability, A2A for remote task exchange, OpenAPI for typed effectors, OAuth and SPIFFE for delegated and workload identity, and open telemetry formats for correlation. Bind them inside an enterprise execution envelope whose business meanings and versioning are controlled independently.

This also protects the architecture from standards evolution. For example, the current MCP revision is stateless, while its authorisation specification operates at the transport level and remains optional for implementations.12 A2A has reached a 1.0 specification while retaining a separate role from MCP.3 OpenTelemetry’s GenAI conventions are evolving and some pages have moved between repositories.13 A canonical envelope absorbs such changes through adapters instead of allowing them to redefine the institution’s evidence or authority model.

Part IV
The argument in one passChart · flow
flowchart TD
  A["The migration that passed<br/>every API test"] --> B["Draw the migration cut before<br/>choosing the fabric"]
  B --> C["Keep seven primitives outside<br/>the runtime"]
  C --> D["Use standards as bridges, not<br/>as the fabric"]
  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
A structural route through A Vendor-Neutral Enterprise Agent Fabric, from The migration that passed every API test to Source ledger. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Run the minimal case: one approved limit change

Take a synthetic account-servicing case. A reviewer has approved a temporary account limit of GBP 1,800. An agent’s task is to prepare and apply that already-approved change. The case is deliberately small because it exposes the boundary without hiding behind organisational complexity.

The requestor is `person:reviewer-17`. The proposing agent is `agent:limit-review-v3`. The executing workload has a SPIFFE-style workload identity. The authoritative case state is version 8 and status `approved`. The decisive policy claim has a digest, validity interval and permitted purpose. The action contract requires amount and currency, supports readback and defines a compensating action. The authority grant permits one `set_account_limit` effect on one account, below GBP 2,500, before expiry, with a human approval identifier.

The model can still make a mistake. It may propose GBP 6,000, refer to state version 7 or use a policy retrieved for analysis-only purposes. None of those failures requires another model to detect them. The deterministic evaluator rejects the envelope because the claim, state and grant do not line up. The model proposes meaning; the control plane proves that the proposal fits the current contract.

The accepted effect has a proposal phase, an authority phase and an observation phase

Figure 7. Minimal proposal-to-effect timeline. Synthetic worked example. The observed postcondition, rather than a successful transport response alone, closes the action.

Now add one failure: the tool accepts the request but its response times out. The workflow must not assume that the effect failed. It records `UNKNOWN_OUTCOME`, pauses, reads the account state and compares the observed limit with the intended postcondition. If the limit is already GBP 1,800, it closes the effect exactly once. If not, it may retry with the same idempotency key. If readback is unavailable, the case escalates rather than manufacturing certainty.

This minimal example reveals why recovery belongs in the portable envelope. A new runtime can generate a new proposal, but it must inherit the existing idempotency key, state version and unknown-outcome record. Otherwise migration itself can duplicate an effect.

Stress a production-shaped case across three runtimes

Worked scenario. The following case is synthetic. It combines common enterprise constraints to expose architecture behaviour; it is not presented as a client deployment or measured result.

A commercial customer requests a temporary covenant waiver. The case requires document extraction, financial calculation, policy interpretation, relationship context, human approval and a controlled update to the servicing system. The enterprise uses three workers: a local document agent for sensitive files, a managed reasoning runtime for policy synthesis, and a specialist remote agent for industry analysis. A durable workflow coordinates them.

Without a fabric, each runtime accumulates its own thread, document references, task identifiers and approval assumptions. The final coordinator may receive fluent summaries but cannot tell whether all agents reasoned over the same customer entity, the same quarter, the same policy version or the same decision purpose. A migration can preserve messages while losing those bindings.

With the fabric, the workflow issues a canonical case identifier and state version. The context compiler publishes claim bundles with provenance, entitlement and effective time. A2A may carry the industry-analysis task and artefact; MCP may expose the document and calculation tools. Each output is promoted into the case only after identity, provenance and schema checks. The managed runtime drafts a waiver proposal, but the action remains a typed proposal with no ambient permission.

A policy decision point evaluates the proposal against the case state, limits and required human role. The reviewer sees the decisive claims and unresolved contradictions rather than a hidden chain of thought. Approval produces a case-bound grant. An execution adapter updates the servicing system, reads back the resulting state and emits an effect receipt. The evidence service joins the request, claims, model and adapter versions, policy decision, approval and observed outcome.

Runtime diversity remains above one case, authority and evidence spine

Figure 8. Production-shaped multi-runtime case. Synthetic sequence. Runtime diversity is preserved, while identity, case state, evidence promotion, approval and effect closure remain on a common spine.

Suppose the managed reasoning provider must be removed during an in-flight case. The durable workflow still knows which artefacts were accepted, which proposal was pending, whether approval occurred and whether any external effect was attempted. A replacement runtime can receive the current case package and generate a new proposal. It does not need the previous provider’s private scratchpad because the institution never treated that scratchpad as the authoritative record.

Some semantic work remains. The replacement may interpret policy differently, require a different prompt or fail quality gates. Vendor neutrality does not promise equivalent cognition. It ensures that the replacement enters a governed case rather than reconstructing one from proprietary residue.

Part V

Build thin adapters and thick contracts

The reference architecture has four logical planes. The runtime plane contains models, planners, local graphs and remote agents. The control plane contains identity binding, authoritative state, the context compiler, action-contract registry, policy and authority services, and recovery coordination. The effect plane contains adapters and systems of record. The evidence plane records decisions, effects and recoveries with shared correlation.

“Thin adapter” does not mean trivial code. An adapter may handle streaming, provider events, task updates or tool dialects. It is thin in semantic authority: it may translate, validate syntax and expose capability, but it cannot redefine what `approved`, `current`, `entitled`, `executed` or `recovered` mean. Those meanings live in versioned contracts that every adapter must satisfy.

The execution envelope is the central object. It includes the identity chain, expected state version, decisive context claim identifiers, canonical action, authority grant, evidence identifiers and recovery contract. Runtime-native messages can be retained for diagnosis, but only an accepted envelope can cross into the effect plane. This makes conformance testing practical. The same positive, negative and unknown-outcome cases run against every adapter.

Capability negotiation prevents the fabric from becoming a lowest-common-denominator wrapper. Each runtime publishes supported capabilities, such as vision, computer use, structured decoding, remote-task streaming or local-data processing. The orchestrator may select a worker because of those differences. The control contract remains stable while the cognition profile varies. Neutrality protects substitution; it does not require sameness.

Make the envelope a release unit

A practical envelope needs more than a collection of identifiers. Each field must have an owner, a schema version and an invariant. The state version must identify the authoritative aggregate, not the adapter's cache. A context digest must name the exact representation used for the decision, while its source reference remains resolvable under the organisation's retention rules. An authority grant must declare its audience so that a token minted for one effector cannot be replayed against another. An action contract must identify the postcondition by which completion will be observed.

These meanings change over time, so the envelope and its validators form a release unit. A policy update, context-claim transformation, action-schema revision or adapter change can alter which proposals pass. The release manifest should therefore bind model and prompt versions to context-compiler, policy, contract and adapter versions, then run a shared regression suite before traffic moves. A rollback restores the whole compatible set. Rolling back only the model while retaining a newly interpreted action schema can create a configuration that never passed evaluation.

The conformance suite should include semantic mutations, not only happy-path examples. Swap the requestor while keeping the same token. Advance case state after proposal. Supersede one decisive claim. Change an action field from pounds to pence. Revoke approval after the proposal but before execution. Return a transport timeout after the effector commits. Then run the same cases through every runtime adapter. This reveals whether the adapter merely serialises fields or actually preserves the control contract.

Routing also belongs above the authority decision. A router may select a cheaper model, a local runtime for sensitive data or a specialist agent for a hard subtask. It may not silently broaden context entitlement, lower an approval threshold or substitute an effector with different postconditions. Those changes require a new envelope or policy decision. Cost and capability routing choose who may attempt the work; the authority kernel still decides what may cross into the world.

The control plane itself must remain replaceable. Its state should use documented schemas, its policies should be versioned outside application code, and its evidence should export in durable formats. Otherwise the organisation merely trades a model-provider lock-in for a platform-team lock-in. The migration cut must be drawable around each control service as well as around each agent runtime.

Apply the migration-cut scorecard

Use the following instrument before platform selection, at architecture review and before any major runtime migration. Score the seven primitives independently. A score of 2 requires both ownership and an executed conformance test. Documentation without a test is not enough. For consequential actions, any 0 is a blocking issue. A 1 needs a named remediation owner and a bounded period of manual operation.

Primitive0: runtime-bound1: exportable2: enterprise-owned and testedDecisive test
IdentityOne shared service accountActor fields can be exportedRequestor, proposer and executor are cryptographically or transactionally boundSubstitute the executor without losing principal lineage
StateThread is the caseSnapshot export requires interpretationVersioned authoritative state and transition rules exist outside runtimeReject a proposal formed against an old version
ContextPrompt text onlySources export without entitlement or timeClaims carry provenance, validity, purpose and supersessionWithdraw one source and find every affected decision
ActionNative tool call is the contractGeneric schema, weak effect semanticsCanonical typed action with preconditions, postconditions and idempotencyRun the same contract through two adapters
AuthorityAmbient token or prompt instructionScopes exist but are broad or manually mappedCase, purpose, resource, limit and expiry are enforced deterministicallyProve that a delegated specialist cannot widen its grant
EvidenceProvider logs onlyExported traces lack decision semanticsDecision, effect and recovery receipts join through stable identifiersReconstruct one decision without provider console access
RecoveryRetry on exceptionRunbook depends on provider stateUnknown outcome, readback, compensation and escalation are explicitInject a timeout after external acceptance

The serious negative control: do less

A public-data research assistant reads documents, drafts summaries and never changes durable state. Its conversations are short, its outputs are reviewed, and losing an in-flight thread has low consequence. Standardising on one integrated vendor may be the better decision. A separate authority kernel, event-sourced case store and recovery coordinator could cost more than the risk they reduce.

This negative control matters because the fabric should be consequence-shaped. Build the full migration cut where actions are long-lived, regulated, cross-domain or difficult to reverse. Use a thinner contract for advisory systems. Vendor neutrality is not a virtue in isolation; it is a control response to migration consequence, concentration risk and operational lifetime.

How the claim changes under pressureChart · state
stateDiagram-v2
  [*] --> Claim
  state "Thought experiment one, delete<br/>the runtime" as Claim
  state "Portability is a bottleneck<br/>property" as Test
  state "1. Identity binds three<br/>principals, not one service<br/>account" 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 one, delete the runtime and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Execute the contract validator

The accompanying Python artefact models one vendor-neutral execution envelope. It deliberately knows nothing about the proposing framework, model provider, cloud or agent SDK. The evaluator checks the seven portable primitives before an adapter can call the external tool.

The positive case has current state, entitled context, a scoped grant, human approval, an idempotency key and a readback path. The negative case varies three causal features: stale state, expired and wrong-purpose context, and an amount above the authority ceiling. The unknown-outcome case demonstrates that a timeout after acceptance produces `PAUSE`, not an automatic retry.

agent_fabric_contract_validator.py

Python 3.11 or later · standard library only · synthetic data · positive, negative and unknown-outcome cases
positive: ALLOW | all invariants satisfied
negative: DENY | case state is stale; proposal targets a stale state version; context claim policy-44 is expired; context claim policy-44 is not entitled for purpose; amount exceeds the authority ceiling
timeout after acceptance: PAUSE | read external state before any retry
Open the complete runnable source
"""Minimal, vendor-neutral contract validator for a consequential agent action.

The runtime may propose an action. This module checks the enterprise-owned
execution envelope before any adapter can call the external tool.

Python 3.11+, standard library only.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from decimal import Decimal, InvalidOperation
from enum import Enum
from typing import Any, Mapping, Sequence


class Decision(str, Enum):
    ALLOW = "ALLOW"
    DENY = "DENY"
    PAUSE = "PAUSE"


@dataclass(frozen=True)
class IdentityChain:
    requestor: str
    proposer_agent: str
    executor_workload: str


@dataclass(frozen=True)
class CaseState:
    case_id: str
    version: int
    status: str


@dataclass(frozen=True)
class ContextClaim:
    claim_id: str
    digest: str
    valid_until: datetime
    permitted_purposes: frozenset[str]


@dataclass(frozen=True)
class ToolContract:
    action_type: str
    version: str
    required_fields: frozenset[str]
    allowed_states: frozenset[str]
    reversible: bool
    supports_readback: bool


@dataclass(frozen=True)
class AuthorityGrant:
    requestor: str
    proposer_agent: str
    executor_workload: str
    case_id: str
    permitted_action: str
    permitted_resource: str
    amount_ceiling: Decimal
    expires_at: datetime
    human_approval_id: str | None = None


@dataclass(frozen=True)
class ActionProposal:
    action_type: str
    resource: str
    parameters: Mapping[str, Any]
    expected_state_version: int
    idempotency_key: str
    purpose: str


@dataclass(frozen=True)
class EvidenceEnvelope:
    trace_id: str
    source_claim_ids: tuple[str, ...]
    policy_version: str
    adapter_version: str


@dataclass(frozen=True)
class RecoveryContract:
    on_timeout: str
    compensation_action: str | None


@dataclass(frozen=True)
class ExecutionEnvelope:
    identity: IdentityChain
    state: CaseState
    context: tuple[ContextClaim, ...]
    tool: ToolContract
    authority: AuthorityGrant
    proposal: ActionProposal
    evidence: EvidenceEnvelope
    recovery: RecoveryContract


@dataclass
class Evaluation:
    decision: Decision
    reasons: list[str] = field(default_factory=list)

    def render(self, label: str) -> str:
        details = "; ".join(self.reasons) if self.reasons else "all invariants satisfied"
        return f"{label}: {self.decision.value} | {details}"


def utc_now() -> datetime:
    return datetime.now(timezone.utc)


def evaluate(envelope: ExecutionEnvelope, authoritative_state_version: int) -> Evaluation:
    """Evaluate the seven portable control primitives.

    This function deliberately knows nothing about the proposing framework,
    model provider, cloud or agent SDK.
    """
    failures: list[str] = []
    now = utc_now()

    # 1. Identity: bind requestor, proposer and executor explicitly.
    if not all(
        (
            envelope.identity.requestor,
            envelope.identity.proposer_agent,
            envelope.identity.executor_workload,
        )
    ):
        failures.append("identity chain is incomplete")
    if envelope.authority.requestor != envelope.identity.requestor:
        failures.append("authority is bound to a different requestor")
    if envelope.authority.proposer_agent != envelope.identity.proposer_agent:
        failures.append("authority is bound to a different proposing agent")
    if envelope.authority.executor_workload != envelope.identity.executor_workload:
        failures.append("authority is bound to a different executing workload")

    # 2. State: reject a proposal formed against a stale case version.
    if envelope.state.version != authoritative_state_version:
        failures.append("case state is stale")
    if envelope.proposal.expected_state_version != authoritative_state_version:
        failures.append("proposal targets a stale state version")
    if envelope.authority.case_id != envelope.state.case_id:
        failures.append("authority is bound to a different case")
    if envelope.state.status not in envelope.tool.allowed_states:
        failures.append("case status does not permit this action")

    # 3. Context: every decisive claim must be current and entitled for purpose.
    claim_ids = {claim.claim_id for claim in envelope.context}
    for claim in envelope.context:
        if not claim.digest:
            failures.append(f"context claim {claim.claim_id} has no digest")
        if claim.valid_until <= now:
            failures.append(f"context claim {claim.claim_id} is expired")
        if envelope.proposal.purpose not in claim.permitted_purposes:
            failures.append(f"context claim {claim.claim_id} is not entitled for purpose")
    if not set(envelope.evidence.source_claim_ids).issubset(claim_ids):
        failures.append("evidence refers to an undeclared context claim")

    # 4. Tool/action contract: validate semantics before adapter translation.
    if envelope.proposal.action_type != envelope.tool.action_type:
        failures.append("proposal does not match the selected tool contract")
    missing = envelope.tool.required_fields - envelope.proposal.parameters.keys()
    if missing:
        failures.append(f"tool parameters missing: {', '.join(sorted(missing))}")
    if not envelope.proposal.idempotency_key:
        failures.append("idempotency key is missing")

    # 5. Authority: grants may narrow, never widen, at a handoff.
    if envelope.authority.expires_at <= now:
        failures.append("authority grant is expired")
    if envelope.authority.permitted_action != envelope.proposal.action_type:
        failures.append("action is outside the authority grant")
    if envelope.proposal.resource != envelope.authority.permitted_resource:
        failures.append("resource is outside the authority grant")
    try:
        amount = Decimal(str(envelope.proposal.parameters.get("amount", "0")))
    except (InvalidOperation, TypeError, ValueError):
        failures.append("amount is not a valid decimal")
        amount = Decimal("0")
    if amount > envelope.authority.amount_ceiling:
        failures.append("amount exceeds the authority ceiling")
    if amount > 0 and envelope.authority.human_approval_id is None:
        failures.append("consequential action lacks human approval")

    # 6. Evidence: retain reconstructive identifiers, not hidden reasoning.
    if not all(
        (
            envelope.evidence.trace_id,
            envelope.evidence.policy_version,
            envelope.evidence.adapter_version,
        )
    ):
        failures.append("evidence envelope is incomplete")

    # 7. Recovery: unknown outcomes need readback or a declared compensation.
    if envelope.recovery.on_timeout == "readback" and not envelope.tool.supports_readback:
        failures.append("recovery asks for readback but the tool cannot observe outcome")
    if (
        not envelope.tool.reversible
        and envelope.recovery.compensation_action is not None
    ):
        failures.append("irreversible action declares a misleading compensation")

    return Evaluation(Decision.DENY if failures else Decision.ALLOW, failures)


def resolve_unknown_outcome(envelope: ExecutionEnvelope) -> Evaluation:
    """Select the safe next step after a timeout following tool acceptance."""
    if envelope.tool.supports_readback and envelope.recovery.on_timeout == "readback":
        return Evaluation(Decision.PAUSE, ["read external state before any retry"])
    if envelope.recovery.compensation_action:
        return Evaluation(Decision.PAUSE, ["reconcile, then consider compensation"])
    return Evaluation(Decision.DENY, ["outcome is unknown and no recovery path exists"])


def make_positive() -> ExecutionEnvelope:
    future = utc_now() + timedelta(days=30)
    return ExecutionEnvelope(
        identity=IdentityChain(
            requestor="person:reviewer-17",
            proposer_agent="agent:limit-review-v3",
            executor_workload="spiffe://example.org/tools/account-limits",
        ),
        state=CaseState(case_id="case-314", version=8, status="approved"),
        context=(
            ContextClaim(
                claim_id="policy-44",
                digest="sha256:9d3f...",
                valid_until=future,
                permitted_purposes=frozenset({"apply-approved-limit"}),
            ),
        ),
        tool=ToolContract(
            action_type="set_account_limit",
            version="2.1",
            required_fields=frozenset({"amount", "currency"}),
            allowed_states=frozenset({"approved"}),
            reversible=True,
            supports_readback=True,
        ),
        authority=AuthorityGrant(
            requestor="person:reviewer-17",
            proposer_agent="agent:limit-review-v3",
            executor_workload="spiffe://example.org/tools/account-limits",
            case_id="case-314",
            permitted_action="set_account_limit",
            permitted_resource="account:GB-42",
            amount_ceiling=Decimal("2500"),
            expires_at=future,
            human_approval_id="approval-991",
        ),
        proposal=ActionProposal(
            action_type="set_account_limit",
            resource="account:GB-42",
            parameters={"amount": "1800", "currency": "GBP"},
            expected_state_version=8,
            idempotency_key="case-314:set-limit:v8",
            purpose="apply-approved-limit",
        ),
        evidence=EvidenceEnvelope(
            trace_id="trace-7f1",
            source_claim_ids=("policy-44",),
            policy_version="limits-policy-12",
            adapter_version="core-adapter-5",
        ),
        recovery=RecoveryContract(
            on_timeout="readback",
            compensation_action="restore_previous_limit",
        ),
    )


def make_negative() -> ExecutionEnvelope:
    positive = make_positive()
    expired = utc_now() - timedelta(days=1)
    return ExecutionEnvelope(
        identity=positive.identity,
        state=CaseState(case_id="case-314", version=7, status="approved"),
        context=(
            ContextClaim(
                claim_id="policy-44",
                digest="sha256:9d3f...",
                valid_until=expired,
                permitted_purposes=frozenset({"analyse-only"}),
            ),
        ),
        tool=positive.tool,
        authority=positive.authority,
        proposal=ActionProposal(
            action_type="set_account_limit",
            resource="account:GB-42",
            parameters={"amount": "6000", "currency": "GBP"},
            expected_state_version=7,
            idempotency_key="case-314:set-limit:v7",
            purpose="apply-approved-limit",
        ),
        evidence=positive.evidence,
        recovery=positive.recovery,
    )


def main() -> None:
    positive = make_positive()
    negative = make_negative()
    print(evaluate(positive, authoritative_state_version=8).render("positive"))
    print(evaluate(negative, authoritative_state_version=8).render("negative"))
    print(resolve_unknown_outcome(positive).render("timeout after acceptance"))


if __name__ == "__main__":
    main()

The validator is intentionally incomplete as an enterprise product. It omits cryptographic verification, persistence, concurrent transactions and external policy integration. Its purpose is experimental: change providers or wrap the evaluator around another runtime, and the acceptance decision remains unchanged because the control semantics are not encoded in the worker.

Respect what the fabric cannot make portable

The architecture has a hard boundary. It cannot guarantee that two models interpret evidence alike, that two planners find equivalent routes, or that a provider-specific computer-use capability has a substitute. It cannot preserve hidden reasoning that a provider does not expose, and it should not treat such reasoning as decision evidence. It cannot eliminate data-egress, sovereignty, latency or cost differences.

It also cannot convert an irreversible action into a reversible one. Recovery contracts can prevent duplicate retries, observe outcomes and coordinate remediation. They cannot unsend a disclosure, remove knowledge already received or restore a market condition. The decision to permit such an effect must therefore use stronger approval, simulation and precondition checks before execution.

Recovery depends on both outcome knowledge and real reversibility

Figure 9. The recovery surface. Conceptual decision matrix. “Reversible” means a real, tested business or technical reversal, not merely another command with the opposite name.

Centralisation creates its own risks. A common control plane can become a high-value attack surface, a latency bottleneck or an organisational gatekeeper. Its policy changes require separation of duties, canarying and rollback. Its evidence store requires minimisation and privacy controls. Its availability design must reflect the consequence of failing closed or failing open. Zero trust guidance and AI risk frameworks help organise these controls, but neither certifies a particular design.919

Finally, portability can become theatre. A team may define a canonical schema yet leave every field as free text, or retain receipts that cannot be joined to observed effects. The migration-cut tests should therefore be run through fault injection: stale state, revoked grants, superseded context, adapter mismatch, timeout after acceptance and provider removal. A fabric is portable only to the extent that these failures are survived in execution, not described in a diagram.

Compact glossary

Execution envelope
The versioned, enterprise-owned record that binds identity, state, context, action, authority, evidence and recovery for one proposed effect.
Migration cut
The boundary across which a runtime can be replaced without losing the meanings needed to authorise, observe or recover work.
Decision receipt
A reconstructive record of the request, state, decisive claims, policy, proposal, grant and disposition.
Effect receipt
A record of adapter acceptance, idempotency, external reference and observed postcondition.
Authority kernel
A deterministic boundary that evaluates grants and mints the narrow capability needed for an accepted action.
Unknown outcome
A state in which transport completion is uncertain and the external effect must be observed before retry or compensation.
Conclusion

Own the invariant, rent the worker

An enterprise agent fabric should not promise that every model, framework or cloud behaves the same. That promise would either be false or would discard the capabilities that make different runtimes useful. The stronger promise is narrower: no runtime may become the sole custodian of the institution’s identity, state, context, action semantics, authority, evidence or recovery.

This changes the architecture decision. Begin with the migration cut and the execution envelope, not with a universal agent SDK. Adopt MCP, A2A, OpenAPI, OAuth, SPIFFE, event and workflow standards at their proper seams. Test every adapter with the same stale-state, revoked-authority and unknown-outcome cases. Permit runtime-specific intelligence above the cut, while keeping consequential effects below a deterministic authority boundary.

The first implementation need not be a grand platform. Choose one consequential workflow, externalise its seven primitives, run the validator against two runtime adapters, and inject a timeout after the effector accepts the command. If the case remains intelligible, authorised and recoverable without either runtime’s console, the fabric has earned its first defensible claim to neutrality.