tesoro 0.1.3

Policies & rules

A policy pack is what the rules are — you write it. A profile is which controls must exist and what must be recorded — the standard writes it. Getting these two confused makes every later sentence wrong, so they are never the same file.

The one rule about rules: they are data, never code

Declarative rules, a fixed comparator vocabulary, no expression language, nothing to eval. This is a security boundary rather than a style preference: an executable policy pack downloaded from a registry is remote code execution wearing a governance hat, and the risk arrives the moment packs are shared between deployments — which is where any successful policy format goes.

Anything a pack cannot express is a missing engine, and the answer is a new engine — code, reviewed as code, on a gated path — not an escape hatch.

The pressure on this will be constant. Every sufficiently expressive policy format is eventually asked for a way to run a small snippet, and each individual request is reasonable. The answer that holds is the one above.

What a pack looks like

tesoro init writes tesoro.yaml (which profile, which pack, where evidence goes) and policies/default.yaml — the rules, which are yours to edit. JSON is accepted everywhere YAML is.

version: 1
name: default

config:
  treasury:                        # the external channel
    balance_usd: "500"
    per_transaction_usd: "10"       # a ceiling, never accumulates
    daily_usd: "50"                 # cumulative
    monthly_usd: "300"
    per_vendor_30d_usd: "100"
    per_resource_30d_usd: "50"
    emergency_reserve_usd: "10"
    velocity_60s: 10                # a rate, not a total
    velocity_1h: 100
    actions_per_day: 500            # a total. omit for no limit; 0 forbids everything
    actions_per_month: 10000

  treasury_internal:               # tokens; overrides only what it names
    per_transaction_usd: "0.04"     # one run's token budget
    daily_usd: "0.15"

rules:
  - id: deny-sanctioned
    priority: 0                    # lowest number decides first
    when:
      vendor.sanctioned: true
    then: REJECT
    reason: sanctioned counterparty

  - id: review-budget-exhausted
    priority: 10
    when:
      budget.ok: false
    then: REVIEW
    reason: outside a budget envelope

Every amount is a string. A YAML number would have lost precision before anything read it, and the test vectors check exactly this.

The pack has a content hash, and it goes into every decision. tesoro check prints it. A label can be reused across edited rules; a hash cannot — which is why the Decision Record schema records the hash rather than the name, and why every measurement stamps it.

The fixed vocabulary

Ten comparators

ComparatorMeansExample
eq · neequals, does not equalvendor.id: {ne: acme}
lt · lteunder, at mostamount_usd: {lte: "1.00"}
gt · gteover, at leasttrust.score: {gte: 0.7}
in · not_inis / is not one ofbudget.binding: {in: [balance, emergency_reserve]}
betweeninclusive rangeamount_usd: {between: ["1.00", "5.00"]}
containsmembership in a list valuevendor.tags: {contains: verified}

eq, ne, in and not_in are null-safe; the ordering comparators are not, because “is an unknown score greater than 0.7” has no honest answer and inventing one is how the four-state rule gets broken.

Twenty facts a rule may read

Two facts for jurisdiction, and the second is the useful one. vendor.jurisdiction is the value an operator declared — and it is null for two different situations: nobody was asked, and an operator was asked and does not know. vendor.jurisdiction_state separates them, with one of undeclared, unknown or declared.

Nothing derives it. Not from the counterparty's address, not from a domain suffix in its id, not from the settlement chain. Each of those correlates with jurisdiction and none of them is jurisdiction — a .de domain is registrable from anywhere. An absent jurisdiction is visibly absent and you can refuse on it; an inferred one is a guess that looks exactly like a declared fact. AEGS-0.1-CTRL-6a makes that a requirement rather than a preference.

rules:
  - id: unvetted-counterparty
    priority: 1
    when:
      vendor.jurisdiction_state: undeclared
    then: REVIEW
    because: nobody has said where this counterparty is

priority is not optional here. Rules are evaluated in priority order and the first match is terminal, so a rule without one sorts last and is reached only when nothing else matched — which for a rule meant to catch unvetted counterparties is indistinguishable from it not working. This example was run before it was published, and it fired for undeclared and fell through for a declared LK.

GroupFacts
the actionamount_usd resource purpose channel agent_id
counterpartyvendor.id vendor.sanctioned vendor.tags vendor.is_new vendor.settled_count vendor.jurisdiction vendor.jurisdiction_state
trust & risktrust.score trust.flags risk.score risk.flags
valueroi.known roi.ratio roi.justified
budgetbudget.ok budget.binding budget.headroom_usd

The pack is validated at load, in full, or it is not loaded. An unknown fact name, an unknown comparator or a verdict that is not one of the four is a startup error rather than a surprise at run time — because a rule that never matches never validates, and a pack could otherwise carry a verdict of MAYBE and look fine until the one request that touched it arrived.

Derived facts: composition without execution

“amount over $100 and the vendor is new” needs no new measurement, only composition. That is a derived fact — still data, built from the same fixed comparators with three fixed connectives:

derived:
  - name: risky_newcomer
    all:                          # all | any | not
      - amount_usd: {gt: "100"}
      - vendor.is_new: true

rules:
  - id: review-risky-newcomer
    priority: 20
    when:
      risky_newcomer: true
    then: REVIEW

What makes this safe is the closure: a pack that could introduce a fourth connective could introduce logic.

Envelopes: the part a per-payment cap cannot express

per_transaction a ceiling. Never accumulates — there is no “used”. $10.00 max, per call daily cumulative. Consumes on settle. $6.02 of $50.00 monthly $66.10 of $300.00 per_vendor_30d $84.90 of $100.00 ← tightest
Four envelopes, one action, and the useful question is which one is closest to biting. Tesoro reports two different fields for two different questions: binding answers why was this refused and exists only on a refusal; tightest answers what bites next and always exists. An earlier version had only the first, under a heading meaning the second — so an approved decision showed no envelope at all, blank precisely when somebody was checking headroom.

A rate counter is not a count envelope, and this box used to conflate them. velocity_60s: 10 and velocity_1h: 100 bound the rate. They permit 2,400 actions a day, and an agent pacing at 97 an hour is compliant with them forever. actions_per_day and actions_per_month bound the total.

No product of a rate limit and a duration is ever compared against anything, so an hourly ceiling of 100 does not imply a daily ceiling of 2,400 — it implies none at all. Both kinds are count envelopes under AEGS-0.1-ENV-7, which has permitted them since 0.1; the only thing missing was a window longer than an hour.

Omit either key for no limit. 0 is not "no limit" — it forbids every action. See what Tesoro does not do for what a count envelope still does not catch.

Rules are evaluated in order, and the first match decides

By priority, lowest number first, with the rule id as the tiebreaker so two rules at the same priority resolve the same way every time. tesoro report --html prints the rules in evaluation order with each condition in plain terms, because the question anybody actually has before a run is which rule is going to stop me — and a count of rules cannot answer it.

Pri  Rule                        When                              Then      Because
0    deny-sanctioned             vendor.sanctioned                 REJECT    sanctioned counterparty
5    deny-over-balance           not budget.ok and budget.binding  REJECT    insufficient balance
                                 is one of balance, emergency_re…
10   review-budget-exhausted     not budget.ok                     REVIEW    outside a budget envelope

Profiles: what the standard requires, not what you want

A profile is a conformance contract. It says which controls must exist and what evidence must be emitted — and it never touches a verdict, which is asserted four different ways in the test suite. Switching profile changes what is scored, never what is decided.

LevelMeans
OPTIONALthe control may be absent entirely
MUST_RECORDthe record must carry the field — a stated null satisfies it
MUST_EXERCISEthe control must actually have run

The distinction between the last two is load-bearing, and getting it wrong punished honesty: an earlier scorer treated intentId: null as a failure. But a stated null is an implementation saying “no intent was declared”, which is true, useful, and exactly what the four-state rule asks for. MUST_RECORD needs the key present, not non-null.

ProfileRequiresFor
nonenothing — but lists all 13 controls explicitlyan opt-out, not an empty contract
aegs-15 exercised, 2 recorded, 6 optionalthe baseline claim
aegs-28 exercised, 1 recorded, 4 optionalthe stricter claim

none lists every control rather than being an empty file, so the shipped manifest is the implementation's own answer to “which controls are defined” — and a profile that extends another may only tighten, checked by a tool.

Two extension points, and why there are exactly two

Derived facts data

Composition of existing facts, in the pack. No gate needed, because nothing new is measured and nothing can execute.

Registered engines code

A genuinely new measurement. Code, reviewed as code, and gated at registration: reserved control names are refused, and a purity check rejects an engine that reads the filesystem, the network or the clock.

A registered engine can only narrow. That is not a rule it is asked to respect — the combination step takes the most severe verdict, so widening is unreachable rather than refused. A hostile engine can refuse things; it cannot approve anything.

Reference

Writing your own rules

The full pack format, both extension points, worked examples.

custom-policies.md →

Profile manifests

The three shipped profiles as machine-readable contracts.

aegs/profiles →

The specification

What a profile may and may not do, normatively.

AEGS →