Permissions¶
Learn: Ask before a tool runs. Guide: Write permission rules.
Import these from alpineagents.permissions. Agent(permissions=[...]) takes a list of them. Before any tool of a
turn runs, use_tools asks them about every call: first every DenyPermission in list order, then the
AllowPermissions and DecidePermissions in list order. The first verdict decides. A call nobody decides about is
denied, with a PermissionWarning.
Verdicts¶
Allowed
dataclass
¶
Allowed()
The verdict that lets a call run. Returned by an AllowPermission or a DecidePermission.
Denied
dataclass
¶
Denied(reason: str, stop: bool = False)
The verdict that refuses a call. Returned by a DenyPermission or a DecidePermission.
The call does not run, and the model gets reason as its error result.
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
|
Example
return Denied("Deleting files is not allowed")
return Denied("The user declined this call. Wait for their next message.", stop=True)
stop
class-attribute
instance-attribute
¶
stop: bool = False
Also stop the turn: the other calls of the turn are cancelled (not run, not asked about), and
state.stopped becomes StoppedByPermission(call, permission) right away, so the loop stops before its next
turn. The run ends normally; state.add_message(Message.user(...)) and run again continue it. A @loop stops by
itself; a loop written without @loop checks state.stopped is None before each turn.
Kinds of permission¶
Permission
¶
Decides whether a tool call may run. Subclass one of DenyPermission, AllowPermission or
DecidePermission (not this class), and implement check, or async acheck, or both.
check(state, call, tool) returns a verdict (Allowed() or Denied(...)) or None for no opinion,
which lets the next permission decide. What a permission may return depends on its kind.
call.argsare the model's arguments, parsed from JSON but not yet checked against the tool's input schema.toolis always aTool(calls to unknown tools, or with broken JSON arguments, are input errors before permissions are asked).- Raising
ToolError(message)refuses the call: the model getsmessageas its error result, and the run continues. Any other exception propagates out ofuse_tools, like one raised by a tool. A failure never counts as allowed.
repr() of a permission names it in ToolOutcome.decided_by and StoppedByPermission.permission, so
give a permission with settings a __repr__ that shows them.
Raises:
| Type | Description |
|---|---|
TypeError
|
When creating an instance of a class that subclasses |
Example
class DenyOutsideProject(DenyPermission):
def check(self, state, call, tool):
path = call.args.get("path")
if isinstance(path, str) and not path.startswith("project/"):
return Denied(f"{path} is outside the project folder")
return None
check
¶
Decides about one call: a verdict, or None to let the next permission decide.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
State
|
The State of the run. |
required |
call
|
ToolCall
|
The tool call. |
required |
tool
|
Tool
|
The tool the call would run. |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
The permission implements only |
DenyPermission
¶
Bases: Permission
A permission that may only deny: check returns Denied(...) or None.
Deny permissions are asked first, wherever they are in the list, so no allow rule can let a denied call through.
AllowPermission
¶
DecidePermission
¶
Bases: Permission
A permission that may allow or deny: check returns Allowed(), Denied(...) or None.
Asked in list order with the allow permissions, after every deny permission.
Built-in permissions¶
DenyByName
¶
DenyByName(patterns: Iterable[str], reason: str | None = None)
Bases: DenyPermission
Denies calls whose tool name matches one of patterns.
A pattern is an fnmatch glob on the name the model calls (call.name), so an MCP tool is
{server}__{tool}: "github__*" denies every tool of the github server.
Example
Agent(model=..., tools=[...], permissions=[DenyByName(["delete_*"]), AllowByDefault()])
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
patterns
|
Iterable[str]
|
Tool names or glob patterns. |
required |
reason
|
str | None
|
What the model is told. Defaults to |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
|
AllowByName
¶
AllowByName(patterns: Iterable[str])
Bases: AllowPermission
Allows calls whose tool name matches one of patterns (fnmatch globs on call.name, so an MCP tool
is {server}__{tool}).
Example
permissions=[AllowByName(["read_file", "github__get_*"]), DecideByHuman()]
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
patterns
|
Iterable[str]
|
Tool names or glob patterns. |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
AllowByReadOnly
¶
AllowByReadOnly(trust_mcp: bool = False)
Bases: AllowPermission
Allows a call when the tool says this call is read-only: tool.hints_for(call.args).read_only.
An MCP server's tools describe themselves, so they are not allowed this way unless trust_mcp=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trust_mcp
|
bool
|
Also believe MCP servers' |
False
|
Raises:
| Type | Description |
|---|---|
TypeError
|
|
DecideByHuman
¶
DecideByHuman(human: Human | None = None)
Bases: DecidePermission
Asks the person whether each call may run. yes allows it; no denies it and stops the turn
(Denied(..., stop=True)), so the person can say what to do instead.
The question goes to the human of the Agent that wrote the permission (the terminal by default), also when
the call it checks is made inside a subagent. It is recorded in the state.history of the State that made the
call, like agent.ask_human. Put it after the permissions that decide without asking, so the person is asked
only about the calls they left open.
Example
permissions=[DenyByName(["delete_*"]), AllowByReadOnly(), DecideByHuman()]
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
human
|
Human | None
|
Who to ask instead of the |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
|
human
instance-attribute
¶
human = human
The Human asked instead of the human of the Agent that wrote the permission, or None for that.
question
¶
The question to ask about call. Override it to ask differently.
Returns:
| Type | Description |
|---|---|
str
|
By default |
check
¶
Asks the person question(call, tool) and allows the call on yes. Any other answer denies it
with stop=True. Raises ValueError if the State has no running Agent.