# Permissions & budgets

Enforce tool policies and bound resource use.

## 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.

```python
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.

```python
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](../durable-agent-runs/#approvals-and-uncertain-tools).

## Guardrails

```python
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](../migrating/#configuration).

