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. PermissionLevel is a str enum: PermissionLevel.ASK == "ask", and it serialises with json.dumps directly.

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

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 explicitly use Agent(permissions=CliPermissionManager()) with CliPermissionManager imported from harnessx; this opt-in manager defaults to ASK. Durable runtimes persist approval requests instead: 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.

Guardrails

from harnessx import Agent, AgentConfig, Limits

agent = Agent(config=AgentConfig(
    limits=Limits(
        max_iterations=20,
        max_context_tokens=100_000,
        max_cost_dollars=0.50,
        input_cost_per_m=3.00,   # application-supplied prices, not a live price lookup
        output_cost_per_m=15.00,
    ),
))
agent.guardrails.usage_summary

Limits is a frozen dataclass validated at construction; an invalid value raises ConfigurationError. A reached guardrail returns a failed RunResult with error details. Limits(max_iterations=0) means unlimited. Transient model failures are governed separately by AgentConfig(retry=RetryPolicy(...)). The flat 0.3 fields (AgentConfig(max_iterations=20), ...) still work but warn on every use and are removed in 0.5. See Migrating to 0.4.