Autonomy Tiers in Code: Enforcing an Agent Approval Matrix

A board-approved autonomy policy that lives in a slide deck changes no agent behaviour. The model still decides what it decides. The only autonomy policy that governs anything is the one the agent’s execution path is forced to consult before it acts.

Most firms that have classified their agents into autonomy tiers stop at the classification. There is a document. It says a tier 1 agent may only read, a tier 2 agent may act on low-value items, a tier 3 agent may act up to some threshold with a human on higher-value work. The board signs it. Then the same agent runs against production with the same permissions it always had, because nothing in the running system knows the document exists. The classification is real. Its enforcement is imaginary.

This is the piece that closes the gap: how to take an autonomy classification the board has signed off and turn it into runtime rules that decide, for each action the agent proposes, whether it auto-executes, escalates to a human, or is blocked. The approved risk appetite governs the action, not the model’s discretion in the moment.

The policy is data, not prompt text

The first mistake is to encode the policy in the system prompt — “you may refund up to 500 without approval”. That is not a control. It is a request, made to a component whose defining property is that it does not reliably follow requests. A prompt instruction is advisory; the model can be argued out of it, confused out of it, or prompt-injected out of it, which is precisely the failure mode the OWASP Top 10 for Agentic Applications, published in December 2025, catalogues under excessive agency and tool misuse.

The policy has to be data, evaluated by deterministic code that sits between the agent’s decision and the outside world. This is the harness doing its job — the model proposes, the harness disposes. The matrix has three inputs and three outputs. The inputs are the agent’s autonomy tier, the risk class of the action type, and the amount at stake. The outputs are auto-execute, require human approval, or deny. Everything the board argued about in the risk-appetite discussion reduces to which cell of that matrix an action lands in.

Keeping it as data matters for a second reason. When the appetite changes — a tier gets tightened after an incident, a new action type is registered, a threshold moves — you edit a table and, ideally, route that edit through change control. You do not redeploy the agent or rewrite a prompt and hope. The control has a version, an author, and an approval trail, which is exactly what a supervisor examining your governance will ask to see.

Evaluated on every action, in the execution path

The engine has to be unavoidable. If the agent can reach a tool without passing through the check, the check is decorative. In practice that means the evaluation wraps the tool-invocation layer — the point where the agent’s intent becomes a real side effect — not some advisory step the model is trusted to call. Here is a minimal, self-contained version of the engine in Python.

"""Runtime enforcement of a board-approved agent autonomy matrix.

Every action an agent proposes is evaluated before it runs. The matrix
is data, not code: it maps (tier, action risk, amount) to one of three
outcomes. Changing the firm's risk appetite means changing the table.
"""

from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
import logging

logger = logging.getLogger("agent.autonomy")


class Decision(str, Enum):
    AUTO = "auto_execute"
    APPROVE = "require_approval"
    DENY = "deny"


class Risk(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"


@dataclass(frozen=True)
class Action:
    type: str        # e.g. "refund", "send_email", "close_position"
    risk: Risk       # fixed when the action type is registered
    amount: Decimal  # exposure; Decimal("0") for non-financial actions


# The matrix the board signed off, as data. Each value is the ceiling up
# to which an action auto-executes; above it, it escalates to a human.
AUTO_CEILING = {
    (1, Risk.LOW): Decimal("0"),      # tier 1: nothing auto-executes
    (2, Risk.LOW): Decimal("500"),
    (2, Risk.MEDIUM): Decimal("0"),
    (3, Risk.LOW): Decimal("5000"),
    (3, Risk.MEDIUM): Decimal("1000"),
    (3, Risk.HIGH): Decimal("0"),
}

# The hard ceiling: the highest risk class a tier may ever attempt.
# Anything above this is denied, not escalated.
DENY_ABOVE = {1: Risk.LOW, 2: Risk.MEDIUM, 3: Risk.HIGH}
_ORDER = {Risk.LOW: 0, Risk.MEDIUM: 1, Risk.HIGH: 2}


def evaluate(tier: int, action: Action) -> Decision:
    """Return the enforced decision for one proposed action."""
    ceiling = DENY_ABOVE.get(tier)
    if ceiling is None or _ORDER[action.risk] > _ORDER[ceiling]:
        decision = Decision.DENY
    else:
        auto = AUTO_CEILING.get((tier, action.risk))
        if auto is not None and action.amount <= auto:
            decision = Decision.AUTO
        else:
            decision = Decision.APPROVE

    # One structured line per decision -- the audit trail a supervisor
    # asks to see, not a debug aid. Log the deny and the approval too.
    logger.info("autonomy_decision", extra={
        "tier": tier, "action_type": action.type,
        "risk": action.risk.value, "amount": str(action.amount),
        "decision": decision.value,
    })
    return decision


class PendingApproval(Exception):
    pass


class ActionDenied(Exception):
    pass


def guard(tier: int, action: Action, execute):
    """Sits in the tool-invocation path. `execute` runs only on AUTO."""
    decision = evaluate(tier, action)
    if decision is Decision.AUTO:
        return execute()
    if decision is Decision.APPROVE:
        raise PendingApproval(action)   # hand to the approval queue
    raise ActionDenied(action)          # hard stop; no human is offered

Two distinctions in that code carry most of the governance weight. The first is that deny and require approval are not the same outcome. An escalation offers a human the choice to proceed; a deny does not reach a human at all, because the action is outside what this agent is permitted to attempt under any circumstances. Collapsing the two — routing everything risky to a human — quietly turns your denies into approvals and hands the agent capabilities the board never granted it. This is the boundary that bounded autonomy exists to draw.

The second is that risk class is a property of the action type, fixed when the type is registered, not something the model asserts per call. If the agent could tell the engine that a fund transfer is low-risk, the engine would be consulting the very component it is meant to constrain. The risk class comes from the catalogue; the amount comes from the action; only the proposal comes from the model.

The log is the point, not a by-product

Every evaluation emits one structured record — including the auto-executes and the denies, not only the escalations. That log is the evidence that the approved appetite actually governed behaviour. Without it you can assert that the matrix was enforced; you cannot demonstrate it. For firms inside the scope of the EU AI Act, Article 14’s human-oversight requirement for high-risk systems is not satisfied by a person theoretically being available. It is satisfied by showing which actions were withheld for a human and which the system was permitted to take alone — which is precisely what the decision log records.

The record should also carry a correlation identifier so an escalated action, its approval, and its eventual execution stitch into one trail. That same identifier is what a compensation or rollback mechanism keys off when an auto-executed action later has to be undone. Enforcement at the gate and reversal after the fact are the same governance problem seen from two ends.

Where this goes wrong in practice

Three failure modes recur. The engine is bypassable, because one tool path was wired directly and never routed through the guard — so audit the tool surface, not the prompt. The matrix drifts from the board’s decision, because someone edited the table without change control — so version it and require sign-off on changes the way you would for any risk-appetite adjustment. And the escalation queue has no teeth, because “require approval” resolves to a notification nobody owns and items auto-proceed after a timeout — which converts your entire approval column back into auto-execute the moment the team is busy.

An autonomy tier that exists only in a document is a description of intent. An autonomy tier that a deterministic engine enforces on every action, and logs while doing it, is a control. The distance between the two is a few hundred lines of code and the discipline to make them unavoidable — and it is the distance between a policy you can wave at a regulator and one you can prove.

Free interactive tool

Interactive deadline calculator

Check which regulations apply to you and when

Regulation across the EU, UK, US and Asia-Pacific has moved considerably in the past eighteen months, and several headline dates have shifted more than once. Twelve questions, about three minutes.

Results are shown on screen — no email required. A dated summary is available to download, and can be sent on if that's more useful. What we do with your answers.

Governance is what happens when nobody is watching.

Policies are easy. Consistent decision-making is harder. Understand where governance exists and where it has quietly become assumed.

Full Governance by Sixteen Pillars

Govern your business. Prove your compliance.

A board assurance cockpit for EU-regulated financial firms — tamper-evident, hash-chained proof of governance across DORA, GDPR, NIS2, ISO 27001, the EU AI Act and MiCA. In development.

See what's coming