Skip to content

Policy language

A policy is a plain Python dict (or a JSON file you load from disk). @agentveto.guard(policy, action="...") decorates the tool it protects; the policy decides, before each invocation, whether the call is allowed, denied, or must be approved by a human.

Shape

POLICY = {
    "default": "allow",  # what happens when no rule matches
    "rules": [
        {
            "name": "no-customer-email",  # recorded in the trace
            "action": "send_email",  # exact, "send_*", or "*"
            "effect": "deny",  # allow | deny | ask
            "when": {  # optional: all must hold
                "to": {"endswith": "@external.com"},
            },
            "reason": "External recipients blocked.",  # or {"en": ..., "zh": ...}
        },
        {
            "name": "big-refund",
            "action": "issue_refund",
            "effect": "ask",
            "when": {"amount_usd": {"gt": 250}},
            "reason": "Refunds > $250 need approval.",
        },
    ],
}

The grammar:

Field Required Notes
default no allow (default) or deny
rules no an ordered list; first match wins
rule.name recommended unique; surfaces in traces and reports
rule.action no, defaults to * exact name, prefix_*, or *
rule.effect yes allow, deny, or ask
rule.when no a dict of paths to conditions; all must hold
rule.reason recommended plain string or {lang: text}; appears in the report

Operators

Inside when, each value is either a bare value (implicit eq) or a dict of one or more operators:

Operator Meaning
eq, ne equality (and inequality)
gt, gte, lt, lte ordered comparison; silently fails to false on incomparable types (e.g. string > 0)
in membership in a list
exists the path is present (true) or absent (false)
endswith, startswith, contains string operators (both sides must be strings)
len_eq, len_gt, len_lt length comparison; works on strings, lists, dicts

Paths are dot-separated: "customer.email" looks up payload["customer"]["email"]. A missing path fails every operator except ne and exists.

Effects

Effect What happens
allow the wrapped function runs
deny VetoError is raised; the wrapped function does not run
ask if a human is at the keyboard and approves, run; otherwise deny

ask defaults to prompting on sys.stdin. In CI, on a server, or in a cron job there is no human, and the call is denied. This is the fail-closed property; it is intentional. Pass a custom prompter to guard() to override (e.g. an in-app approval queue).

First match wins

Rules are evaluated in order. The first rule whose action matches and whose when clauses all hold determines the decision. Order your rules with the most specific ones first.

"rules": [
    {"action": "send_*", "when": {"to": {"endswith": "@external.com"}}, "effect": "deny"},
    {"action": "send_*", "effect": "ask"},   # everything else needs a human
    {"action": "*", "effect": "allow"},      # everything else is fine
]

Validating and explaining without running

The CLI lets you check a policy and walk one action through it without running any code:

agentveto policy show    policy.json
agentveto policy validate policy.json --strict
agentveto policy explain policy.json --action send_email \
    --payload '{"to":"bob@external.com","body":"hi"}'

explain returns the same shape the runtime decision code returns, so a "why is this denied" question can be answered by a terminal command in 200 ms instead of reading the source.

Localised reasons

reason may be a string, or a dict of language code to string:

{"reason": {"en": "Refunds > $250 need approval.", "zh": "超过 $250 的退款需要人工审批。"}}

AGENTVETO_LANG or LANG selects which one is shown.

Loading from a file

import agentveto.policy_io as pio

policy = pio.load_policy("policy.json")


@agentveto.guard(policy, action="send_email")
def send_email(to, subject, body): ...

If the policy file is malformed, load_policy raises PolicyError with a message you can act on (line / rule index / what went wrong). The runtime path never falls back to a broken policy; you find out at import time.