# `WardrikPolicy`

Decides each tool call an agent makes: run it, ask a person first, or refuse it.

    iex> policy =
    ...>   WardrikPolicy.new!(
    ...>     allow: ["web_search", "write_file(notes/*)"],
    ...>     ask: %{"books/create_bill(amount >= 1000)" => "A bill this size needs a person."},
    ...>     deny: ["shell/*"]
    ...>   )
    iex> WardrikPolicy.decide(policy, "web_search")
    :allow
    iex> WardrikPolicy.decide(policy, "books__create_bill", arguments: %{"amount" => 9400})
    {:ask, "A bill this size needs a person."}
    iex> WardrikPolicy.decide(policy, "shell__exec")
    {:deny, "shell/exec is not allowed for this agent."}
    iex> WardrikPolicy.explain(policy, "shell__exec")
    %WardrikPolicy.Decision{
      effect: :deny,
      message: "shell/exec is not allowed for this agent.",
      rule: "shell/*",
      cause: :rule
    }

The strictest rule wins: deny, then ask, then allow.

## Rules

  * `:allow` - tool patterns that run.
  * `:ask` - tool patterns that wait for a person. A map of pattern to message sets the prompt.
  * `:deny` - tool patterns that are refused. A map of pattern to message sets the refusal.
  * `:allow_after` - a map of label to patterns that stay allowed after the run reads content
    with that label. See "Content from outside" below.
  * `:default` - what a call no rule names gets: `:allow`, `:ask`, `:deny`, or a map by
    annotation. `:allow` unless set.
  * `:budget` - caps what the agent spends on model calls. See `WardrikPolicy.Budget`.
  * `:judge` - asks a model about each call. See `WardrikPolicy.Judge`.

A pattern names a tool, what the call acts on, or its arguments:

    "web_search"                            # one tool
    "github/*"                              # every tool from the github MCP server
    "write_file(notes/*)"                   # what the call acts on, given as :resources
    "books/create_bill(amount >= 1000)"     # an argument; =, !=, >, >=, < and <=
    "email/send(to != *@acme.com)"          # a comma means all conditions must hold

An MCP tool named `github__create_issue` also matches as `github/create_issue`.

## Defaults by annotation

A call no rule names follows `:default`. The default can depend on the tool's annotations:

    iex> policy =
    ...>   WardrikPolicy.new!(
    ...>     allow: ["write_file(notes/*)"],
    ...>     default: [read_only: :allow, else: :ask]
    ...>   )
    iex> WardrikPolicy.decide(policy, "write_file", resources: ["notes/today.md"])
    :allow
    iex> WardrikPolicy.decide(policy, "write_file", resources: ["secrets/key"])
    {:ask, "write_file needs your approval."}
    iex> WardrikPolicy.decide(policy, "read_file", annotations: %{read_only: true})
    :allow

When a tool has several annotations, the strictest wins. `else` covers a tool with none of them,
and is `:allow` unless set.

## Policy files

Parse the YAML into a map, then call `decode/2`:

    iex> {:ok, policy} =
    ...>   WardrikPolicy.decode(%{"deny" => ["email/send(to != *@acme.com)"], "default" => "ask"})
    iex> WardrikPolicy.decide(policy, "email__send", arguments: %{"to" => "x@evil.example"})
    {:deny, "email/send is not allowed for this agent."}
    iex> WardrikPolicy.decide(policy, "web_search")
    {:ask, "web_search needs your approval."}

`to_json/1` writes a policy back as that map.

## Content from outside

A run that has read content from outside, such as a web page, may have been told what to call.
So once a run carries the `"outside"` label, a call to a gated tool asks a person, even if a rule
allows it. Pass the run's labels as `:labels`. `allow_after` keeps a call allowed for tools a
stranger can't misuse:

    iex> policy =
    ...>   WardrikPolicy.new!(
    ...>     allow: ["linear/*"],
    ...>     allow_after: %{"outside" => ["github/add_labels"]}
    ...>   )
    iex> gated = %{gated: true}
    iex> WardrikPolicy.decide(policy, "linear__create_issue", annotations: gated, labels: ["outside"])
    {:ask, "linear/create_issue is allowed, but this run has read content from outside, which may be what asked for it."}
    iex> WardrikPolicy.decide(policy, "github__add_labels", annotations: gated, labels: ["outside"])
    :allow

A tool is gated when its annotations include `gated: true`. Any label that `allow_after` names
holds gated calls the same way `"outside"` does. Other labels hold nothing.

# `decision`

```elixir
@type decision() :: :allow | {:ask, String.t()} | {:deny, String.t()}
```

# `effect`

```elixir
@type effect() :: :allow | :ask | :deny
```

# `t`

```elixir
@type t() :: %WardrikPolicy{
  budget: WardrikPolicy.Budget.t() | nil,
  by_annotation: %{required(String.t()) =&gt; effect()},
  default: effect(),
  judge: WardrikPolicy.Judge.t() | nil,
  name: String.t() | nil,
  rules: list()
}
```

# `decide`

```elixir
@spec decide(t() | [t()], String.t(), keyword()) :: decision()
```

Decides a call to the tool `name`. Returns `:allow`, `{:ask, prompt}` or `{:deny, message}`.

Given a list of policies, the strictest decision wins.

## Options

  * `:arguments` - the call's arguments, as a map with string keys. `%{}` unless set.
  * `:resources` - what the call acts on, such as file paths. `[]` unless set.
  * `:annotations` - the tool's annotations, as `%{read_only: true}`. `%{}` unless set.
  * `:labels` - labels for what the run has read, as `["outside"]`. `[]` unless set.
  * `:evaluate` - asks a judge's model. See `t:WardrikPolicy.Judge.evaluate/0`.

# `decode`

```elixir
@spec decode(map(), keyword()) :: {:ok, t()} | {:error, String.t()}
```

Reads a policy from a map with string keys, such as one parsed from YAML.

Returns `{:error, message}` for an unknown key or a rule that can't be parsed. Takes the same
options as `new!/2`.

# `explain`

```elixir
@spec explain(t() | [t()], String.t(), keyword()) :: WardrikPolicy.Decision.t()
```

Decides a call like `decide/3`, but returns a `WardrikPolicy.Decision` that says what decided.

Given a list of policies, the strictest decision wins. If any policy denies the call, it's
denied. Otherwise, if any asks, it asks.

# `new!`

```elixir
@spec new!(keyword() | map(), keyword()) :: t()
```

Builds a policy from a keyword list or map. See the module docs for the keys.

    WardrikPolicy.new!([deny: ["shell/*"], budget: [usd: 5.00]], name: "acme")

Raises `ArgumentError` with the message `decode/2` would return if a rule, budget or judge
can't be read.

## Options

  * `:name` - names the policy in each `WardrikPolicy.Decision`. `nil` unless set.

# `to_json`

```elixir
@spec to_json(t()) :: map()
```

Encodes a policy as the map `decode/2` reads, such as to store it as JSON.

Leaves out what the policy doesn't set. The policy's `:name` is not part of it.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
