Policies

A policy file is your authorization rulebook. It lives on your machine, it is human-readable, and every authorization decision is evaluated against it locally.

How policies work

When authorize() runs, it reads your policy.yaml file and evaluates the incoming request against the rules top-to-bottom. The first rule that matches wins. If nothing matches, the defaultVerdict applies — and if you omit it, it defaults to pending_approval.

Shotoku fails closed. If the policy file is missing, malformed, or contains unknown fields, the request is denied. Unknown fields are rejected instead of silently ignored, because a typo in an authorization policy should not weaken it.

Example policy

policy.yaml
defaultVerdict: pending_approval

rules:
  # Known AI APIs: approve up to budget
  - resource: "api.openai.com"
    actions: ["api_call"]
    verdict: approved
    maxAmount: 5.00
    maxDailyAmount: 50.00

  - resource: "api.anthropic.com"
    actions: ["api_call"]
    verdict: approved
    maxAmount: 2.00
    maxDailyAmount: 20.00

  # Payments require human approval
  - resource: "stripe.com"
    actions: ["purchase"]
    verdict: pending_approval

  # Block all other spending
  - resource: "*"
    actions: ["purchase"]
    verdict: denied

Top-level fields

FieldTypeWhat it does
defaultVerdictAuthorizationStatusWhat happens when no rule matches. Defaults to pending_approval if omitted.
rulesPolicyRule[]The ordered list of rules. Evaluated top-to-bottom — the first match wins.
grantTtlSecondsnumberOptional. How long a human approval stays redeemable, in seconds. Defaults to 900 (fifteen minutes).
budgetsPolicyBudget[]Optional. Daily ceilings per agent, across every resource. See Agent budgets.

Rule fields

Each entry in the rules array is a PolicyRule:

FieldTypeWhat it does
resourcestringThe domain or service this rule applies to. Use "*" to match anything not matched by an earlier rule.
actorstringOptional. Limits this rule to agents matching this pattern — * is a wildcard, so team-* matches team-alpha and team-billing. Omit to match all agents.
actionsAgentAction[]Optional. Limits this rule to specific action types. Omit to match all actions.
railsExecutionRail[]Optional. Limits this rule to specific execution rails (x402, mcp, etc.). Omit to match all rails.
verdictAuthorizationStatusThe outcome when this rule matches: approved, denied, or pending_approval.
recipientsstring[]Optional. Limits this rule to payments to these exact recipients (an x402 payTo, for example). A request with no recipient does not match.
maxAmountnumberOptional. Maximum allowed spend per single transaction in USD. Exceeding it denies the request even if the rule would otherwise approve it, unless overLimit says to ask a human.
maxDailyAmountnumberOptional. Maximum total spend for this resource in a rolling 24-hour window in USD. Exceeding it denies the request, unless overLimit says to ask a human.
overLimitstringOptional. What a request over maxAmount or maxDailyAmount gets: denied (the default) or pending_approval. Only on a rule with verdict: approved and a limit.

Wildcard rules

A resource: "*" rule matches any request that was not matched by an earlier rule. Use it to define a catch-all policy at the bottom of your file.

yaml
# Deny all purchases not covered above
- resource: "*"
  actions: ["purchase"]
  verdict: denied

# Send everything else for human review
- resource: "*"
  verdict: pending_approval

Rules are evaluated in order. A wildcard at position 3 only catches requests that did not match rules 1 and 2. Order matters.


Daily limits

maxDailyAmount is evaluated against a rolling 24-hour window, not a calendar day. Shotoku reads the local ledger to compute how much has been spent for the given actor and resource combination in the past 24 hours, then checks whether this request would push it over the cap.

The daily total is keyed as actor|resource. Two different actors spending against the same resource do not share a budget — each actor has its own counter.

yaml
- resource: "api.openai.com"
  actions: ["api_call"]
  verdict: approved
  maxAmount: 5.00        # no single call costs more than $5
  maxDailyAmount: 50.00  # total spend per actor per 24h

Over the limit: deny or ask

By default, a request over a rule's limit is denied. With overLimit: pending_approval, it waits for a human instead, with the exceeded limit as the reason. Small refunds go through on their own; a big one gets a person.

yaml
- resource: paypal__create_refund
  verdict: approved
  maxAmount: 100               # refunds up to $100 go through
  maxDailyAmount: 1000
  overLimit: pending_approval  # a bigger one waits for a human

Once a human approves, the agent's identical retry goes through, once, and counts toward the daily limit like any approved spend. Rules do not fall through: a second, lower rule for the same resource never sees a request the first one matched.


When the amount is unknown

A missing amount is not a zero amount. When the matched rule has maxAmount or maxDailyAmount and the request has no amount, the limit cannot be checked, so the request waits for a human: Amount unknown; the $100 per-transaction limit cannot be checked. A rule without limits still approves requests that carry no amount.

Wrapped MCP tools carry no amount of their own. Give their server a pricer so limits apply to them; see the MCP guide.


Agent budgets

Rule limits are per resource: a resource: "*" rule with a $200 daily limit allows $200 for each vendor. A budget caps what one agent spends in total, whatever it pays.

yaml
rules:
  - resource: "*"
    verdict: approved

budgets:
  - actor: support-agent
    maxDailyAmount: 1000      # everything this agent spends, per 24h
  - actor: "team-*"
    maxDailyAmount: 300
    scope: shared             # all team agents together

Over a budget is always a denial, even after a human approves: an approval never spends money that is already gone.


Rail-specific rules

You can write rules that only apply when an action is taken through a specific execution rail. For example, to require approval for all x402 payments regardless of resource:

yaml
- resource: "*"
  rails: ["x402"]
  actions: ["purchase"]
  verdict: pending_approval

x402 policy example

If you are using the x402 payment rail, the agent passes rail: "x402"and action: "purchase" with the payment amount. Your policy can match specifically on these:

policy-x402.yaml
defaultVerdict: pending_approval

rules:
  # Trust low-cost data endpoints
  - resource: "api.datavendor.com"
    actions: ["purchase"]
    rails: ["x402"]
    verdict: approved
    maxAmount: 0.10
    maxDailyAmount: 5.00

  # Anything more expensive needs a human
  - resource: "*"
    actions: ["purchase"]
    rails: ["x402"]
    verdict: pending_approval