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.