# Tools

Register functions and configured built-ins with consistent permissions, schemas, and execution policies.

A tool has two parts: a Python handler that performs an operation, and a registered
name, description, input schema, and execution policy that the agent uses to call
it. Registration makes a handler available; it does not execute it.

## Choose a registration style

| Style | Example | Best use |
| --- | --- | --- |
| Constructor function | `Agent(tools=[fetch_url])` | A small, explicit list of handlers |
| Constructor built-in name | `Agent(tools=["fetch_url"])` | Load a shipped tool without importing its handler |
| Constructor bundle | `Agent(tools=["web"])` | Load a built-in group |
| Later registration | `agent.tools.register_tool(fetch_url)` | Assemble tools after creating the agent |
| Decorator | `@agent.tools.register(...)` | Define a custom handler beside its registration |
| Built-in helper | `register_web_tools(agent.tools, ...)` | Configure and select tools in a bundle |
| Registry loading | `registry.load_builtin("web", ...)` | The same built-in helper options through one entry point |
| Tool definition | `Agent(tools=[definition])` | Reuse an explicit schema and execution policy |

All of these inherit the agent's permission policy when permission is omitted.
A normal `Agent()` defaults to **ALLOW**. A custom manager can instead default to
ASK or DENY. A preconfigured `ToolDefinition` retains its explicit permission.
Registration style does not choose the permission default.

Prefer functions for simple registration and helpers for configured capabilities.
Bundle names currently include `filesystem`, `bash`, `web`, `memory`, and `all`;
a bundle can contain more tools than one standalone function. The `Skill` tool
and the knowledge tools (`search_concepts`, `read_concept`, `get_neighbors`) are
registered by `Agent(skills=...)` and `Agent(knowledge=...)`, not by a bundle name;
see [Skills](../skills/) and [Knowledge (OKF)](../knowledge/).

## Constructor registration and registration afterward

```python
from harnessx import Agent
from harnessx.builtin.web import fetch_url

agent = Agent(tools=[fetch_url])
```

The constructor infers the tool name, schema, and description from the function.
You can also register afterward, before starting the run:

```python
from harnessx import Agent
from harnessx.builtin.web import fetch_url

agent = Agent()
agent.tools.register_tool(fetch_url)
```

Both expose `fetch_url` to subsequent model calls. Finish changing bindings before
starting a run; mutating a registry during execution can invalidate an in-flight
call. Use `async with agent:` or `await agent.aclose()` to close an agent when done.

A constructor list may mix functions, built-in names, and `ToolDefinition` objects.
Passing an existing `ToolRegistry` attaches that same registry to an ordinary
agent; it does not clone the registry. Declare subagent tools separately: each
specialist task receives a fresh registry with copied schemas and metadata.

## Direct function versus a registration helper

These imports serve different purposes:

```python
from harnessx.builtin.web import fetch_url           # operation
from harnessx.builtin.web import register_web_tools  # registration/configuration
```

`fetch_url` is an async Python function. `register_web_tools` installs the web
bundle into a supplied registry and returns the list of registered names.
The web bundle currently contains `fetch_url`.

```python
from harnessx import Agent
from harnessx.builtin.web import register_web_tools

agent = Agent()
registered = register_web_tools(
    agent.tools,
    include=["fetch_url"],
    max_length=20_000,
)
assert registered == ["fetch_url"]
```

This uses the same HTTP-fetch implementation, with configured response truncation.
With a non-default `max_length`, the helper creates a wrapper whose model input is
only `url`; the model cannot override that configured limit. Direct `fetch_url`
registration exposes its `url` and optional `max_length` function parameters.
With the helper's default `max_length=500_000`, it currently registers the direct
function, so that parameter remains visible. Truncation limits the returned text,
not bytes downloaded from the network.

Both approaches inherit permissions. Configuration can still change the handler
and schema; equivalent permissions do not imply identical capabilities.

Do not pass `register_web_tools` as an agent tool: its argument is a registry, not
an input for the model. Call the helper during application setup.

## Permission defaults and precedence

| Registration | Ordinary agent | Manager default ASK | Manager default DENY |
| --- | --- | --- | --- |
| Function, name, bundle, or helper; permission omitted | ALLOW | ASK | DENY |
| Explicit `permission=ALLOW` | ALLOW | ALLOW | ALLOW |
| Explicit `permission=ASK` | ASK | ASK | ASK |
| Explicit `permission=DENY` | DENY | DENY | DENY |

A **manager override for the tool name takes precedence over every row**. Otherwise,
an explicit tool permission takes precedence over the manager default. After
resolving the effective level, DENY blocks execution even if there is a previous
session grant. A session grant can satisfy ASK without another callback.

For example, register an explicit policy or override it later:

```python
from harnessx import Agent, PermissionLevel
from harnessx.builtin.web import fetch_url

agent = Agent()
agent.tools.register_tool(fetch_url, permission=PermissionLevel.ALLOW)
agent.permissions.set_permission("fetch_url", PermissionLevel.DENY)
```

### ASK needs an approval mechanism

A plain agent does not display a prompt automatically. Direct execution and
ordinary runs deny ASK calls when no approval callback is provided.

For a terminal application:

```python
from harnessx import Agent, CliPermissionManager
from harnessx.builtin.web import fetch_url

agent = Agent(tools=[fetch_url], permissions=CliPermissionManager())
```

`CliPermissionManager` defaults to ASK. That includes built-in tools registered
without an explicit policy. Alternatively, supply a synchronous or asynchronous
callback. This complete callback example approves only one known URL:

```python
from harnessx import Agent, PermissionLevel, PermissionManager, ToolCall, ToolDefinition
from harnessx.builtin.web import fetch_url

async def approve(call: ToolCall, definition: ToolDefinition) -> bool:
    return call.name == "fetch_url" and call.input.get("url") == "https://example.com"

agent = Agent(
    tools=[fetch_url],
    permissions=PermissionManager(
        default_level=PermissionLevel.ASK,
        approval_callback=approve,
    ),
)
```

A UI can implement the same callback by awaiting the user's decision. Providing
only `approval_callback` leaves the default at ALLOW; also select ASK for the
tools that should request approval. Callbacks must return an actual boolean.

Durable `AgentRuntime` runs persist ASK requests and return `awaiting_input`.
Use the returned execution key with `runtime.approve(...)`, then resume the run;
see the recovery documentation. This persisted approval behavior applies to the
outer delegation call, not to the non-durable specialist's internal tools.
MCP tools retain their explicitly configured per-server permission policy.

## Decorators and schema inference

```python
from harnessx import Agent, PermissionLevel

agent = Agent()

@agent.tools.register(permission=PermissionLevel.ALLOW, replay_policy="safe")
def calculate(a: int, b: int) -> int:
    """Multiply two numbers.

    Args:
        a: First number.
        b: Second number.
    """
    return a * b
```

The description before an `Args:` or other recognized section becomes the tool
description, including multiple lines. An `Args:` section documents parameters.
Type hints drive JSON Schema: primitive types, nullable unions, `Literal`, nested
lists, and dictionaries with string keys are supported. Nullable parameters remain
required unless they have a Python default. Unsupported annotations fail during
registration; use `register_with_schema` for a custom contract. `replay_policy`
accepts `ReplayPolicy.SAFE`, `ReplayPolicy.IDEMPOTENT`, `ReplayPolicy.MANUAL`, or
the plain strings; `ToolDefinition.replay_policy` stores the string.

Sync and async handlers both work. Synchronous handlers run outside the event
loop. A timeout stops waiting for a synchronous handler; Python cannot forcibly
stop its worker thread or reverse an external effect.

## Explicit schemas and reusable definitions

Use a schema when type inference cannot express the input contract:

```python
from harnessx import Agent, ToolRegistry

async def label(value: str) -> str:
    return value.upper()

registry = ToolRegistry()
definition = registry.register_with_schema(
    name="label",
    description="Uppercase a short label.",
    input_schema={
        "type": "object",
        "properties": {"value": {"type": "string", "maxLength": 40}},
        "required": ["value"],
        "additionalProperties": False,
    },
    handler=label,
)
agent = Agent(tools=[definition])
```

`name`, `description`, `input_schema`, and `handler` are the positional parameters
of `register_with_schema`; `permission`, `concurrent`, `replay_policy`,
`timeout_seconds`, and `replace` are keyword-only. Registering a `ToolDefinition`
copies its schema and metadata while retaining the handler. Registration options
override only specified metadata; registering an existing definition does not
silently reset its permissions or replay policy.

## Configured built-in tools

Helpers support `include`, `exclude`, `permission`, and `replace`, plus options
specific to the bundle. Unknown names, filters, and unsupported options fail.

For filesystem inspection, configure a root and only the operations you need:

```python
from pathlib import Path
from harnessx import Agent
from harnessx.builtin import register_filesystem_tools

agent = Agent()
register_filesystem_tools(
    agent.tools,
    include=["read_file", "list_directory"],
    base_path=str(Path.cwd()),
    max_read_bytes=1_000_000,
    max_directory_entries=1_000,
)
```

The root must exist. Scoped operations reject symlinks in files and parent
directories using descriptor-relative, no-follow access on POSIX. This is an
application boundary, not an OS sandbox for arbitrary code: hard links, mounts,
and other processes relocating directories require stronger isolation. Without
`base_path`, filesystem tools use host filesystem access under process permissions.

Configured filesystem limits are hidden from model input. Existing files require
`overwrite=True`; writes publish atomically. Writes and downloadable-file
creation remain non-concurrent when registered through filesystem helpers,
regardless of permission. Direct standalone functions use ordinary registration
metadata, so choose `concurrent=False` explicitly if needed.

Download copies use configured `output_dir`, otherwise `AGENT_OUTPUT_DIR` or
`./output`. Keep the output directory application-owned. Filesystem reads and
writes run outside the event loop.

Other options include a sandbox for the bash helper and memory stores for the
memory helper. `Agent(tools=["bash"], sandbox=sandbox)` binds a caller-owned
[sandbox](../sandbox/) as the bash built-in's engine, so the model's shell commands
run inside it; the registry exposes the binding as `agent.tools.sandbox`. Single-tool
loading accepts the corresponding bundle options:

```python
from pathlib import Path
from harnessx import ToolRegistry

registry = ToolRegistry()
registry.load_builtin("read_file", base_path=str(Path.cwd()))
```

## Registry-wide tool policy

`AgentConfig(tools=ToolPolicy(...))` carries registry-wide options to the registry
the agent adopts, including a plain list of functions:

```python
from harnessx import Agent, AgentConfig, ToolPolicy
from harnessx.builtin.web import fetch_url

agent = Agent(
    config=AgentConfig(tools=ToolPolicy(default_timeout_seconds=60, dedupe_calls=True)),
    tools=[fetch_url],
)
assert agent.tools.policy.default_timeout_seconds == 60
```

Precedence, highest first: a value given to the
`ToolRegistry(default_timeout_seconds=, dedupe_calls=)` constructor, the agent's
`config.tools`, then the built-in default (`DEFAULT_TIMEOUT_SECONDS`, 300 seconds,
and no deduplication). Tools registered with their own `timeout_seconds` are never
changed. `registry.policy` shows the effective values. With `dedupe_calls=True`,
identical repeated calls within one run return the first result.

## Replacement, timeouts, and replay

Duplicate tool names fail by default. Use `replace=True` to deliberately replace a
binding during setup. Inspect `get_tool(name)`, `list_tools()`, or
`get_tool_params()`; the last returns canonical provider-neutral tool schemas.

Ordinary function registrations default to `concurrent=True`, a timeout of
`DEFAULT_TIMEOUT_SECONDS` (300 seconds) unless a `ToolPolicy` changes the default,
and `replay_policy="manual"`. Configure these with `register_tool` or the decorator.
Built-in helpers do not automatically mark fetching or other operations replay-safe.

Durable execution uses `manual` to stop for recovery when an interrupted operation's
outcome is unknown. Select `safe` only when repeating an operation is safe, or
`idempotent` when the integration actually enforces idempotency. Neither setting
provides exactly-once external effects on its own.

## Calling a function versus executing an agent tool

`await fetch_url(url)` is an ordinary Python call. It bypasses the registry's
schema validation, permission manager, timeout, and durable execution records.

`await registry.execute(call, permissions=agent.permissions)` validates the input,
checks permission, and applies the registered timeout. Without a manager it uses
the ordinary ALLOW default for unspecified policies; explicit ASK without a
callback and explicit DENY return the error result `"Permission denied"`. It does
not provide durable replay or the agent's complete middleware/event pipeline.

`agent.run(...)`, `agent.run_stream(...)`, and `AgentRuntime` use the shared engine
for model-requested tools. They apply execution checks before dispatch, with
persisted approvals and recovery when using a durable runtime. Handler exceptions
usually become error tool results; runtime/storage errors may fail the run.
