The shared vocabulary
Six terms the rest of this documentation uses precisely. They are worth ten minutes because getting any of them slightly wrong makes every later sentence wrong — and because most of them come from a real bug that a loose definition allowed.
Three names: AEGL, AEGS, Tesoro
| Name | Is | Is not |
|---|---|---|
| AEGL | Autonomous Economic Governance Layer — the category | a package |
| AEGS | Autonomous Economic Governance Standard — the open specification | an implementation |
| Tesoro | a Python package; a policy-engine host | the standard |
In prose, a layer is an AEGL. Tesoro implements AEGS, and AEGS is one profile
Tesoro can enforce rather than Tesoro's identity — you can run it with
profile: none and it governs exactly as well, it just makes no conformance claim.
Two channels
An agent spends in two directions, and they are never one budget.
internal | external | |
|---|---|---|
| What | tokens the agent burns thinking | what it pays out |
| Currency | real money, on your provider key | often stablecoin, on a chain |
| Counterparty | one provider you chose | anyone the agent picks |
| When exhausted | REJECT | REVIEW |
That last row is the whole reason they are separate. The same breach has a different correct answer depending on which money it was.
A payout can wait for a person — REVIEW means pausable, and a human can look at it. A token budget cannot: there is no human in the loop of a running agent, so “pause and ask” becomes “hang”, and the tokens already spent are spent. Worse, starting a step that cannot finish wastes the budget that is already short.
An implementation may share a limit between channels but never an envelope, because an envelope carries consumption. Sharing consumption would mean an agent's thinking exhausted its ability to pay, which is not a relationship anybody intended.
Four verdicts
APPROVE < REVIEW < ESCALATE < REJECT
| Verdict | Means | The agent should |
|---|---|---|
| APPROVE | proceed | pay, then settle |
| REVIEW | a human should look — pausable | queue it and continue with something else |
| ESCALATE | a human must look — blocking | stop this line of work |
| REJECT | no, and no amount of waiting changes it | abandon the action |
The ordering is normative — it is not a display preference — and the middle pair is
the one that invites disagreement. Swapping REVIEW and ESCALATE fails six
of the standard's test vectors, which exist precisely because two implementations could otherwise
each be internally consistent and mutually incompatible.
Narrowing, and why widening is unreachable
Every control returns an assessment, and the final verdict is the most severe one any control produced. So:
- any control may tighten a verdict another control set;
- no control may widen one — it cannot rescue an action another control refused.
That second property is stronger than it looks, because widening is not forbidden, it is unreachable. Taking a maximum has no branch that could return something lower. A hostile or buggy control can refuse things it should not; it cannot approve anything. Which is what makes a third-party extension safe to load, and why an optional advisory model is allowed to exist at all — it can only make the answer stricter.
Attribution — the field that matters most
Which control decided, not what was decided. A refusal with no attributable cause is not auditable evidence.
It sounds trivial and is not. Consider a sanctioned counterparty asking for more than the per-transaction ceiling. Both controls refuse. If attribution means “the last control that narrowed”, the answer depends on which limit happened to trip first — so a sanctions refusal can be reported as a budget problem, and the operator raises a limit that had nothing to do with it.
The standard fixes this with dispositive controls: a finding that decides
attribution whether or not it narrowed anything, and which must be declared rather than
inferred. Tesoro declares exactly one — sanctions.
Thirteen controls
A control is a named thing that can examine an action. The name set is closed and matching is exact:
| Control | Asks |
|---|---|
AgentIdentity | who is acting, under whose authority |
EconomicIntent | what was this agent sent to do |
Policy | what do the operator's rules say |
BudgetEnvelope | is there headroom |
RiskAssessment | does this action's shape score as risky |
TrustAssessment | has this counterparty earned anything |
GovernanceDecision | the verdict itself, as a recorded artifact |
EvidenceRecord | the chained journal entry |
Authorization | the attribution and reasons |
ConformanceDeclaration | what levels are claimed |
AMLAssessment | screening — a schema with no engine behind it |
ComplianceAssessment | jurisdictional checks — also unimplemented |
IncidentRecord | what went wrong, recorded |
Exact matching looks pedantic until two deployments disagree about whether
budgetenvelope is the budget envelope — at which point a profile requiring
BudgetEnvelope silently scores an implementation that never had it. A case variant is
an extension, not the defined control.
Envelopes
An envelope is a limit plus the consumption against it. Two properties are worth separating, because conflating them was a real defect:
| Field | Answers | Exists |
|---|---|---|
binding | why was this refused? | only on a refusal |
tightest | what bites next? | always |
An earlier version of the report had only binding, under a heading meaning
tightest — so an approved decision showed no envelope at all, blank precisely when
somebody was checking headroom.
A per-call ceiling is not an envelope in this sense: it never accumulates, so it has
no used value. Rendering it as $0.00 of $10.00 beside the cumulative rows
reads as “nothing was spent”, which is false — and was also a real defect.
Four states
absent ≠ not-run ≠ unknown ≠ zero. Four
different answers, and collapsing any two loses something a reader needs.
| State | Means |
|---|---|
absent | the field is not there. Nothing was claimed, so nothing may be inferred |
not-run | the control exists, was asked, and declined to answer |
unknown | it ran and cannot report a value |
zero | it ran, and the answer really is zero or false |
This comes from a real bug. An unmeasured vendor history rendered as
0, so every advisor treated established counterparties as strangers — the
system was reading “we have never measured this” as “this counterparty has no history”.
The same rule applies to a validator: “I could not check” is not “this is
invalid”. And to an unreachable external assessor: the answer is not-run, never
pass, because fail-open is a governance layer that stops governing exactly
when it is attacked.
Profiles vs policy packs
The distinction most worth keeping straight, because they are both YAML and both about rules:
| Profile | Policy pack | |
|---|---|---|
| Says | which controls must exist, what must be recorded | what the rules are — the numbers |
| Written by | the standard | you |
| Changes | what is scored | what is decided |
| Example | aegs-1, aegs-2, none | policies/default.yaml |
A profile never touches a verdict — asserted four different ways in the test suite.
Switching from aegs-1 to none changes no decision; it only stops making a
conformance claim.
Evidence
A Decision Record is the artifact one decision produces: the action, the verdict, the attributed control, the reasons, the policy hash, and both version lines. Records are appended to a hash-chained journal, where each entry commits to its predecessor.
| Detected | Not detected |
|---|---|
| editing an entry · deleting one from the middle · reordering · replay | deleting entries off the end |
Any prefix of a valid chain is itself valid, so truncation is undetectable without an external anchor. That is disclosed rather than papered over — and the fix that looks like one is worth naming: writing the chain head to a second file beside the journal is not an anchor, because whoever can truncate the journal can rewrite that file too.
Money
Integer atomic units internally. USDC has 6 decimals, so 2500000 is
$2.50. Conversion happens once at the boundary, with an explicitly specified rounding
mode (half-up).
amount_usd="2.50" # a decimal string
amount_usd=2500000 # atomic units — the same amount
amount_usd=2.50 # TypeError
The rounding mode is pinned because every language's default differs and none is wrong in isolation
— an implementation inheriting its own has made a decision it did not know it was making. And a
float raises rather than rounding, because 0.1 + 0.2 is not
0.3 and there is no rounding the layer could pick that the caller would know about.