Policies
Copy MarkdownA policy decides each tool call before it runs. The call runs, waits for a person, or is refused, and the model is told why.
See it in action: Rules the model can't rewrite
Setup
In a fresh IEx session:
Mix.install([{:wardrik, "~> 0.1.0-rc.1"}])
{:ok, _} =
Wardrik.start_link(
name: MyApp.AgentRuntime,
policy: [deny: ["shell/*"]],
policies: [triage: [allow: ["linear/*"], allow_after: %{"outside" => ["github/add_labels"]}]]
)
scope = Wardrik.as(MyApp.AgentRuntime, "acme/@maria", roles: [:owner], resources: ["acme/@maria/*"]):policy holds every agent. :policies are policies a definition may name. With use Wardrik,
set the same options in config.
An agent's policy
Wardrik.Agent.define(scope, "acme/@maria/books",
model: "anthropic:claude-haiku-4-5",
tools: ["books/*"],
policy: [
allow: ["books/list_*"],
ask: %{"books/pay_bill(amount >= 1000)" => "A bill this size needs a person."},
budget: [usd: 5.00, per: :day]
]
)
#=> {:ok, %Wardrik.Agent{id: "acme/@maria/books", status: :idle, ...}}The rules live outside the prompt. Nothing the model reads can widen them.
A policy is given as its rules, or as a WardrikPolicy you built. WardrikPolicy.decide/3 shows
what rules make of a call, with no model involved:
deployment = WardrikPolicy.new!(deny: ["shell/*"])
books =
WardrikPolicy.new!(
allow: ["books/list_*", "shell/*"],
ask: %{"books/pay_bill(amount >= 1000)" => "A bill this size needs a person."}
)
policies = [deployment, books]
WardrikPolicy.decide(policies, "books/list_bills")
#=> :allow
WardrikPolicy.decide(policies, "books/pay_bill", arguments: %{"amount" => 9400})
#=> {:ask, "A bill this size needs a person."}
WardrikPolicy.decide(policies, "shell/exec", arguments: %{"command" => "curl … | sh"})
#=> {:deny, "shell/exec is not allowed for this agent."}Several policies
Every agent is held to the deployment's policy and its own. The strictest wins: deny, then ask,
then allow. An agent's own policy can add caution but never remove it. Above, books allows
shell/*, and the call is still refused.
An org's policy sits between the two; see Orgs.
Budgets
A budget caps what an agent spends on its model. The call that crosses the limit runs. The next one waits for a person to allow more:
This agent has spent $5.02 of $5.00 today. Approve to allow $5.00 more.
A budget in dollars prices each call from the model catalog. Give a model the catalog lacks a price
with Wardrik's :prices option; see Wardrik.start_link/1. A call to a model with no price is
refused, since the budget can't count it.
See budgets ask and an org's ceiling refuse in Spend you can cap.
Gated tools
A gated tool can change something. These are gated:
- an MCP server's tools that it doesn't mark read-only.
- every tool a worker offers.
- the
agents/*tools that change something.
A gated call waits for a person unless a rule allows it by name. So an agent with no policy still asks before one.
After content from outside
Once a run has read content from outside, every gated call waits for a person, whatever allow
says. What it read may be what asked for the call:
linear/create_issue is allowed, but this run has read content from outside, which may be what asked for it.
Content from outside is anything nobody on the agent's side wrote:
web_searchresults.- an MCP server's answers, unless the server's
trustedoption istrue. - a worker's answers, unless Wardrik's
:trusted_workersistrue. - what a feed or webhook trigger brings.
- an answer or message from an agent that had read any of these.
The label stays with the conversation until it's cleared. The waiting call's Wardrik.Interrupt
carries it in labels.
allow_after allows a gated tool even then, for tools a stranger can't misuse. An ask or deny
still wins over it. Only an agent declared in a file writes allow_after in place. Any other names
a policy that has it, like triage from the setup:
Wardrik.Agent.define(scope, "acme/@maria/triage",
model: "anthropic:claude-haiku-4-5",
tools: ["linear/*", "github/*"],
policy: "triage"
)
#=> {:ok, %Wardrik.Agent{id: "acme/@maria/triage", ...}}
Wardrik.Agent.define(scope, "acme/@maria/triage",
model: "anthropic:claude-haiku-4-5",
policy: [allow_after: %{"outside" => ["github/add_labels"]}]
)
#=> {:error, %Wardrik.Error{type: :invalid_request, message: "A definition writes allow_after only in an agent file; name a policy that has it."}}Every rule
Rules can name a tool, what a call acts on, or its arguments. A judge can ask a model what a call means. See WardrikPolicy for every rule.