Policy files
A policy is a heso.toml file of ordered [[rule]] blocks. The engine checks them in order and the first rule that matches an action decides it.
Every gated action carries a verb and a set of fields. The engine walks the rules by ascending order and stops at the first one whose subject, verb, scope, and conditions all match. That rule’s decision is what the engine enforces and stamps onto the receipt. No scoring, no priority math: first match wins.
A worked policy
Here is a real file: block deletes on the production account, route large payments to two approvers, redact tool calls, and allow everything else.
# heso.toml — rules are tried in ascending order.
# The first rule whose subject, verb, scope, and conditions all
# match decides the action. Put dangerous and specific lanes first;
# the broad allow goes last.
[[rule]]
id = "block-prod-deletes"
order = 10
enabled = true
subject = { kind = "account", value = "prod" }
verb = "delete"
scope = "*"
conditions = []
decision = "block"
[[rule]]
id = "approve-large-payments"
order = 20
enabled = true
subject = { kind = "any" }
verb = "payment"
scope = "*"
conditions = [
{ field = "amount_usd", op = "gt", value = 5000, display = "amount over $5,000" },
]
decision = "require_approval"
approvers = ["finance-lead", "cfo"]
sla_minutes = 120
[[rule]]
id = "redact-tool-calls"
order = 30
enabled = true
subject = { kind = "any" }
verb = "tool_call"
scope = "*"
conditions = []
decision = "redact"
[[rule]]
id = "allow-rest"
order = 99
enabled = true
subject = { kind = "any" }
verb = "any"
scope = "*"
conditions = []
decision = "allow"Read it top to bottom. A delete on the prod account hits block-prod-deletes and stops. A $9,000 payment skips that rule (wrong verb) and lands on approve-large-payments, which routes it to a human with a 120-minute window. A tool call is redacted before signing. Everything else falls through to allow-rest.
Rule fields
Every field a single [[rule]] block can carry.
- idstringrequired
- A stable, unique identifier for the rule. It is stamped onto the receipt as rule_id and named in any [FLOOR_BYPASS] error, so keep it human-readable.
- ordernumberrequired
- Where the rule sits in evaluation. Rules are matched by ascending order, top-down. Since the first match wins, order is how you decide which rule an action falls into.
- enabledboolrequired
- Whether the rule participates in matching. A disabled rule is skipped entirely, as if it were not in the file.
- subject{ kind, value? }required
- Who the action belongs to.
{ kind = "any" }matches every agent, or pin a role with{ kind = "workflow", value = "..." }or{ kind = "account", value = "..." }. - verbstringrequired
"any"or exactly one of the seven action verbs:llm_call,tool_call,http_request,payment,data_export,account_change,delete. See Actions & verbs.- scopestringrequired
"*"to match any host, or a host string to narrow the rule to the action’s target host. Useful for scoping a rule to one API or domain.- conditionsarrayrequired
- Zero or more checks on the action’s fields, each an inline table of
{ field, op, value, display }. Allconditions on a rule are AND’d; for OR, write two rules. Use[]for an unconditional rule. See Conditions & operators for the operator set. - decisionstringrequired
- What the engine does on a match:
allow,block,redact, orrequire_approval(route to a human). - approversstring[]
- Label strings naming who may approve. Required when
decision = "require_approval". See Human approval. - sla_minutesnumber
- How long an approval may sit before it breaches its service-level window. Recorded on the approval so reviewers see the clock.
Ordering and the default
Because the first match wins, the order of your rules decides everything. The pattern:
- Most specific, most dangerous rules first — blocks and approvals you never want skipped.
- Narrowing rules next — redactions and host-scoped checks.
- A broad allow last, as the catch-all for everything benign.
If an action reaches the end without matching any rule, the default decision is allow. The default is permissive, so the safety in your policy comes from the explicit block, redact, and approval rules you write above the catch-all.
The verbs payment, delete, account_change, and data_export carry a built-in floor that the engine enforces when it loads the policy. A policy may tightena floored lane, but it can never bypass it — you cannot blanket-allow a delete without the floor’s approval. A bypass attempt is rejected at policy load with a [FLOOR_BYPASS] error naming the offending rule id and verb. See Pinned floors.
Testing and deploying
Don’t ship a policy edit blind. Simulate it against a captured action to see which rule would match and what it would decide, then pull and deploy the file once the simulation reads right. Test & deploy walks the full loop.