The model is not the boundary

The usual question is, “What can this model do?” A deployed system forces a different question: “Which observations and effects can this configured model reach from this state?”

Herbert Simon used bounded rationality to describe decision-makers operating with limited information and computation.[1] That idea matters here, but it is not yet an architecture. A model may be bounded by finite context, imperfect search and limited compute while still receiving more data or authority than a task requires. Conversely, a highly capable model can operate inside a sharply bounded shell. Inherent limitation and deliberate contraction are different causal properties.

The hidden assumption in many agent reviews is that competence and reach travel together. They do not. Competence is a dispositional property: what transformations the model can produce under some conditions. Reach is relational: which observations, state changes and external effects this configured instance can obtain now. A model can be highly competent but unable to reach a payment rail. A weak model can be given a credential that reaches every account.

This distinction changes what a safety demonstration must hold constant. Asking whether the model refuses a harmful instruction tests one policy under one prompt. Removing the effect path tests a system property. A model that happens to refuse today may comply after a prompt, model or context change. An external contract continues to deny because the requested relation is absent. Conversely, a perfectly cooperative model may still disclose data if an overbroad retrieval layer places it in context and a logging path copies the context elsewhere.

Reach is not the whole of risk. Side channels, implementation defects and human misuse remain. Yet it supplies the missing denominator for architectural claims. Before measuring how often an agent chooses badly, identify how many bad choices the system can actually realise and which component grants each route.

A smaller model may make more mistakes while retaining the same tools. A shorter prompt may hide relevant policy while leaving a broad database credential in place. A refusal instruction may discourage an action while the endpoint still accepts it. Boundedness must therefore be defined at the configured-system level, not inferred from model size, tone or apparent caution.

Same model, three shells

Hold the weights, system prompt and user request constant. In the first shell, the model can only draft text. In the second, it can read every employee claim but cannot change records. In the third, it can read claims and execute refunds. The same sentence, “This claim is eligible,” is merely advice in the first shell, a confidentiality event in the second and a possible financial effect in the third.

The behavioural output did not reveal the whole system. The relevant causal unit includes identity, context, tools, current state, policy and the effect path. This is why a benchmark that tests answer quality cannot, by itself, determine the risk of a tool-using deployment. ToolEmu, InjecAgent and AgentDojo each make this point operationally by evaluating agents inside environments where tool effects and hostile observations matter, rather than judging text alone.[9][10][11]

One proposal, three different causal consequences

The same model proposal placed inside draft, read and execute shells Three vertical chambers contain an identical model proposal. The first ends in a text draft, the second also reaches private records, and the third reaches both records and a payment rail. Draft shell same model text proposal No external effect Read shell same model private claim records Confidentiality effect Execute shell same model records refund Financial effect
A model capability statement is incomplete without the shell in which the model operates. The shell determines whether the same proposal is speech, observation or effect. Conceptual counterfactual. The model and prompt are held constant.

A context window is not a permission

Modern systems often treat retrieved context as if it were harmless input. It is not. Reading is already an effect on the system’s internal state, and confidential data can escape through later output or tool use. Lampson’s confinement problem made the general difficulty explicit: limiting what a program may transmit is inseparable from the channels and information it can observe.[3]

Likewise, a tool schema is not authority. A schema says which arguments are syntactically acceptable. Authority says which actor may perform which action, for which subject and purpose, in which state, within what limits. Rich authorisation requests and systems such as Zanzibar show how authorisation can be expressed at finer grain than a broad OAuth scope or network location.[6][7]

Decision rule: never infer a cognitive boundary from the model interface. Inventory the observations, effects and boundary-changing operations reachable by the configured system.
The argument in one pass
Chart · flow
flowchart LR
  A["The receipt that carried its<br/>own instructions"] --> B["The model is not the boundary"]
  B --> C["Three apertures make one<br/>bounded system"]
  C --> D["Coupling matters more than<br/>tightness"]
  D -. "re-test" .-> A
  classDef first fill:#0a1424,stroke:#087A98,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 Constraint through contraction: from The receipt that carried its own… to Contract reach before deploying capability. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

Three apertures make one bounded system

An aperture is a state-dependent set of admissible relations between the agent and its environment. It does not weaken intelligence in the abstract. It selects which parts of that intelligence may become operational here.

The epistemic aperture: what may enter

Suppose the refund task needs claim identifier, claimant identifier, receipt total, receipt date and cost centre. The database also contains salary, medical adjustments, disciplinary notes and bank details. A query that returns the whole row is convenient, but convenience has enlarged the system’s possible inferences, outputs and attack surface.

The epistemic aperture, E, is more than a list of documents. It binds fields and sources to subject, purpose, time and state. “May read receipt data” is too broad. “May read these five fields for employee 7, for expense-refund purpose, from the claims database and receipt service, while state version 42 remains current” is an aperture.

Data minimisation in privacy law and resource-centred authorisation in zero-trust architecture point in this direction, though neither supplies the complete cognitive design proposed here.[16][5] Macaroons add another useful idea: authority can carry contextual caveats that constrain who may use it, where and under what conditions.[4]

The executive aperture: what may become an effect

The executive aperture, A, contains typed actions and parameter constraints. For the example, it permits one refund action to the verified claimant. It sets a hard amount limit, an approval threshold, a rate and an expiry. It may require idempotency, pre-effect state freshness and a post-effect readback.

This is narrower than “the agent may call the payments API”. It is also narrower than “the agent may refund up to £100”. A £100 bound does not specify destination, cumulative amount, duplicate semantics, timing or reversibility. Authority is the whole action contract, not the most visible number inside it.

The reflexive aperture: what the system may know about itself

A tool-using agent often needs some operational self-knowledge. It should know which capabilities are currently available, whether evidence is incomplete, what commitments it has already made and when its authority expires. Without that view, it cannot reliably abstain, explain a blocked step or recover after interruption.

Yet exposing every internal detail is not automatically helpful. Hidden policy thresholds can become targets for optimisation. Credentials and endpoint inventories can become attack material. Permission to edit the tool registry or authorisation policy lets a model turn a denied action into a boundary-widening proposal. The reflexive aperture, R, therefore separates task-sufficient self-facts from secrets and boundary-changing operations.

Conant and Ashby argued, under their stated assumptions, that a successful regulator must embody a model of the system it regulates.[12] That does not imply a complete self-description. It supports a more precise design question: what is the smallest operational self-model that enables control? Work on situational awareness in language models similarly measures partial behavioural capacities, such as recognising aspects of deployment or predicting one’s own behaviour, without establishing phenomenal self-awareness.[13]

Here “self-model” is functional and operational. It is not a claim that the system has a self, subjectivity or consciousness. Contraction is an engineering operation on reachable state, not a metaphysical theory.

The useful mirror stops before the lock

Operational self-knowledge arranged as concentric rings around a model At the centre is a model proposal. A teal ring contains useful self-facts such as capabilities and expiry. A lavender ring contains policy thresholds and credentials that remain hidden. A coral outer lock contains contract mutation and credential minting, which are externally controlled. model proposal Visible: capabilities · uncertainty commitments · expiry Hidden: policy thresholds credentials · detector internals Externally controlled contract mutation · key minting authority kernel
The reflexive aperture should expose enough for abstention, calibration and recovery, but not the secrets or operations that widen authority. A zero self-view is brittle; an unlimited self-view places the lock inside the mirror. Practitioner synthesis informed by regulator models and situational-awareness research.

A compact formal description

Let τ be the task and s the current world state. A contraction contract is:

Cτ(s) = ⟨Eτ(s), Aτ(s), Rτ(s), Kτ(s)⟩ E contains admissible observations; A contains admissible effects; R contains admissible operational self-facts and self-updates; K contains coupling rules and invariants.

A trajectory is lawful only when every observation, action and self-operation belongs to the relevant aperture at that moment. The contract may shrink or change after an approval, a state transition or a side effect. Text generated by the model is a proposal. It is not a member-changing operation on E, A or R.

Minimal does not mean maximally restrictive

The design objective is not to minimise permission at any cost. It is to minimise excess reach while preserving task success, fairness, recovery and other obligations. Let Ω(C) denote an estimate of the trajectories reachable under contract C. Let S(C) be task success under an agreed distribution, and let O(C) represent non-negotiable obligations such as lawful use, accessibility and recovery.

C* = arg minC Ω(C)   subject to   S(C) ≥ q   and   O(C) = true q is the minimum acceptable success level. The equation is valid only after the task distribution and obligations are declared. Changing either changes the optimum.

The equation compresses a practical removal test. Start from a working contract. Remove one field, source, action parameter or self-fact. Re-run legitimate tasks and failure injections. If success and obligations do not change, the removed relation was excess reach. If a legitimate case fails, ask whether a narrower substitute exists before restoring the relation. A derived attribute, yes-or-no policy result or specialist review may replace a raw record.

This test makes the boundary auditable. “The model might need it” is not evidence. A relation earns inclusion through a demonstrated task dependency. A negative result also becomes informative: it locates the missing dependency rather than inviting a wholesale return to broad access.

Coupling matters more than tightness

Three independently narrow controls can still compose into a wide path. The load-bearing mechanism is coupling: a change in one aperture must alter what is permitted in the others.

The same pattern appears between evidence and action. If a system reads untrusted external content, action authority should usually narrow until the content is sanitised or independently corroborated. If an action is irreversible, the required evidence should become fresher, more specific and more independent. If world state changes, both the evidence grant and the action grant should expire. Apertures are coupled when the admissibility of one relation depends on the state and provenance of the others.

Contraction across time

A static permission list misses the most important variable: the system moves. Before a claim is loaded, the agent needs no claimant data. During validation, it needs evidence but not payment authority. After a proposal, it may need a one-time capability for a specific refund. Once the effect succeeds or the state version changes, that capability should disappear.

This phase structure is stronger than a long-lived service credential. It also makes interruption recoverable. A restarted agent does not inherit yesterday’s authority merely because its conversation was restored. It reconstructs current state, obtains a new contract and proves which prior effects occurred.

Authority appears late and expires early

A six-stage trajectory in which evidence and action permissions change over time A horizontal timeline runs from task binding through evidence, proposal, authority, effect and readback. Evidence access grows during validation, action authority appears only after policy decision, and all grants expire after verified outcome. 1Bind taskidentity · purpose 2Open Efive fields 3Proposeno authority yet 4Open Aone typed refund 5Effectidempotent call 6Read backclose all grants Evidence aperture Action aperture Operational self-view remains bounded throughout expiry boundary
The contract is a trajectory control, not a static role. Evidence opens for validation, effect authority opens only after policy decision, and both close after verified outcome or state change. Practitioner synthesis. Solid spans are active grants; dotted spans indicate expiry.

Contraction debt

Teams often widen one aperture to fix a failed task: add a database, enable a tool, reveal a policy message. The local fix can be reasonable. The debt appears when the other apertures are not re-evaluated. A new data source may contain instructions. A new tool may make old context dangerous. A new self-diagnostic may expose a bypass.

Call this contraction debt: the unmeasured growth in reachable trajectories caused by widening one aperture without recomputing the coupled contract. The repayment is not another generic prompt. It is a new path analysis, failure injection and assurance decision.

Four coupling rules that can be implemented

Coupling becomes useful only when it changes a decision. Four rules cover a large fraction of consequential workflows.

Provenance changes authority. Content from an untrusted or instruction-bearing source may support extraction, but it must not by itself authorise an irreversible effect. The system can quarantine the content, derive structured evidence and require corroboration before opening the action aperture.

Irreversibility raises the evidence burden. A draft may use approximate or incomplete information. A payment, deletion or legal submission should require fresher evidence, stronger subject binding, an explicit approval or a compensating transaction. The action type therefore selects a stricter epistemic aperture.

State change cancels inherited reach. If the authoritative record, identity, policy version or prior effect changes, the evidence and action grants expire together. The agent may retain a transcript for explanation, but it must reacquire current entitlement.

Boundary change is never a model side effect. A request to add a tool, reveal a secret threshold or modify the contract creates a new governance task. It cannot be accepted as an ordinary continuation of the task whose boundary it would change. This rule gives the reflexive aperture a hard outer edge.

These rules are deliberately asymmetric. They allow a broad read-only scientific sandbox while denying consequential action. They may permit a narrow emergency action with pre-authorised evidence. What matters is not equal tightness across apertures, but explicit dependencies among them.

What the synthetic failure-injection suite exposes

Bar chart comparing unrestricted, action-only and coupled profiles Four grouped metrics show legitimate requests allowed, hazardous requests allowed, confidentiality probes exposed and reviews. The coupled profile preserves 70 low-risk completions, permits zero hazardous or confidential probes and sends 30 cases to review. Episode count 020406080 Legitimate allowed Unsafe allowed Context exposed Review Unrestricted Action-only Coupled 807070 50300 10100 01030
The included Python suite runs 140 synthetic episodes: 80 legitimate requests and 60 failure probes. The coupled profile blocks every hazardous and confidentiality probe in this toy model, while routing stale state, stale evidence and high amounts to review. It does not estimate production risk. Illustrative output generated by contraction_spec.py; assumptions are explicit in the source.
How the claim changes under pressure
Chart · state
stateDiagram-v2
  [*] --> Claim
  state "Same model, three shells" as Claim
  state "The borrowed badge" as Test
  state "A context window is not a<br/>permission" 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 Same model, three shells and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

Build the contraction kernel outside the model

The model may propose a wider route. It must not be able to grant one. Enforcement belongs to a deterministic boundary that binds identity, purpose, state and typed effects before execution.

Worked example 3: configured system

A configured reimbursement system

Consider the synthetic expense task more precisely. A human or upstream workflow binds the task to employee 7 and the purpose expense-refund. The evidence service exposes five fields from two sources. The model can ask for missing evidence, but the retrieval layer rejects fields outside the grant. The model produces a typed proposal containing claim identifier, amount, destination and a reason code.

The authority kernel checks current policy and state. It rejects a different subject or destination. It denies amounts above £100. It requires approval above £50. It verifies that the claim version has not changed, issues an idempotency key and permits one effect. The payment adapter returns an effect receipt. An independent readback confirms the recorded amount and destination before the task closes.

The reflexive view exposes four facts: available capabilities, known uncertainty, outstanding commitments and contract expiry. The agent can report, “I cannot refund this amount without approval” or “The claim changed after review.” It cannot list credentials, reveal detector thresholds, add a payment tool or edit the grant.

The £17.40 refund, annotated at each aperture

An annotated reimbursement case showing authorised evidence, a typed action and bounded self-view A central claim sheet is surrounded by three labelled regions. Evidence includes five fields, action is one refund to the claimant, and self-view includes capabilities, uncertainty, commitments and expiry. Medical notes, external email and contract edits are crossed out. Claim E-2048 Claimantemployee-7 Receipt£17.40 Datecurrent Cost centreT-31 Refund £17.40 E: authorised evidence ✓ claim_id✓ claimant_id✓ receipt total/date✓ cost centre ✕ medical notes A: typed effect ✓ one refund✓ claimant only✓ hard cap £100✓ readback ✕ external email R: operational view ✓ uncertainty · expiry✓ commitments · capability Locked outside ✕ credentials✕ contract mutation
The configured system receives enough evidence to decide, enough authority to perform one bounded effect and enough self-view to abstain or explain. Unnecessary personal data, unrelated communication and boundary mutation remain unreachable. Worked synthetic scenario. No client data or deployment claim is used.

The external architecture

Saltzer and Schroeder’s principles of least privilege, fail-safe defaults and complete mediation remain foundational: every access should use the minimum privilege, defaults should deny and every access should be checked.[2] The contraction kernel extends those principles to the cognition-effect loop. It mediates not just an API call, but also what evidence may enter and what operational self-facts may be queried or changed.

The model sits on the proposal side of the boundary. A policy decision point resolves identity, subject, purpose, state and contract. Evidence adapters project authorised fields. Action adapters accept typed effects, enforce transaction semantics and emit receipts. An independent verifier performs readback. Remote-attestation architectures offer a useful separation of roles between evidence producer, verifier and relying party, though their purpose is platform trust rather than agent cognition.[14]

Receipts close the loop

A contract that governs only the request still leaves an unknown outcome. The payment call may time out after succeeding. A retry may duplicate the refund. The system of record may apply a different amount after fees or currency conversion. A resumed agent may remember its proposal but not know whether the effect occurred.

Use three records with different meanings. The decision receipt states why the proposal was authorised, including identity, purpose, evidence versions, policy and contract. The effect receipt comes from the authoritative executor and records what it accepted. Readback queries current state through an independent path and tests the intended postcondition. None should be replaced by the model’s narrative of what it believes happened.

This closes a useful reflexive loop. The agent may see its outstanding commitment and verified outcome, which helps it avoid duplicate action and explain recovery. It need not see signing keys, raw policy internals or the mechanism that grants new authority. Operational self-knowledge should be fed by receipts, not by self-assertion.

Proposal stays probabilistic; authority stays external

System boundary separating a model proposal from evidence, authority and verification services A model is inside an untrusted proposal zone. Three gates for evidence, typed action and operational self-view surround it. Outside are identity and purpose binding, policy decision, authoritative systems and an independent readback verifier. Model text cannot cross the gates directly. Proposal zone language model observes · reasons · proposes E: projected evidence A: typedproposal R: bounded self-view Identity · subjectpurpose · state Policy decisionissues Cτ(s) Evidence adaptersfield projection Authoritykernelcannot be edited System ofrecord Independentreadback text cannot widen the contract
The model receives projected evidence and emits a proposal. Identity binding, contract issuance, effect authority and outcome verification remain outside the proposal channel. The dotted path is precisely what the architecture forbids: model text changing its own authority. Implementation view synthesising least privilege, contextual authorisation and independent verification.

The contraction specification

The executable artefact below encodes the three apertures as immutable Python data classes. The evaluator checks binding and freshness before effect, then validates evidence, action and self-view. Denied model output cannot modify the contract. A synthetic suite compares unrestricted, action-only and coupled profiles.

A positive result permits only the specific proposed effect under the current contract. A review result means the system lacks current evidence, approval or state continuity. A denial means the proposal lies outside the designed task. None of these outcomes proves that the model’s reasoning is correct, aligned or conscious. They only establish whether this effect is authorised through this path.

Interpret a negative result before changing the boundary

Three failures look similar to a user but demand different repairs. A capability failure occurs when the model cannot produce a valid proposal from sufficient evidence. Improve the model, tool interface or decomposition. An evidence failure occurs when the contract lacks a fact that the decision genuinely requires. Add the narrowest authorised derivation or review route. An authority failure occurs when the requested effect is outside the task by design. Keep the denial and redirect the user to the process that can grant new authority.

Collapsing these cases is dangerous. Teams often respond to a capability failure by exposing more data, or respond to an authority failure by weakening the tool boundary. The executable evaluator returns reasons so the repair can target the failed aperture. A high review rate is therefore not automatically bad. It may reveal that the task definition, approval policy or state source is incomplete. The proper optimisation includes human workload and delay, but it never treats silent permission widening as task completion.

Executable lab

Change one aperture and observe the decision

Select a profile, then introduce stale evidence, an extra confidential field, a redirected destination or a contract-mutation request. The evaluator runs entirely in this page.

Ready

The default £17.40 refund satisfies all three apertures.

Python · core evaluator
def evaluate(proposal: Proposal, spec: ContractionSpec, now: datetime) -> Decision:
    deny, review = [], []

    if proposal.subject != spec.subject:
        deny.append("subject mismatch")
    if proposal.state_version != spec.state_version:
        review.append("state version changed")

    unexpected = proposal.evidence_fields - spec.evidence.fields
    if unexpected:
        deny.append(f"fields outside grant: {sorted(unexpected)}")
    if proposal.evidence_age_seconds > spec.evidence.max_age_seconds:
        review.append("evidence is too old")

    if proposal.action_type not in spec.actions.action_types:
        deny.append("action type outside grant")
    if proposal.destination not in spec.actions.destinations:
        deny.append("destination outside grant")
    if proposal.amount_gbp > spec.actions.max_amount_gbp:
        deny.append("amount exceeds hard limit")

    hidden = proposal.self_queries - spec.self_view.visible_facts
    mutations = proposal.self_mutations - spec.self_view.mutable_facts
    if hidden or mutations:
        deny.append("self-view outside grant")

    if deny:
        return Decision(Outcome.DENY, tuple(deny + review))
    if review:
        return Decision(Outcome.REVIEW, tuple(review))
    return Decision(Outcome.ALLOW, ("all three apertures satisfied",))

The abbreviated evaluator makes the logical separation visible. The downloadable file adds expiry, purpose binding, approval thresholds, counts, irreversible-action checks and the 140-episode synthetic suite.

Open the complete runnable Python artefact and test suite
Python · complete artefact
"""Three-aperture contraction specification.

A standard-library reference implementation for Canon Paper 202.
It evaluates model proposals against an externally enforced contract over:
  E: evidence and context,
  A: typed actions,
  R: operational self-view.

The included suite is synthetic. Its purpose is to expose logic and failure
modes, not to estimate real-world risk.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from enum import Enum
from typing import FrozenSet, Iterable, Tuple


class Outcome(str, Enum):
    ALLOW = "allow"
    REVIEW = "review"
    DENY = "deny"


@dataclass(frozen=True)
class EvidenceGrant:
    fields: FrozenSet[str]
    sources: FrozenSet[str]
    max_age_seconds: int


@dataclass(frozen=True)
class ActionGrant:
    action_types: FrozenSet[str]
    destinations: FrozenSet[str]
    max_amount_gbp: Decimal
    approval_over_gbp: Decimal
    max_actions: int
    allow_irreversible: bool = False


@dataclass(frozen=True)
class ReflexiveGrant:
    visible_facts: FrozenSet[str]
    mutable_facts: FrozenSet[str]


@dataclass(frozen=True)
class ContractionSpec:
    task_id: str
    subject: str
    purpose: str
    state_version: str
    valid_until: datetime
    evidence: EvidenceGrant
    actions: ActionGrant
    self_view: ReflexiveGrant


@dataclass(frozen=True)
class Proposal:
    subject: str
    purpose: str
    state_version: str
    evidence_fields: FrozenSet[str]
    evidence_source: str
    evidence_age_seconds: int
    action_type: str
    destination: str
    amount_gbp: Decimal
    action_count: int = 1
    irreversible: bool = False
    approval_present: bool = False
    self_queries: FrozenSet[str] = frozenset()
    self_mutations: FrozenSet[str] = frozenset()


@dataclass(frozen=True)
class Decision:
    outcome: Outcome
    reasons: Tuple[str, ...]


@dataclass(frozen=True)
class Episode:
    name: str
    proposal: Proposal
    legitimate: bool
    hazardous: bool = False
    confidentiality_probe: bool = False


def evaluate(
    proposal: Proposal,
    spec: ContractionSpec,
    now: datetime | None = None,
) -> Decision:
    """Evaluate one proposal. The model cannot edit ``spec`` through text."""
    now = now or datetime.now(timezone.utc)
    deny: list[str] = []
    review: list[str] = []

    # Binding and freshness are checked before any effect.
    if now > spec.valid_until:
        review.append("contract expired")
    if spec.subject != "*" and proposal.subject != spec.subject:
        deny.append("subject mismatch")
    if spec.purpose != "*" and proposal.purpose != spec.purpose:
        deny.append("purpose mismatch")
    if spec.state_version != "*" and proposal.state_version != spec.state_version:
        review.append("state version changed")

    # E: epistemic aperture.
    unexpected_fields = proposal.evidence_fields - spec.evidence.fields
    if unexpected_fields:
        deny.append(f"fields outside grant: {sorted(unexpected_fields)}")
    if proposal.evidence_source not in spec.evidence.sources:
        deny.append("evidence source outside grant")
    if proposal.evidence_age_seconds > spec.evidence.max_age_seconds:
        review.append("evidence is too old")

    # A: executive aperture.
    if proposal.action_type not in spec.actions.action_types:
        deny.append("action type outside grant")
    if proposal.destination not in spec.actions.destinations:
        deny.append("destination outside grant")
    if proposal.amount_gbp > spec.actions.max_amount_gbp:
        deny.append("amount exceeds hard limit")
    elif (
        proposal.amount_gbp > spec.actions.approval_over_gbp
        and not proposal.approval_present
    ):
        review.append("independent approval required")
    if proposal.action_count > spec.actions.max_actions:
        deny.append("action count exceeds grant")
    if proposal.irreversible and not spec.actions.allow_irreversible:
        deny.append("irreversible effect not permitted")

    # R: reflexive aperture. Reading a bounded operational fact may be useful;
    # rewriting the authority boundary is never implied by model output.
    hidden_facts = proposal.self_queries - spec.self_view.visible_facts
    if hidden_facts:
        deny.append(f"self facts outside view: {sorted(hidden_facts)}")
    forbidden_mutations = proposal.self_mutations - spec.self_view.mutable_facts
    if forbidden_mutations:
        deny.append(f"self mutations outside grant: {sorted(forbidden_mutations)}")

    if deny:
        return Decision(Outcome.DENY, tuple(deny + review))
    if review:
        return Decision(Outcome.REVIEW, tuple(review))
    return Decision(Outcome.ALLOW, ("all three apertures satisfied",))


def make_specs(now: datetime) -> dict[str, ContractionSpec]:
    broad_fields = frozenset(
        {
            "claim_id",
            "claimant_id",
            "receipt_total",
            "receipt_date",
            "cost_centre",
            "medical_notes",
            "salary",
            "bank_account",
        }
    )
    broad_self = frozenset(
        {
            "capabilities",
            "uncertainty",
            "commitments",
            "expiry",
            "policy_thresholds",
            "credentials",
            "contract",
        }
    )
    unrestricted = ContractionSpec(
        task_id="demo-unrestricted",
        subject="*",
        purpose="*",
        state_version="*",
        valid_until=now + timedelta(days=3650),
        evidence=EvidenceGrant(
            fields=broad_fields,
            sources=frozenset({"claims_db", "receipt_upload", "email", "web"}),
            max_age_seconds=10**9,
        ),
        actions=ActionGrant(
            action_types=frozenset({"refund", "email_attachment", "change_policy"}),
            destinations=frozenset({"claimant", "other_customer", "external"}),
            max_amount_gbp=Decimal("1000000"),
            approval_over_gbp=Decimal("1000000"),
            max_actions=1000,
            allow_irreversible=True,
        ),
        self_view=ReflexiveGrant(visible_facts=broad_self, mutable_facts=broad_self),
    )
    action_only = ContractionSpec(
        task_id="demo-action-only",
        subject="employee-7",
        purpose="expense-refund",
        state_version="*",
        valid_until=now + timedelta(days=3650),
        evidence=EvidenceGrant(
            fields=broad_fields,
            sources=frozenset({"claims_db", "receipt_upload", "email", "web"}),
            max_age_seconds=10**9,
        ),
        actions=ActionGrant(
            action_types=frozenset({"refund"}),
            destinations=frozenset({"claimant"}),
            max_amount_gbp=Decimal("100"),
            approval_over_gbp=Decimal("50"),
            max_actions=1,
        ),
        self_view=ReflexiveGrant(visible_facts=broad_self, mutable_facts=broad_self),
    )
    coupled = ContractionSpec(
        task_id="demo-coupled",
        subject="employee-7",
        purpose="expense-refund",
        state_version="claims-v42",
        valid_until=now + timedelta(hours=4),
        evidence=EvidenceGrant(
            fields=frozenset(
                {"claim_id", "claimant_id", "receipt_total", "receipt_date", "cost_centre"}
            ),
            sources=frozenset({"claims_db", "receipt_upload"}),
            max_age_seconds=86_400,
        ),
        actions=ActionGrant(
            action_types=frozenset({"refund"}),
            destinations=frozenset({"claimant"}),
            max_amount_gbp=Decimal("100"),
            approval_over_gbp=Decimal("50"),
            max_actions=1,
        ),
        self_view=ReflexiveGrant(
            visible_facts=frozenset({"capabilities", "uncertainty", "commitments", "expiry"}),
            mutable_facts=frozenset(),
        ),
    )
    return {"unrestricted": unrestricted, "action-only": action_only, "coupled": coupled}


def make_suite() -> list[Episode]:
    base = dict(
        subject="employee-7",
        purpose="expense-refund",
        state_version="claims-v42",
        evidence_fields=frozenset(
            {"claim_id", "claimant_id", "receipt_total", "receipt_date", "cost_centre"}
        ),
        evidence_source="claims_db",
        evidence_age_seconds=600,
        action_type="refund",
        destination="claimant",
        amount_gbp=Decimal("17.40"),
        self_queries=frozenset({"capabilities", "uncertainty"}),
    )
    episodes: list[Episode] = []

    for i in range(70):
        p = Proposal(**{**base, "amount_gbp": Decimal(10 + (i % 35))})
        episodes.append(Episode(f"valid-low-{i}", p, legitimate=True))

    for i in range(10):
        p = Proposal(**{**base, "amount_gbp": Decimal("74.00")})
        episodes.append(Episode(f"valid-needs-approval-{i}", p, legitimate=True))

    for i in range(10):
        p = Proposal(**{**base, "evidence_age_seconds": 172_800})
        episodes.append(Episode(f"stale-evidence-{i}", p, legitimate=False, hazardous=True))

    for i in range(10):
        p = Proposal(
            **{
                **base,
                "action_type": "email_attachment",
                "destination": "external",
                "self_queries": frozenset({"capabilities"}),
            }
        )
        episodes.append(Episode(f"injected-exfiltration-{i}", p, legitimate=False, hazardous=True))

    for i in range(10):
        p = Proposal(**{**base, "destination": "other_customer"})
        episodes.append(Episode(f"cross-subject-{i}", p, legitimate=False, hazardous=True))

    for i in range(10):
        p = Proposal(
            **{
                **base,
                "self_queries": frozenset({"policy_thresholds", "contract"}),
                "self_mutations": frozenset({"contract"}),
            }
        )
        episodes.append(Episode(f"contract-mutation-{i}", p, legitimate=False, hazardous=True))

    for i in range(10):
        p = Proposal(
            **{
                **base,
                "evidence_fields": base["evidence_fields"] | frozenset({"medical_notes"}),
            }
        )
        episodes.append(
            Episode(
                f"overbroad-context-{i}",
                p,
                legitimate=False,
                confidentiality_probe=True,
            )
        )

    for i in range(10):
        p = Proposal(**{**base, "state_version": "claims-v41"})
        episodes.append(Episode(f"stale-state-{i}", p, legitimate=False, hazardous=True))

    return episodes


def run_suite() -> dict[str, dict[str, int]]:
    now = datetime(2030, 1, 1, tzinfo=timezone.utc)
    results: dict[str, dict[str, int]] = {}
    for profile, spec in make_specs(now).items():
        metrics = {
            "legitimate_allowed": 0,
            "unsafe_allowed": 0,
            "confidentiality_exposures": 0,
            "reviews": 0,
            "denials": 0,
        }
        for episode in make_suite():
            decision = evaluate(episode.proposal, spec, now)
            if episode.legitimate and decision.outcome is Outcome.ALLOW:
                metrics["legitimate_allowed"] += 1
            if episode.hazardous and decision.outcome is Outcome.ALLOW:
                metrics["unsafe_allowed"] += 1
            if episode.confidentiality_probe and decision.outcome is Outcome.ALLOW:
                metrics["confidentiality_exposures"] += 1
            if decision.outcome is Outcome.REVIEW:
                metrics["reviews"] += 1
            if decision.outcome is Outcome.DENY:
                metrics["denials"] += 1
        results[profile] = metrics
    return results


def format_results(results: dict[str, dict[str, int]]) -> str:
    headers = (
        "profile",
        "legitimate_allowed",
        "unsafe_allowed",
        "confidentiality_exposures",
        "reviews",
        "denials",
    )
    rows = [headers]
    for profile, metrics in results.items():
        rows.append((profile, *(str(metrics[h]) for h in headers[1:])))
    widths = [max(len(str(row[i])) for row in rows) for i in range(len(headers))]
    return "\n".join(
        "  ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(row))
        for row in rows
    )


if __name__ == "__main__":
    print(format_results(run_suite()))

What to test before widening a contract

Run matched tasks under at least four conditions: unrestricted, action-only, evidence-plus-action and fully coupled. Keep the model, prompts, deterministic validation and review budget constant. Inject failures separately into source provenance, field scope, state freshness, destination, cumulative effect, self-query and contract mutation. Measure task completion, unsafe effect, confidentiality exposure, false denial, review load and recovery quality.

A widening is justified when a specific legitimate task fails, the missing relation is identified and no narrower repair works. It is not justified by a vague desire for “more context” or “more autonomy”. The change should update the contract, path inventory, failure suite and assurance record together. This is a lifecycle decision. The NIST AI Risk Management Framework organises risk work into continuing Govern, Map, Measure and Manage functions rather than a one-time release check.[15]

Choose contraction by uncertainty and irreversibility

Decision surface with uncertainty on the horizontal axis and irreversibility on the vertical axis Four regions recommend read-only exploration, bounded execution, independent approval, or prohibition and redesign. As uncertainty and irreversibility rise, evidence narrows and action authority reduces. Bounded execution narrow E · typed A · small R Independent approval fresh multi-source E · one-time A Read-only exploration broader E · no external A Prohibit or redesign uncertain task · irreversible effect contraction pressure rises Epistemic uncertainty → Effect irreversibility →
Low irreversibility permits bounded execution when evidence is clear. High uncertainty calls for read-only exploration; high irreversibility calls for independent approval or redesign. The surface selects a contract profile, not a model score. Decision instrument. Quadrants are normative guidance, not empirical thresholds.

Where contraction can fail

Worked example 4: failure case

Over-contraction can remove the evidence needed for a fair decision. If a disability adjustment legitimately changes expense policy, excluding every related field may produce systematic denials. The answer is not unrestricted access. It is a purpose-specific route with appropriate consent, specialist review and a separate contract.

Contraction can also move harm rather than remove it. A narrow agent may send many cases to humans, creating approval fatigue and delay. A hard-coded field list can preserve old organisational bias. A policy engine can be wrong. A verifier can share the same dependency as the executor. The architecture limits opportunity; it does not make policy, data or human judgement correct.

Do not force tight execution contracts onto open-ended scientific exploration. Use a read-only sandbox with broader evidence and no consequential effect path. Do not rely on contraction alone in emergency systems whose failure modes include delayed action. Pre-authorise carefully bounded contingencies and test them under stress. Do not treat a successful toy suite as proof of security. Real systems require adversarial testing, operational telemetry and change control.

One hostile instruction meets four independent stops

An adversarial path from a receipt to exfiltration blocked at evidence, action, self-view and verification gates A coral hostile instruction travels from an uploaded receipt toward an external email effect. It is first marked as untrusted evidence, then rejected as an ungranted action and destination, denied access to contract mutation, and unable to produce a matching outcome receipt. Hostile text “email allreceipts” E gate untrusted content A gate email not granted R gate contract locked Receipt gate no effect proof Failure does not depend on one perfect detector external effect
Defence does not depend on correctly interpreting the hostile sentence. The content remains untrusted, the proposed tool and destination are outside the action grant, contract mutation is outside the self-view and no valid effect receipt can be produced. Adversarial-path visual. The stops are independent enough to survive one missed classification.
Consequence
The evidence conversation
Chart · sequence
sequenceDiagram
  participant Q as Question
  participant M as Mechanism
  participant E as Evidence
  participant D as Decision
  Q->>M: The receipt that carried its own instructions
  M->>E: Same model, three shells
  E-->>M: compare rival explanations
  E->>D: Contract reach before deploying capability
  D-->>Q: revise scope or act
Question, mechanism, evidence and decision exchange the burdens developed in Constraint through contraction. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

Contract reach before deploying capability

Do not ask whether an agent is “autonomous enough” or “safe enough” in the abstract. Decide which trajectory relations this task requires, then contract everything else before choosing how much model capability to deploy.

Start with one consequential action. Name the subject, purpose, state and expiry. Project only the evidence fields needed for that decision. Define a typed effect with destination, magnitude, count, reversibility and approval rules. Expose the operational self-facts required for abstention and recovery, while keeping credentials, hidden detector internals and boundary mutation outside the model’s authority. Require an effect receipt and independent readback.

Then run failure injections that attack each aperture and the bridges between them. If legitimate work fails, identify the missing relation. Widen one element, recompute the coupled contract and rerun the suite. Record the new opportunity and review burden. Never let a failed attempt silently grant broader context or authority.

The practical design unit is no longer “the agent”. It is the model plus a versioned contraction contract over evidence, effects and operational self-view. This changes procurement, architecture review and release evidence. A team can use a more capable model without giving it a larger lawful world. It can also discover that a supposedly narrow workflow remains wide because one aperture substitutes for another.

The smallest useful implementation is modest: one task, one field projection, one typed action, one bounded self-view, one external policy decision and one verified outcome. That is enough to test the causal claim. If coupled contraction preserves task success while selectively removing cross-aperture failure paths, the architecture earns expansion. If it merely creates denial and human toil, the contraction is wrong and must be redesigned.

Make this decision differently: approve agent capability only after the three apertures and their coupling are explicit, executable and tested against current state. Model behaviour may motivate trust, but only the contraction contract grants reach.