Packages
AI agent framework for Elixir built on OTP. TEA-based agents with crash isolation, inter-agent messaging, team supervision, and real SSE streaming to Anthropic, OpenAI, Ollama, and more.
Current section
Files
Jump to
Current section
Files
lib/raxol/agent/authorization/policy.ex
defmodule Raxol.Agent.Authorization.Policy do
@moduledoc """
A single policy in the ALLOW/ASK/DENY authorization engine.
Distinct from `Raxol.Agent.Policy` (operational resilience: retry/cache/timeout)
and from the deny-only `Raxol.Agent.PermissionHook` -- this is the authorization
trichotomy with label state and human-in-the-loop ASK.
A policy is data: when it applies (phase + condition gate), what labels it may
write, how its ASK approvals are remembered, and an `evaluate` function that
returns a `Raxol.Agent.Authorization.Verdict`.
## Applicability gates (cheap-first)
1. **Phase** -- `phases` is `:all` or a list
(`:input | :tool_call | :tool_result | :output`).
2. **Conditions** -- a label gate `%{key => value | [values]}`: AND across
keys, OR within a key's list, against the current label snapshot.
## Writable labels
`writable_labels` is `:all` or a whitelist; a verdict's `writes` are filtered to
keys the policy may write before they touch label state.
## ASK scope (approval memory)
* `:once` -- not remembered; ask again next time.
* `:session` -- remembered for the engine's lifetime.
* `:root` -- remembered per evaluation `route` (e.g. the root of a spawn
tree), so approving once covers the whole tree.
"""
alias Raxol.Agent.Authorization.Verdict
@type phase :: :input | :tool_call | :tool_result | :output | atom()
@type scope :: :once | :session | :root
@type t :: %__MODULE__{
name: atom() | binary(),
phases: :all | [phase()],
conditions: map(),
writable_labels: :all | [term()],
scope: scope(),
evaluate: (map() -> Verdict.t())
}
@enforce_keys [:name, :evaluate]
defstruct name: nil,
phases: :all,
conditions: %{},
writable_labels: :all,
scope: :session,
evaluate: nil
@doc "Build a policy. Requires `:name` and `:evaluate`."
@spec new(keyword() | map()) :: t()
def new(attrs), do: struct!(__MODULE__, attrs)
@doc "Run the policy's `evaluate` against `context`, filtering its writes."
@spec run(t(), map()) :: Verdict.t()
def run(%__MODULE__{evaluate: evaluate} = policy, context) do
verdict = evaluate.(context)
%{verdict | writes: filter_writes(verdict.writes, policy.writable_labels)}
end
@doc "Does this policy apply at `phase` given the current `labels`?"
@spec applies?(t(), phase(), map()) :: boolean()
def applies?(%__MODULE__{} = policy, phase, labels) do
phase_match?(policy.phases, phase) and conditions_met?(policy.conditions, labels)
end
defp phase_match?(:all, _phase), do: true
defp phase_match?(phases, phase) when is_list(phases), do: phase in phases
# AND across keys, OR within a key's list of accepted values.
defp conditions_met?(conditions, labels) do
Enum.all?(conditions, fn {key, accepted} ->
value = Map.get(labels, key)
case accepted do
list when is_list(list) -> value in list
single -> value == single
end
end)
end
defp filter_writes(writes, :all), do: writes
defp filter_writes(writes, allowed) when is_list(allowed), do: Map.take(writes, allowed)
end