AgentToolPolicy
AgentToolPolicy provides capability masks for AI agent tool calls. It also provides
deterministic pre-dispatch policy. The library has no runtime dependencies. The
caller owns application catalogs, persistence, classification, execution, and audits.
This library is not an execution sandbox, authorization provider, credential store, or model classifier.
Installation
Add the Hex package to mix.exs:
def deps do
[
{:agent_tool_policy, "~> 0.1.0"}
]
end
Or pin the tagged Git source:
def deps do
[
{:agent_tool_policy,
git: "https://github.com/logandonley/agent_tool_policy.git", tag: "v0.1.0"}
]
end
Tool-access masks
The caller supplies the tool universe and a baseline function. A mask narrows each integration or system scope to all tools, live read tools, or an explicit set.
alias AgentToolPolicy.Access
universe = [
%{
kind: :integration,
scope: "mail",
tools: [
%{name: "mail__list", read: true},
%{name: "mail__send", read: false}
]
}
]
baseline = fn _scope -> :all end
stored = [%{"kind" => "integration", "scope" => "mail", "level" => "read"}]
allowed = stored |> Access.parse() |> Access.resolve(universe, baseline)
# => MapSet.new(["mail__list"])
Use the resolved MapSet to filter advertised tool specifications. Use it again to
authorize each dispatch. Filtering advertised tools is not enough. A model or caller
can still propose a hidden tool by name.
resolve/3 fails closed when it encounters malformed stored entries. It does not
require a validation call. An invalid entry denies its named scope. A malformed
non-map entry denies the entire universe.
Write paths should still validate masks. Call AgentToolPolicy.Access.validate/2,
then AgentToolPolicy.Access.normalize/3 and AgentToolPolicy.Access.dump/1 before
persistence.
The library passes the opaque credential_id field through unchanged. It does not
resolve or authorize credentials.
Autonomous-action policy
The caller classifies an action using plain data:
class = %{surface: :outward, reversible: false, time_sensitive: true}
ActionPolicy.decide/1 applies deterministic precedence to the class, caller-owned
posture, allowlist result, and optional guard result:
alias AgentToolPolicy.ActionPolicy
ActionPolicy.decide(%{
class: class,
posture: :safe,
allowlisted?: false
})
# => :gate
Applications select evaluator modules explicitly and pass them to
ActionGuard.evaluate/2:
alias AgentToolPolicy.{ActionGuard, ActionPolicy}
action = %{name: "mail__send", arguments: %{}, class: class, ctx: %{org_id: "org-1"}}
finding = ActionGuard.evaluate(action, [MyApp.OutboundSafetyEvaluator])
guard = if finding.verdict == :block, do: :block, else: :allow
ActionPolicy.decide(%{
class: class,
posture: :auto,
allowlisted?: false,
guard: guard
})
A shadow evaluator's block becomes a flag. Enforced blocks outrank flags. Flags outrank allows. The guard retains the first reason at the winning severity. The caller owns evaluator selection, configuration, failure containment, and evaluator safety.
Responsibility boundary
The caller owns tool catalogs, principal baselines, and credential authorization. The caller defines action classifications and allowlist policy. The caller selects and configures evaluators. The caller also handles evaluator failures. The caller controls persistence, receipts, approvals, and undo execution. The caller provides audit events, telemetry, and dispatch enforcement.
The library does not recover from malformed action-policy input. It does not catch evaluator exceptions. It does not normalize malformed evaluator results. These failures propagate to the caller.
License
MIT License. See the repository's LICENSE file.