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/sandbox_hook.ex
defmodule Raxol.Agent.SandboxHook do
@moduledoc """
`Raxol.Agent.CommandHook` implementation that consults an agent's
declared `sandbox/0` list before allowing a directive to fire.
## How it works
The hook reads the agent's sandbox list via `context.agent_module`
at pre-execute time -- callbacks defined on the agent module are
the source of truth. For each directive type it can authorize
(`Async`, `Shell`, `SendAgent`), it builds the action atom and
payload shape expected by `Raxol.Agent.Sandbox.Chain.authorize/4`
and walks the chain.
## Wiring
Consumers prepend this module to the agent's `command_hooks/0`
list at startup. The `Raxol.Agent.effective_hooks/1` helper does
the prepending automatically:
hooks = Raxol.Agent.effective_hooks(MyAgent)
Raxol.Agent.Session.start_link(
id: :my_agent,
app_module: MyAgent,
hooks: hooks
)
When the agent declares no `sandbox/0` (returns `[]`), the chain
walk is trivially `:ok` and the hook is a no-op pass-through.
## Telemetry
On deny, emits `[:raxol, :agent, :sandbox, :denied]` with
metadata `%{agent_id, agent_module, action, reason}` so
`Raxol.Agent.ThreadLogRouter` (or any other handler) can route
the denial as durable audit.
"""
@behaviour Raxol.Agent.CommandHook
alias Raxol.Agent.Directive.{Async, SendAgent, Shell}
alias Raxol.Agent.Sandbox.Chain
@impl true
def pre_execute(%Shell{command: command, opts: opts}, context) do
decide(
:shell,
%{command: command, opts: opts},
%Shell{command: command, opts: opts},
context
)
end
def pre_execute(%SendAgent{target_id: target_id, message: message}, context) do
decide(
:send_agent,
%{target_id: target_id, message: message},
%SendAgent{target_id: target_id, message: message},
context
)
end
def pre_execute(%Async{fun: fun}, context) do
decide(:async, %{fun: fun}, %Async{fun: fun}, context)
end
def pre_execute(other, _context), do: {:ok, other}
defp decide(action, payload, command, context) do
sandboxes = fetch_sandboxes(context)
case Chain.authorize(sandboxes, action, payload, sandbox_ctx(context)) do
:ok ->
{:ok, command}
{:deny, reason} = denied ->
emit_denied(context, action, reason)
denied
end
end
defp fetch_sandboxes(%{agent_module: nil}), do: []
defp fetch_sandboxes(%{agent_module: module}) when is_atom(module) do
if function_exported?(module, :sandbox, 0) do
module.sandbox() || []
else
[]
end
end
defp fetch_sandboxes(_), do: []
defp sandbox_ctx(%{agent_id: agent_id, agent_module: agent_module}) do
%{agent_id: agent_id, agent_module: agent_module}
end
defp sandbox_ctx(_), do: %{}
defp emit_denied(context, action, reason) do
:telemetry.execute(
[:raxol, :agent, :sandbox, :denied],
%{},
%{
agent_id: Map.get(context, :agent_id),
agent_module: Map.get(context, :agent_module),
action: action,
reason: reason
}
)
end
end