Permission levels
Tools use ALLOW (execute), ASK (request approval), or DENY (reject).
Omitting the permission inherits the manager's default, which is ALLOW.
This applies to constructor tools, later registrations, and direct
ToolRegistry.execute() calls without a custom manager. Built-in helpers also
inherit the manager default, so file writes, shell commands, and URL fetching
use ALLOW under a default agent. Select ASK explicitly when approval is
required. Explicit MCP server policies remain in effect. Per-tool overrides take
precedence over explicit registration permissions and the manager default.
DENY also overrides a previous session grant.
from harnessx import PermissionLevel
# At registration
@agent.tools.register(permission=PermissionLevel.ALLOW)
def add(a: int, b: int) -> int: ...
# Or override later
agent.permissions.set_permission("run_bash", PermissionLevel.ASK)
agent.permissions.grant_session("read_file") # pre-approve for this session
Approval callbacks
Direct execution handles ASK through a sync or async callback. Without a
callback it denies the call; the SDK never implicitly reads stdin.
from harnessx import Agent, PermissionLevel, PermissionManager, ToolCall, ToolDefinition
async def approve(call: ToolCall, definition: ToolDefinition) -> bool:
return await approval_ui.request(call.name, call.input)
agent = Agent(permissions=PermissionManager(
default_level=PermissionLevel.ASK,
approval_callback=approve,
))
Setting default_level=PermissionLevel.ASK requests approval for tools without
an explicit permission. Supplying only approval_callback leaves the default
at ALLOW; the callback handles tools explicitly configured as ASK.
Terminal apps can use CliPermissionManager, an opt-in manager that defaults to
ASK and prompts on the console:
from harnessx import Agent, CliPermissionManager
agent = Agent(tools=["bash"], permissions=CliPermissionManager())
Durable approvals
Durable runtimes persist approval requests instead of calling back. The run
returns needs_input, and await runtime.approve(pending, resume=True) or
await runtime.decline(pending, resume=True) records the decision and finishes
the run. A declined tool sees the result "Permission denied". See
Durable agent runs.
Budgets
AgentConfig(limits=Limits(...)) bounds iterations, context, and cost. A
reached cost limit returns a failed RunResult with error details. A reached
iteration limit asks the model for a final answer and completes with
result.limited true, unless final_answer_on_limit=False makes it fail.
agent.guardrails.usage_summary reports accumulated usage, including what
delegated specialists spent. Agents & the
loop lists every Limits field and its default. Transient model
failures are governed separately; see Retries & fallback
models.