# Builtin Tools

PyClaw ships eleven builtin tools (twelve if you count the optional, default-off
`fetch_mcp_resource`). Four are write-class, the rest are read-class. The
distinction matters because Sprint 1's permission tier system uses `tool_class`
to decide what's auto-permitted under `tier=read-only` vs auto-denied. See the
[permissions guide](./permissions.md) for the tier matrix.

## The eleven default tools

| Tool | Class | Default | What it does |
|---|---|---|---|
| `bash` | `write` | on | Executes shell commands in a subprocess. The only sandboxed tool by design. |
| `read` | `read` | on | Reads a file inside the workspace. Supports `offset` / `limit` for line ranges. |
| `write` | `write` | on | Creates or overwrites a file inside the workspace. |
| `edit` | `write` | on | String-replace inside a workspace file (`old_string` → `new_string`). |
| `grep` | `read` | on (Sprint 4') | In-process regex search of workspace files. Returns structured `{path, line_number, line}` matches. |
| `glob` | `read` | on (Sprint 4') | In-process glob matching. Returns workspace-relative paths sorted by mtime descending. |
| `web_fetch` | `read` | on (Sprint 4') | HTTPS GET → markdown via `httpx` + `markdownify`. Schemes whitelisted to https by default. |
| `skill_view` | `read` | when `progressiveDisclosure=true` | Loads full SKILL.md content for a named skill. |
| `memorize` | `memory-write-safe` | when `memory_store` configured | Persists facts (L2) or procedures (L3). |
| `forget` | `write` | when `memory_store` configured | Archives an L3 SOP (destructive). |
| `update_working_memory` | `read` | when `redis_client` configured | Per-session scratchpad in Redis. |

The default-off optional tool:

| Tool | Class | Default | What it does |
|---|---|---|---|
| `fetch_mcp_resource` | `read` | **off** | Reads a named MCP resource (`server` + `uri`) from a connected MCP server. |

## Why three new tools (Sprint 4')

Pre-Sprint-4' PyClaw was below the industry Tier 2 baseline: it had
`bash`/`read`/`write`/`edit` but no in-process search and no built-in URL
fetching. This produced two daily pain points:

- Under `tier=read-only` the agent had no way to search code (`bash` is denied,
  `read` is single-file) — so a "research"-mode session was essentially blind
  beyond a single file at a time.
- Fetching a URL meant falling back to `bash curl + sed/awk`, wasting tokens
  and breaking under read-only.

The 11-agent ecosystem survey ([survey §3.1](../../DailyWork/planning/exploration/2026-05-17-agent-ecosystem-preinstall-survey.md)
— private) confirmed `grep` / `glob` / `web_fetch` are present in 8-10 of 11
surveyed agents. Sprint 4' brings PyClaw to that baseline.

## Sandbox interaction (in-process bypasses srt by design)

Sprint 3's [srt sandbox](./sandbox.md) wraps `bash` and MCP stdio subprocesses.
The new Tier 2 tools (`web_fetch`, `grep`, `glob`, `fetch_mcp_resource`) are
**pure-Python in-process implementations** — no subprocess spawn, no srt wrap.
This is a feature, not a bug:

- Under `tier=read-only`, `bash` is auto-denied (`tool_class="write"`). If
  in-process `grep` were sandboxed too, a read-only research session would
  have no way to search code. The whole point of the read-only tier is "safe
  exploration" — the in-process read tools are what make that possible.
- srt has per-spawn overhead (settings file generation, process startup). For
  trivial searches that takes ~30ms; in-process is ~0.1ms.
- Pure Python `re` and `pathlib.Path.rglob` are deterministic and don't
  depend on a `rg` / `fd` binary that may or may not be on PATH inside the
  sandboxed env.

The boundary for in-process tools is **the workspace + tier classification**,
not srt:

| Concern | Bash / MCP subprocesses | In-process Tier 2 tools |
|---|---|---|
| Filesystem boundary | srt `allowWrite` / `denyRead` | `WorkspaceResolver.resolve_within` |
| Network egress | srt `allowedDomains` / `deniedDomains` | `web_fetch` `allowed_schemes` / `allowed_domains` |
| Tier enforcement | runner `tool_class` lookup | runner `tool_class` lookup |

If you need `grep`/`glob`/`web_fetch` egress controls in production, run PyClaw
inside a NetworkPolicy / egress proxy that enforces them at the host level.

## Configuration

Two new top-level sections in `pyclaw.json`:

```jsonc
{
  "builtinTools": {
    "webFetchEnabled": true,
    "grepEnabled": true,
    "globEnabled": true,
    "fetchMcpResourceEnabled": false
  },
  "webFetch": {
    "timeoutSeconds": 30.0,
    "maxChars": 200000,
    "allowedSchemes": ["https"],
    "allowedDomains": []
  }
}
```

### `builtinTools` fields

| Field | Type | Default | Purpose |
|---|---|---|---|
| `webFetchEnabled` | bool | `true` | Register `web_fetch` at startup. |
| `grepEnabled` | bool | `true` | Register `grep` at startup. |
| `globEnabled` | bool | `true` | Register `glob` at startup. |
| `fetchMcpResourceEnabled` | bool | `false` | Register `fetch_mcp_resource` at startup. Default off — turn on when a real MCP `resources` consumer (KnowMe / GitHub MCP / filesystem MCP) is in production. |

Disabling a tool removes it from the system prompt entirely (no "this tool was
disabled" placeholder). The agent simply doesn't know it exists.

### `webFetch` fields

| Field | Type | Default | Purpose |
|---|---|---|---|
| `timeoutSeconds` | float | `30.0` | Total HTTP timeout per request. |
| `maxChars` | int | `200000` | Server-side cap on returned markdown. Truncation marker is appended when the cap is hit. |
| `allowedSchemes` | string[] | `["https"]` | URL schemes that may be fetched. `file://` is rejected by default; add `"http"` only if you have a specific reason. |
| `allowedDomains` | string[] | `[]` | Optional host allowlist. Empty means unrestricted (subject to host-process network rules). When non-empty, only URLs whose host appears in the list are fetched. |

## Per-tool details

### `web_fetch`

```json
{ "url": "https://example.com/page", "max_chars": 50000 }
```

- Follows up to 10 redirects. HTTPS only by default; the scheme allowlist
  rejects `file://` and `http://` unless explicitly enabled.
- Optional per-call `max_chars` argument can shrink the cap below
  `webFetch.maxChars` but never raise it.
- Strips `<script>`, `<style>`, `<noscript>`, `<template>` from the HTML before
  markdown conversion (BeautifulSoup `decompose` + `markdownify`). Stripping
  is element-level, so `alert(1)` inside a `<script>` does not leak into the
  output.
- 4xx/5xx responses are returned as `is_error=True` with the status code.
- Timeouts and connect errors are surfaced as `is_error=True` with a clear
  reason.

Failure modes the agent will see:

| Cause | Returned message |
|---|---|
| Disallowed scheme | `web_fetch: scheme '<scheme>' not in allowed_schemes ['https']` |
| Disallowed domain | `web_fetch: host '<host>' not in allowed_domains [...]` |
| HTTP timeout | `web_fetch: timeout after Ns for '<url>'` |
| Connect error | `web_fetch: connection error: ...` |
| 4xx/5xx | `web_fetch: HTTP <code> for '<url>'` |
| Aborted by user | `web_fetch: aborted` |

### `grep`

```json
{
  "pattern": "def main\\(",
  "path": "src",
  "include": "**/*.py",
  "max_results": 100,
  "case_sensitive": true
}
```

- Pattern is Python `re`. PCRE-only features (lookbehind, recursion, possessive
  quantifiers) are not supported. Invalid regex returns `is_error=True` with
  the `re.error` message.
- `path` is workspace-relative; `WorkspaceResolver.resolve_within` rejects
  escapes (`/etc`, `../foo`, etc.).
- Files with a null byte in the first 8 KB are skipped (binary heuristic).
- Total run is bounded to 5 seconds via `asyncio.wait_for`. Over-broad patterns
  on huge trees fail with a clear timeout message instead of hanging the agent.
- Returns JSON `{matches: [{path, line_number, line}, ...], count, truncated?}`.
  Line numbers are 1-indexed.

### `glob`

```json
{ "pattern": "**/*.py", "path": ".", "max_results": 200 }
```

- Pattern is `pathlib.Path.rglob` syntax. Brace expansion (`{src,lib}/*.py`)
  is **not** supported in V1 (tracked as a follow-up).
- Same workspace boundary as `grep` / `read`.
- Symlinks whose resolved target falls outside the workspace are silently
  excluded (debug-logged). Symlinks within the workspace are followed.
- Results sorted by `mtime` descending (newest first), then truncated to
  `max_results`. Useful for "which files changed most recently?" queries.
- Returns JSON `{paths: [...], count, truncated?}`.

### `fetch_mcp_resource` (optional, default off)

```json
{ "server": "filesystem", "uri": "file:///workspace/README.md" }
```

- Only registered when `builtinTools.fetchMcpResourceEnabled = true`.
- The named MCP server must be currently connected (`/mcp list` reports
  `connected`). Disconnected or unknown servers return `is_error=True`.
- Wraps `mcp.ClientSession.read_resource(uri)`. Text content is concatenated;
  binary blobs are returned as `[binary blob, mimeType=..., length=N]` markers
  so the agent does not blindly inline them.

## Permission tier interaction

| Tier | `bash` | `read` / `write` / `edit` | New tier-2 tools | `memorize` |
|---|---|---|---|---|
| `read-only` | denied | only `read` allowed; `write`/`edit` denied | **all allowed** | allowed |
| `approval` | gated by `tools_requiring_approval` | gated likewise | **not gated by default** | not gated |
| `yolo` | allowed | allowed | allowed | allowed |

Under `tier=approval`, the new tools are **not** in the default
`tools_requiring_approval` list (`["bash", "write", "edit"]`). To gate
`web_fetch` (e.g. for paranoid deployments), add it explicitly:

```jsonc
{
  "channels": {
    "web": {
      "toolsRequiringApproval": ["bash", "write", "edit", "web_fetch"]
    }
  }
}
```

`grep` and `glob` are pure local reads, gating them in `approval` tier is
generally noise; PyClaw does not recommend it.

## Per-deployment opt-out

Disable a single tool without redeploying:

```jsonc
{
  "builtinTools": {
    "webFetchEnabled": false
  }
}
```

This works for any of the four flags. The tool is not registered, the system
prompt does not mention it, and the agent treats calls to it as unknown-tool
errors — same behaviour as if PyClaw had never shipped it.

For airgapped / no-egress deployments, set `webFetchEnabled = false`.

## Audit log

All Tier 2 tools emit the same structured audit line as existing builtins
(`tool_name`, `tool_class`, `caller_user_id`, `tier_source`, `decision`,
`decided_by`). No new audit fields. See [permissions.md § Audit log](./permissions.md#audit-log)
for the schema.

## Out of scope (explicitly not built)

- **`web_search`** — Use a search MCP server (Brave Search, Tavily, Exa) for
  per-deployment provider/key flexibility.
- **`todo`** — `update_working_memory` already provides per-session ephemeral
  state.
- **`apply_patch` Lark grammar** — `edit` covers the same use cases.
- **`sub_agent` / `Task`** — Reserved for a future change once SOP-extraction
  signals show repeated multi-task fan-out patterns.
- **LSP integration** — PyClaw is not IDE-embedded; Feishu/Web users derive no
  value from LSP feedback.
- **Bundled SKILL.md content** — Skills are data-driven via self-evolution +
  the new `.claude/skills/` cross-ecosystem discovery (see
  [skill-hub-compatibility.md](./skill-hub-compatibility.md)).
