Permissions
The SDK gives you fine-grained control over which tools the agent can execute. Using the can_use_tool callback, your application can approve, deny, or even modify tool inputs before they run.
How It Works
When the agent wants to use a tool and the permission mode requires approval, the SDK calls your can_use_tool function with:
tool_name— the concrete tool being invoked (for example"bash","read_file", or"replace_by_string")tool_input— the arguments the agent passed to the toolctx— aToolPermissionContextwith metadata (call ID, confirmation details)
Your callback returns either PermissionResult.allow() or PermissionResult.deny(reason).
can_use_tool receives individual tool names such as "read_file", "bash", "create_file", or "replace_by_string" — not group names.
Basic Callback
from adal_agent_sdk import (
AdalAgentClient,
AdalAgentOptions,
PermissionResult,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str,
tool_input: dict,
ctx: ToolPermissionContext,
):
# Block dangerous commands
if tool_name == "bash":
command = tool_input.get("command", "")
if "rm -rf" in command:
return PermissionResult.deny("Blocked dangerous shell command")
# Allow everything else
return PermissionResult.allow()
options = AdalAgentOptions(
workspace=".",
permission_mode="default",
can_use_tool=can_use_tool,
)
Modifying Tool Input
You can approve a tool call while changing its arguments:
async def can_use_tool(tool_name, tool_input, ctx):
if tool_name == "bash":
command = tool_input.get("command", "")
# Force quiet mode on pytest
if command.strip() == "pytest":
return PermissionResult.allow(
updated_input={**tool_input, "command": "pytest -q"}
)
# Add timeout to long-running commands
if "npm install" in command:
return PermissionResult.allow(
updated_input={**tool_input, "timeout": 120}
)
return PermissionResult.allow()
Using Context
The ToolPermissionContext provides additional metadata:
async def can_use_tool(tool_name, tool_input, ctx):
print(f"Tool call ID: {ctx.tool_call_id}")
# Check confirmation details (e.g., diff preview for edits)
if ctx.confirmation:
diff = ctx.confirmation.get("diff", "")
if "+++ secret" in diff:
return PermissionResult.deny("Edit touches sensitive file")
return PermissionResult.allow()
Common Patterns
Allowlist Specific Tools
ALLOWED = {"read_file", "grep", "glob"}
async def can_use_tool(tool_name, tool_input, ctx):
if tool_name in ALLOWED:
return PermissionResult.allow()
return PermissionResult.deny(f"{tool_name} not in allowlist")
Block File Writes Outside a Directory
import os
SAFE_DIR = "/tmp/sandbox"
async def can_use_tool(tool_name, tool_input, ctx):
if tool_name in ("create_file", "replace_by_string", "rewrite_file"):
path = tool_input.get("file_path", "")
abs_path = os.path.abspath(path)
if not abs_path.startswith(SAFE_DIR):
return PermissionResult.deny(f"Writes only allowed in {SAFE_DIR}")
return PermissionResult.allow()
Log All Tool Calls
import logging
logger = logging.getLogger("adal_audit")
async def can_use_tool(tool_name, tool_input, ctx):
logger.info(f"Tool: {tool_name}, Input: {tool_input}, ID: {ctx.tool_call_id}")
return PermissionResult.allow()
Interactive Approval (Terminal)
async def can_use_tool(tool_name, tool_input, ctx):
print(f"\n⚠️ Agent wants to run: {tool_name}")
print(f" Input: {tool_input}")
answer = input(" Allow? [y/N]: ").strip().lower()
if answer == "y":
return PermissionResult.allow()
return PermissionResult.deny("User denied")
Permission Modes
The permission_mode option controls when can_use_tool is called:
| Mode | Behavior |
|---|---|
"default" | All tool calls trigger can_use_tool |
"acceptEdits" | Read-only tools auto-approved; writes trigger callback |
"yolo" | All tools auto-approved (callback never called) |
Use "yolo" for fully automated pipelines where you trust the prompt. Use "default" with a callback for production systems where you need an audit trail or safety guardrails.
Error Handling
If your callback raises an exception, the SDK automatically denies the tool call with the exception message:
async def can_use_tool(tool_name, tool_input, ctx):
# If this raises, the tool is denied with the error message
validate_input(tool_input)
return PermissionResult.allow()
Without a Callback
If permission_mode="default" is set but no can_use_tool callback is provided, all tool calls are denied by default. Always provide a callback when using "default" mode.