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.