Packages
nous
0.15.3
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/permissions.ex
defmodule Nous.Permissions do
# blocked?/2 / requires_approval?/2 use MapSet.member? on the deny/allow
# set fields of %Policy{}; capture-syntax false positive for opacity.
@dialyzer :no_opaque
@moduledoc """
Tool-level permission policy engine.
Controls which tools can be used, which require user approval,
and which are denied entirely. This operates at the tool level —
for message-level input filtering, see `Nous.Plugins.InputGuard`.
## Quick Start
# Use a preset policy
policy = Nous.Permissions.default_policy()
# Check tool access
Nous.Permissions.blocked?(policy, "bash")
#=> false
Nous.Permissions.requires_approval?(policy, "bash")
#=> true
Nous.Permissions.requires_approval?(policy, "file_read")
#=> false
# Filter a tool list
tools = [bash_tool, read_tool, write_tool]
allowed = Nous.Permissions.filter_tools(policy, tools)
## Custom Policies
policy = %Nous.Permissions.Policy{
mode: :default,
deny_names: MapSet.new(["bash"]),
deny_prefixes: ["web_"],
approval_required: MapSet.new(["file_write", "file_edit"])
}
"""
alias Nous.Permissions.Policy
@doc """
Returns the default permission policy.
Read and search tools are open. Write and execute tools require approval.
"""
@spec default_policy() :: Policy.t()
def default_policy do
%Policy{
mode: :default,
approval_required: MapSet.new(["bash", "file_write", "file_edit"]),
deny_names: MapSet.new(),
deny_prefixes: []
}
end
@doc """
Returns a permissive policy where all tools are open.
"""
@spec permissive_policy() :: Policy.t()
def permissive_policy do
%Policy{mode: :permissive}
end
@doc """
Returns a strict policy where all tools require approval.
"""
@spec strict_policy() :: Policy.t()
def strict_policy do
%Policy{mode: :strict}
end
@doc """
Creates a policy from keyword options.
## Options
* `:mode` — `:default`, `:permissive`, or `:strict`
* `:deny` — list of tool names to block
* `:deny_prefixes` — list of prefixes to block (e.g. `["web_"]`)
* `:approval_required` — list of tool names requiring approval
## Examples
policy = Nous.Permissions.build_policy(
mode: :default,
deny: ["bash"],
deny_prefixes: ["web_"],
approval_required: ["file_write"]
)
"""
@spec build_policy(keyword()) :: Policy.t()
def build_policy(opts \\ []) do
%Policy{
mode: Keyword.get(opts, :mode, :default),
deny_names: opts |> Keyword.get(:deny, []) |> Enum.map(&String.downcase/1) |> MapSet.new(),
deny_prefixes: opts |> Keyword.get(:deny_prefixes, []) |> Enum.map(&String.downcase/1),
allow_names:
opts |> Keyword.get(:allow, []) |> Enum.map(&String.downcase/1) |> MapSet.new(),
allow_prefixes: opts |> Keyword.get(:allow_prefixes, []) |> Enum.map(&String.downcase/1),
approval_required:
opts
|> Keyword.get(:approval_required, [])
|> Enum.map(&String.downcase/1)
|> MapSet.new()
}
end
@doc """
Checks if a tool name is blocked by the policy.
Matches against deny_names (exact, case-insensitive) and
deny_prefixes (prefix match, case-insensitive).
## Examples
iex> policy = %Nous.Permissions.Policy{deny_names: MapSet.new(["bash"]), deny_prefixes: ["web_"]}
iex> Nous.Permissions.blocked?(policy, "bash")
true
iex> Nous.Permissions.blocked?(policy, "web_fetch")
true
iex> Nous.Permissions.blocked?(policy, "file_read")
false
"""
@spec blocked?(Policy.t(), String.t()) :: boolean()
def blocked?(
%Policy{
mode: mode,
deny_names: deny_names,
deny_prefixes: deny_prefixes,
allow_names: allow_names,
allow_prefixes: allow_prefixes
},
tool_name
) do
name = String.downcase(tool_name)
cond do
MapSet.member?(deny_names, name) ->
true
Enum.any?(deny_prefixes, &String.starts_with?(name, String.downcase(&1))) ->
true
mode == :strict ->
# Deny-by-default in strict mode: only tools explicitly allowed
# via :allow_names / :allow_prefixes pass the filter. This closes
# the gap where strict_policy() with empty deny lists silently
# exposed every tool at the filter layer.
not (MapSet.member?(allow_names, name) or
Enum.any?(allow_prefixes, &String.starts_with?(name, String.downcase(&1))))
true ->
false
end
end
@doc """
Checks if a tool requires approval under the policy.
## Examples
iex> policy = Nous.Permissions.strict_policy()
iex> Nous.Permissions.requires_approval?(policy, "file_read")
true
iex> policy = Nous.Permissions.permissive_policy()
iex> Nous.Permissions.requires_approval?(policy, "bash")
false
"""
@spec requires_approval?(Policy.t(), String.t()) :: boolean()
def requires_approval?(%Policy{mode: :permissive}, _tool_name), do: false
def requires_approval?(%Policy{mode: :strict}, _tool_name), do: true
def requires_approval?(%Policy{mode: :default, approval_required: required}, tool_name) do
MapSet.member?(required, String.downcase(tool_name))
end
@doc """
Filters a list of `Nous.Tool` structs, removing blocked tools.
## Examples
tools = [bash_tool, read_tool, write_tool]
policy = Nous.Permissions.build_policy(deny: ["bash"])
filtered = Nous.Permissions.filter_tools(policy, tools)
# Returns [read_tool, write_tool]
"""
@spec filter_tools(Policy.t(), [Nous.Tool.t()]) :: [Nous.Tool.t()]
def filter_tools(%Policy{} = policy, tools) when is_list(tools) do
Enum.reject(tools, fn tool ->
blocked?(policy, tool.name)
end)
end
@doc """
Partitions tools into {allowed, blocked} based on the policy.
## Examples
{allowed, denied} = Nous.Permissions.partition_tools(policy, tools)
"""
@spec partition_tools(Policy.t(), [Nous.Tool.t()]) :: {[Nous.Tool.t()], [Nous.Tool.t()]}
def partition_tools(%Policy{} = policy, tools) when is_list(tools) do
Enum.split_with(tools, fn tool ->
not blocked?(policy, tool.name)
end)
end
end