Skip to content

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

reason is not a string, or stop is not a bool.

ValueError

reason is empty or whitespace only.

Example
return Denied("Deleting files is not allowed")
return Denied("The user declined this call. Wait for their next message.", stop=True)

reason instance-attribute

reason: str

What the model is told.

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.args are the model's arguments, parsed from JSON but not yet checked against the tool's input schema.
  • tool is always a Tool (calls to unknown tools, or with broken JSON arguments, are input errors before permissions are asked).
  • Raising ToolError(message) refuses the call: the model gets message as its error result, and the run continues. Any other exception propagates out of use_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 Permission directly or more than one kind, or that implements neither check nor acheck.

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

check(state: State, call: ToolCall, tool: Tool) -> Allowed | Denied | None

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. call.args are not yet checked against the tool's input schema.

required
tool Tool

The tool the call would run.

required

Raises:

Type Description
TypeError

The permission implements only acheck. Run the Agent with arun.

acheck async

acheck(state: State, call: ToolCall, tool: Tool) -> Allowed | Denied | None

The async version of check, used by arun and ause_tools. By default it runs check on a worker thread, so a blocking check does not block the event loop.

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

Bases: Permission

A permission that may only allow: check returns Allowed() or None.

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 "{call.name} is not allowed.".

None

Raises:

Type Description
TypeError

patterns is not a list of strings, or reason is not a string.

ValueError

reason is empty or whitespace only.

patterns instance-attribute

patterns = _patterns(patterns, 'DenyByName')

The tool names or fnmatch globs, as a tuple.

reason instance-attribute

reason = reason

What the model is told, or None for "{call.name} is not allowed.".

check

check(state: State, call: ToolCall, tool: Tool) -> Denied | None

Refuses a call whose name matches one of patterns, with reason. Has no opinion otherwise.

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

patterns is not a list of strings.

patterns instance-attribute

patterns = _patterns(patterns, 'AllowByName')

The tool names or fnmatch globs, as a tuple.

check

check(state: State, call: ToolCall, tool: Tool) -> Allowed | None

Allows a call whose name matches one of patterns. Has no opinion otherwise.

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' readOnlyHint.

False

Raises:

Type Description
TypeError

trust_mcp is not a bool.

trust_mcp instance-attribute

trust_mcp = trust_mcp

Whether MCP servers' readOnlyHint is believed.

check

check(state: State, call: ToolCall, tool: Tool) -> Allowed | None

Allows a call when tool.hints_for(call.args).read_only is true. Has no opinion otherwise, and always for an MCPTool unless trust_mcp is true.

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 human of the Agent that wrote the permission, for example a chat app for approvals.

None

Raises:

Type Description
TypeError

human is not a Human object.

human instance-attribute

human = human

The Human asked instead of the human of the Agent that wrote the permission, or None for that.

question

question(call: ToolCall, tool: Tool) -> str

The question to ask about call. Override it to ask differently.

Returns:

Type Description
str

By default Run {call}?, e.g. Run write_file(path="a.md", content="x")?.

check

check(state: State, call: ToolCall, tool: Tool) -> Allowed | Denied | None

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.

acheck async

acheck(state: State, call: ToolCall, tool: Tool) -> Allowed | Denied | None

The async version of check: asks the Human with aask (or ask on a worker thread).

AllowByDefault

Bases: AllowPermission

Allows every call. Put it last in the list to allow everything no permission before it denied.

check

check(state: State, call: ToolCall, tool: Tool) -> Allowed

Allows the call.