# 内置工具

PyClaw 出厂带 11 个内置工具(算上默认关闭的可选 `fetch_mcp_resource` 是 12 个)。
4 个是 write 类,其余是 read 类。这个分类很重要 —— Sprint 1 的权限 tier 系统用
`tool_class` 决定 `tier=read-only` 下哪些自动放行、哪些自动拒绝。完整 tier 矩阵
见 [权限指南](./permissions.md)。

## 11 个默认工具

| 工具 | 类别 | 默认 | 用途 |
|---|---|---|---|
| `bash` | `write` | 开 | 在子进程里跑 shell 命令。设计上唯一被沙箱包裹的工具。 |
| `read` | `read` | 开 | 读 workspace 内的文件,支持 `offset` / `limit` 行范围。 |
| `write` | `write` | 开 | 在 workspace 内创建或覆盖文件。 |
| `edit` | `write` | 开 | 在 workspace 内的文件做字符串替换(`old_string` → `new_string`)。 |
| `grep` | `read` | 开 (Sprint 4') | 进程内正则搜索 workspace 文件,返回结构化 `{path, line_number, line}` 命中。 |
| `glob` | `read` | 开 (Sprint 4') | 进程内 glob 匹配,返回 workspace 相对路径,按 mtime 倒序。 |
| `web_fetch` | `read` | 开 (Sprint 4') | HTTPS GET → markdown(`httpx` + `markdownify`)。默认只允许 https 协议。 |
| `skill_view` | `read` | `progressiveDisclosure=true` 时启用 | 加载某个具名 skill 的完整 SKILL.md 内容。 |
| `memorize` | `memory-write-safe` | 配了 `memory_store` 时启用 | 持久化事实(L2)或流程(L3)。 |
| `forget` | `write` | 配了 `memory_store` 时启用 | 归档某个 L3 SOP(破坏性)。 |
| `update_working_memory` | `read` | 配了 `redis_client` 时启用 | Redis 里的会话级 scratchpad。 |

默认关闭的可选工具:

| 工具 | 类别 | 默认 | 用途 |
|---|---|---|---|
| `fetch_mcp_resource` | `read` | **关** | 从已连接的 MCP server 读一个具名 resource(`server` + `uri`)。 |

## 为什么加了 3 个新工具(Sprint 4')

Sprint 4' 之前 PyClaw 没达到行业 Tier 2 基线:有 `bash`/`read`/`write`/`edit` 但
没有进程内搜索、没有内置 URL 抓取。这每天造成两个痛点:

- `tier=read-only` 时 agent 没法搜代码(`bash` 被拒、`read` 只能读单文件)——
  "调研模式"会话基本上一次只能看一个文件。
- 抓 URL 内容只能退化到 `bash curl + sed/awk`,既浪费 token 又在 read-only 下根本不能用。

11 家 agent 生态调研([survey §3.1](../../DailyWork/planning/exploration/2026-05-17-agent-ecosystem-preinstall-survey.md)
—— private)确认 `grep` / `glob` / `web_fetch` 在 11 家里 8-10 家都有。
Sprint 4' 把 PyClaw 拉到这条基线。

## 沙箱交互(进程内工具按设计绕开 srt)

Sprint 3 的 [srt 沙箱](./sandbox.md) 包裹 `bash` 和 MCP stdio 子进程。新的 Tier 2
工具(`web_fetch`、`grep`、`glob`、`fetch_mcp_resource`)是**纯 Python 进程内实现** ——
不 spawn 子进程、不过 srt。这是 feature 不是 bug:

- `tier=read-only` 时 `bash` 自动拒(`tool_class="write"`)。如果进程内 `grep`
  也被沙箱包,read-only 调研会话就没法搜代码。read-only tier 的核心价值是
  "安全探索",进程内 read 工具就是让这件事能做的关键。
- srt 每次 spawn 有开销(生成 settings 文件、启进程)。简单搜索这个开销 ~30ms;
  进程内 ~0.1ms。
- 纯 Python 的 `re` 和 `pathlib.Path.rglob` 是确定的,不依赖沙箱里 PATH 上是否有
  `rg` / `fd` 二进制。

进程内工具的边界是 **workspace + tier 分类**,不是 srt:

| 关注点 | Bash / MCP 子进程 | 进程内 Tier 2 工具 |
|---|---|---|
| 文件系统边界 | srt `allowWrite` / `denyRead` | `WorkspaceResolver.resolve_within` |
| 网络出站 | srt `allowedDomains` / `deniedDomains` | `web_fetch` `allowed_schemes` / `allowed_domains` |
| Tier 强制 | runner `tool_class` 查询 | runner `tool_class` 查询 |

如果生产环境需要给 `grep`/`glob`/`web_fetch` 加出站控制,在主机层跑 PyClaw
进 NetworkPolicy / 出站代理来强制,工具自身不做这事。

## 配置

`pyclaw.json` 加两个顶层 section:

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

### `builtinTools` 字段

| 字段 | 类型 | 默认 | 用途 |
|---|---|---|---|
| `webFetchEnabled` | bool | `true` | 启动时注册 `web_fetch`。 |
| `grepEnabled` | bool | `true` | 启动时注册 `grep`。 |
| `globEnabled` | bool | `true` | 启动时注册 `glob`。 |
| `fetchMcpResourceEnabled` | bool | `false` | 启动时注册 `fetch_mcp_resource`。默认关 —— 等真有 MCP `resources` 的消费者(KnowMe / GitHub MCP / filesystem MCP)进生产时再打开。 |

关掉某个工具会把它从 system prompt 完全移除(没有"工具被禁"的占位符),
agent 根本不知道它存在。

### `webFetch` 字段

| 字段 | 类型 | 默认 | 用途 |
|---|---|---|---|
| `timeoutSeconds` | float | `30.0` | 单次 HTTP 请求总超时。 |
| `maxChars` | int | `200000` | 返回 markdown 的服务端上限,触顶时会追加截断标记。 |
| `allowedSchemes` | string[] | `["https"]` | 允许抓取的 URL 协议。`file://` 默认拒,只有明确需要才加 `"http"`。 |
| `allowedDomains` | string[] | `[]` | 可选域名白名单。空表示不限制(仍受主机进程网络规则约束)。非空时只抓 host 在列表里的 URL。 |

## 各工具细节

### `web_fetch`

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

- 跟随最多 10 跳重定向。默认只 HTTPS;协议白名单拒 `file://` 和 `http://`(除非显式开)。
- 调用时可选 `max_chars` 参数,可以把上限调小到 `webFetch.maxChars` 之下,但不能调大。
- HTML 转 markdown 前先剥离 `<script>`、`<style>`、`<noscript>`、`<template>` 元素树
  (BeautifulSoup `decompose` + `markdownify`)。剥的是元素而不是 tag 名,所以
  `<script>` 里的 `alert(1)` 不会泄漏到输出。
- 4xx/5xx 返回 `is_error=True` 带状态码。
- 超时和连接错误返回 `is_error=True` 带原因。

agent 看到的失败信号:

| 原因 | 返回消息 |
|---|---|
| 协议不允许 | `web_fetch: scheme '<scheme>' not in allowed_schemes ['https']` |
| 域名不允许 | `web_fetch: host '<host>' not in allowed_domains [...]` |
| HTTP 超时 | `web_fetch: timeout after Ns for '<url>'` |
| 连接错误 | `web_fetch: connection error: ...` |
| 4xx/5xx | `web_fetch: HTTP <code> for '<url>'` |
| 用户中止 | `web_fetch: aborted` |

### `grep`

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

- pattern 是 Python `re` 语法。PCRE 独有特性(lookbehind、递归、占有量词)不支持。
  无效正则返回 `is_error=True` 带 `re.error` 消息。
- `path` 是 workspace 相对路径;`WorkspaceResolver.resolve_within` 拒绝越界
  (`/etc`、`../foo` 等)。
- 文件前 8 KB 含 null 字节判定为二进制,跳过。
- 整个运行有 5 秒上限(`asyncio.wait_for`)。在大目录上跑过宽 pattern 时会因超时
  失败,而不是把 agent 挂死。
- 返回 JSON `{matches: [{path, line_number, line}, ...], count, truncated?}`,
  行号 1 起始。

### `glob`

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

- pattern 是 `pathlib.Path.rglob` 语法。V1 **不**支持大括号展开(`{src,lib}/*.py`),
  作为 follow-up 跟踪。
- 跟 `grep`/`read` 同样的 workspace 边界。
- 符号链接的解析目标在 workspace 之外时静默剔除(debug 日志),workspace 内的链接
  会跟随。
- 结果按 `mtime` 倒序(最新在前),然后截断到 `max_results`。"最近改了哪些文件"
  这种查询很合适。
- 返回 JSON `{paths: [...], count, truncated?}`。

### `fetch_mcp_resource`(可选,默认关)

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

- 只在 `builtinTools.fetchMcpResourceEnabled = true` 时注册。
- 指定的 MCP server 当前必须连接成功(`/mcp list` 显示 `connected`)。
  断开或不存在的 server 返回 `is_error=True`。
- 包 `mcp.ClientSession.read_resource(uri)`。文本内容拼接返回;二进制 blob
  以 `[binary blob, mimeType=..., length=N]` 标记返回,避免 agent 盲目内联。

## 与权限 tier 的交互

| Tier | `bash` | `read` / `write` / `edit` | 新 Tier 2 工具 | `memorize` |
|---|---|---|---|---|
| `read-only` | 拒 | 只 `read` 通过;`write`/`edit` 拒 | **全部通过** | 通过 |
| `approval` | 由 `tools_requiring_approval` 门控 | 同上 | **默认不门控** | 默认不门控 |
| `yolo` | 通过 | 通过 | 通过 | 通过 |

`tier=approval` 时,新工具**不**在默认的 `tools_requiring_approval` 列表里
(`["bash", "write", "edit"]`)。要给 `web_fetch` 加门控(比如严控部署),
显式加进去:

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

`grep` 和 `glob` 是纯本地读,在 `approval` tier 给它们加门控通常只是噪音,
PyClaw 不推荐这么做。

## 部署级关停

不重新部署的情况下关掉单个工具:

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

四个 flag 都可以这样关。工具不会注册,system prompt 不会提到它,agent 调用时
报"工具未知"错误 —— 行为跟 PyClaw 从未实现过这个工具一样。

气隙 / 无出站环境部署:`webFetchEnabled = false`。

## 审计日志

所有 Tier 2 工具发跟原内置工具一样的结构化审计行(`tool_name`、`tool_class`、
`caller_user_id`、`tier_source`、`decision`、`decided_by`),没新增字段。
schema 见 [permissions.md § Audit log](./permissions.md#audit-log)。

## 显式不做(不是漏了)

- **`web_search`** —— 用搜索类 MCP server(Brave Search、Tavily、Exa),按部署
  灵活换 provider / API key。
- **`todo`** —— `update_working_memory` 已经覆盖会话级临时状态。
- **`apply_patch` Lark 文法** —— `edit` 已能覆盖同样的用例。
- **`sub_agent` / `Task`** —— 等 SOP 提取数据出现"agent 想 fan-out 多任务"信号
  之后再单独开 change。
- **LSP 集成** —— PyClaw 不是 IDE-embedded,飞书/Web 用户用不上 LSP feedback。
- **Bundled SKILL.md 内容** —— skill 走数据驱动:self-evolution + 新加的
  `.claude/skills/` 跨生态发现路径(见 [skill-hub-compatibility.md](./skill-hub-compatibility.md))。
