# aisuite Security Best Practices: Using Tool Policies and Approval Workflows

> Discover aisuite security best practices for enforcing allowlists requiring human approval and auditing tool invocations with flexible tool policies and approval workflows.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: best-practices
- Published: 2026-08-04

---

**aisuite provides a flexible tool-policy framework in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) that allows you to enforce allowlists, require human approval for high-risk operations, and audit every tool invocation through the execution engine in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py).**

The andrewyng/aisuite library enables powerful LLM-driven agents, but exposing tools to language models requires robust security guardrails. By implementing **aisuite security tool policies and approval workflows**, you can control exactly which functions agents invoke, under what conditions, and with full observability into every decision.

## How aisuite Tool Policies Work

The policy enforcement engine operates through four distinct phases implemented across [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) and [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py):

1. **Policy Definition**: You instantiate a policy class such as `AllowToolsPolicy` or `RequireApprovalPolicy` that implements the `evaluate(context)` method, returning a `ToolPolicyDecision` object. The `AllowToolsPolicy` class checks tool names against an explicit allowlist (defined in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) at lines 30-41).

2. **Context Building**: When a tool call is imminent, the system constructs a `ToolPolicyContext` containing the agent name, tool name, arguments, trace identifiers, and metadata. This occurs within the `_prepare_tool_call` method in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) (lines 448-464).

3. **Policy Evaluation**: The engine delegates to `_evaluate_tool_policy` (lines 399-408 in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py)), which invokes your policy's `evaluate` method. The resulting `ToolPolicyDecision` indicates whether the call is `allowed=True/False`, along with an optional reason and metadata.

4. **Enforcement and Auditing**: If denied, the tool call short-circuits immediately, returning `"Tool call denied by policy"` to the LLM (handled in `_prepare_tool_call` at lines 506-531). All decisions are appended to `self.last_policy_events` (lines 490-499) and emitted as trace events (`tool.allowed`, `tool.denied`) for complete observability.

## Implementing Security Policies in aisuite

### Using Allowlists with AllowToolsPolicy

The safest default posture is explicit allowlisting. The `AllowToolsPolicy` class in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) permits only specified tools while denying all others.

```python
import aisuite as ai

# Restrict agent to read-only operations

allow_policy = ai.AllowToolsPolicy(
    allowed_tools=["read_file", "list_dir"],
    reason="Only read-only tools are allowed."
)

result = ai.Runner.run_sync(
    agent,
    prompt="List the files in /tmp and read the first one.",
    tool_policy=allow_policy,
)

```

### Requiring Human Approval

For high-risk operations, implement human-in-the-loop controls using `RequireApprovalPolicy`. This class accepts a callback function that receives the `ToolPolicyContext` and returns a `ToolPolicyDecision`.

```python
import aisuite as ai

def approval_callback(context: ai.ToolPolicyContext):
    # Auto-approve safe read operations

    if context.tool_name.startswith("read_"):
        return ai.ToolPolicyDecision(allowed=True)
    
    # Require manual review for destructive operations

    return ai.ToolPolicyDecision(
        allowed=False,
        reason="Manual approval required for this tool."
    )

approval_policy = ai.RequireApprovalPolicy(callback=approval_callback)

response = ai.Runner.run_sync(
    agent,
    prompt="Delete the temporary cache directory.",
    tool_policy=approval_policy,
)

```

The `RequireApprovalPolicy` implementation resides in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) at lines 43-61.

### Chaining Multiple Policies

For defense in depth, combine policies by creating a composite wrapper that evaluates each policy sequentially, denying immediately on the first rejection:

```python
class CompositePolicy:
    def __init__(self, *policies):
        self.policies = policies

    def evaluate(self, ctx: ai.ToolPolicyContext):
        for p in self.policies:
            decision = p.evaluate(ctx)
            if not decision.allowed:
                return decision
        return ai.ToolPolicyDecision(allowed=True)

# Layer allowlist with human approval

policy = CompositePolicy(
    ai.AllowToolsPolicy(["read_file", "list_dir"]),
    ai.RequireApprovalPolicy(approval_callback)
)

result = ai.Runner.run_sync(
    agent,
    "Read /etc/passwd and list its directory.",
    tool_policy=policy,
)

```

### Monitoring Policy Events

After execution, inspect `response.tool_policy_events` to audit decisions. The execution engine populates this list in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) (lines 490-499) by appending results to `self.last_policy_events`.

```python
client = ai.Client()
response = client.run("Summarize the contents of /var/log/syslog.")

for ev in response.tool_policy_events:
    print(f"{ev['tool_name']}: allowed={ev['allowed']} (reason={ev.get('reason')})")

```

## Security Best Practices for aisuite Tool Policies

Follow these patterns to secure production deployments:

- **Default-deny posture**: Always start with `AllowToolsPolicy` containing an explicit list of safe tools. Never deploy with `AllowAllToolPolicy`, which bypasses all security checks and is defined in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py).

- **Risk-based approval tiers**: Use `RequireApprovalPolicy` for destructive operations (write, delete, execute) while allowing safe read-only tools automatically.

- **Immutable audit trails**: Export `tool_policy_events` to persistent storage. The engine emits structured trace events (`tool.allowed`, `tool.denied`) that capture the full decision context including agent name, tool name, and justification.

- **Runtime policy swapping**: The architecture supports switching policies between invocations, enabling strict deny-all modes in production and relaxed constraints in development environments.

## Summary

- aisuite enforces tool security through the policy framework in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) and the execution engine in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py).
- **AllowToolsPolicy** implements explicit allowlisting by checking tool names against permitted lists in the `evaluate()` method.
- **RequireApprovalPolicy** enables human-in-the-loop workflows through customizable callback functions that receive full `ToolPolicyContext` data.
- The `_evaluate_tool_policy` function in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) orchestrates evaluation, while `_prepare_tool_call` handles context creation and denial enforcement.
- Always inspect `last_policy_events` or `response.tool_policy_events` to maintain audit trails of all tool invocation decisions.

## Frequently Asked Questions

### What is the default behavior if no tool policy is specified in aisuite?

If you do not provide a `tool_policy` parameter, aisuite may default to permissive settings depending on the runner configuration. You should explicitly set `AllowToolsPolicy` with a restricted allowlist in production to prevent unauthorized tool access, as the `AllowAllToolPolicy` class bypasses all security checks.

### How does aisuite handle policy violations during tool execution?

When a policy's `evaluate()` method returns `allowed=False`, the `_prepare_tool_call` method in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) immediately short-circuits the tool invocation. It returns a structured error payload with the message "Tool call denied by policy" to the LLM and logs a `tool.denied` trace event without executing the underlying function.

### Can I implement custom policy logic beyond allowlists and approval workflows?

Yes. The policy system requires only that your class implements an `evaluate(context)` method accepting a `ToolPolicyContext` and returning a `ToolPolicyDecision`. You can integrate external authorization services, time-based restrictions, or attribute-based access control by subclassing the base policy interface defined in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py).

### Where are policy decisions logged for compliance auditing?

All policy evaluations are stored in the `last_policy_events` list within the tool execution engine ([`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py), lines 490-499). These events are attached to the response object as `tool_policy_events`, containing the tool name, decision boolean, reason string, and metadata for every invocation attempt.