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/tool_policy.ex
defmodule Raxol.Agent.ToolPolicy do
@moduledoc """
Composable authorizers for the LLM tool-call path.
A tool authorizer is a `(action_module, params, context) -> :ok | {:deny,
reason}` function placed in the agent context under `:tool_authorizer`.
`Raxol.Agent.Action.ToolConverter.dispatch_tool_call/3` consults it before
invoking the Action, so a prompt-injected LLM cannot drive a sensitive tool
(e.g. a payment Action) just by emitting a tool call. Absent, tool calls are
allowed (backward compatible) -- set one to enforce.
context = %{tool_authorizer: Raxol.Agent.ToolPolicy.denylist(["payment_execute_xochi_intent"])}
Raxol.Agent.Strategy.ReAct.execute(action_spec, state, Map.merge(base, context))
Authorizers compose with `all/1` (deny if any denies).
Richer phase-aware policy (ALLOW/ASK/DENY) lives in
`Raxol.Agent.Authorization.Engine` at the `:tool_call` phase; delegating to it
from here is a follow-up.
"""
@type authorizer :: (module(), map(), map() -> :ok | {:deny, term()})
@doc "Allow every tool call."
@spec allow_all() :: authorizer()
def allow_all, do: fn _module, _params, _context -> :ok end
@doc "Deny every tool call."
@spec deny_all(term()) :: authorizer()
def deny_all(reason \\ :tools_disabled),
do: fn _module, _params, _context -> {:deny, reason} end
@doc "Allow only Actions whose tool name is in `names`; deny the rest."
@spec allowlist([String.t()]) :: authorizer()
def allowlist(names) when is_list(names) do
set = MapSet.new(names)
fn module, _params, _context ->
if action_name(module) in set, do: :ok, else: {:deny, :not_in_allowlist}
end
end
@doc "Deny Actions whose tool name is in `names`; allow the rest."
@spec denylist([String.t()]) :: authorizer()
def denylist(names) when is_list(names) do
set = MapSet.new(names)
fn module, _params, _context ->
if action_name(module) in set, do: {:deny, :denied}, else: :ok
end
end
@doc """
Deny Actions whose metadata marks them `sensitive: true` (fund-movers like
the payment Actions); allow the rest. This is the default policy applied when
no `:tool_authorizer` is set, so a prompt-injected LLM cannot drive a
sensitive tool unless the agent explicitly opts in (e.g. with `allow_all/0`).
"""
@spec deny_sensitive() :: authorizer()
def deny_sensitive do
fn module, _params, _context ->
if sensitive?(module), do: {:deny, :sensitive_tool}, else: :ok
end
end
@doc "Combine authorizers: the first `{:deny, _}` wins; otherwise `:ok`."
@spec all([authorizer()]) :: authorizer()
def all(authorizers) when is_list(authorizers) do
fn module, params, context ->
Enum.reduce_while(authorizers, :ok, fn auth, :ok ->
case auth.(module, params, context) do
:ok -> {:cont, :ok}
{:deny, _reason} = denied -> {:halt, denied}
end
end)
end
end
defp action_name(module), do: module.__action_meta__().name
defp sensitive?(module),
do: Map.get(module.__action_meta__(), :sensitive, false) == true
end