OpenClaw 的 Read 工具基于 pi SDK 的 createReadTool 构建,在它之上增加了自适应分页、路径安全守卫、容器路径映射、图片 MIME 修正等能力。
整体架构
pi-coding-agent.createReadTool() ← 上游 SDK,基础读文件能力
↓
createOpenClawReadTool() ← 自适应分页 + 图片处理
↓ (可选)
wrapToolWorkspaceRootGuard() ← 路径安全守卫
↓ (Sandbox 场景)
createSandboxedReadTool() ← Sandbox:替换 I/O 为 bridge
相关页面:src/agents/edit-tool,src/agents/sandbox-bridge
上游层:createReadTool
来源:@earendil-works/pi-coding-agent(私有 npm 包),提供基础能力:
- 文件读取(utf-8 文本、二进制图片)
- MIME 检测(file-magic)
- offset/limit 分页
OpenClaw 不直接暴露上游工具,而是通过 createOpenClawReadTool 包装。
OpenClaw 包装层:createOpenClawReadTool
位于 src/agents/pi-tools.read.ts,对所有读操作进行增强:
export function createOpenClawReadTool(base, options): AnyAgentTool {
return {
...base,
execute: async (toolCallId, params, signal) => {
// 1. 参数校验
assertRequiredParams(record, REQUIRED_PARAM_GROUPS.read, base.name);
// 2. 自适应分页
const result = await executeReadWithAdaptivePaging({ base, args, maxBytes });
// 3. 剥离 truncation 内部细节,不透传给 LLM
const strippedResult = stripReadTruncationContentDetails(result);
// 4. 图片 MIME 修正
const normalizedResult = await normalizeReadImageResult(strippedResult, filePath);
// 5. 超大图片裁剪
return sanitizeToolResultImages(normalizedResult, ...);
},
};
}自适应分页
当用户未指定 limit 时,自动分多页读取:
| 常量 | 值 | 说明 |
|---|---|---|
DEFAULT_READ_PAGE_MAX_BYTES | 32KB | 默认单次读取上限 |
MAX_ADAPTIVE_READ_MAX_BYTES | 128KB | 自适应读取硬上限 |
MAX_ADAPTIVE_READ_PAGES | 4 | 最多自动翻页次数 |
ADAPTIVE_READ_CONTEXT_SHARE | 0.1 | context window 的 10% 用于读取 |
CHARS_PER_TOKEN_ESTIMATE | 4 | token → 字符估算系数 |
maxBytes 按模型 context window 动态计算:
maxBytes = clamp(contextTokens × 4 × 0.1, 32KB, 128KB)
注意:offset 从 1 开始(非 0),这是上游 pi-coding-agent 的约定。
累积内容超 maxBytes 时截断,末尾追加提示:
[Read output capped at 128KB for this call. Use offset=N to continue.]
如果用户显式传了 limit,则跳过自适应分页,直接单次读取。
Offset Beyond EOF 处理
isOffsetBeyondEof 检测 offset 超出文件末尾的情况(错误信息匹配正则 /^Offset \d+ is beyond end of file/),返回空结果而非抛错。
Read Continuation Notice 剥离
stripReadContinuationNotice 从读取结果中移除上游的 “继续读” 提示文案(如 Use offset=N to continue.),避免 LLM 看到多个分页结果中残留的导航提示。
Truncation Details 剥离
上游 createReadTool 在截断时会返回 details.truncation.content 字段,包含被截断行的原始数据(用于上游内部 UI 渲染)。
stripReadTruncationContentDetails 移除 details.truncation 中的 content 字段,保留 truncated、outputLines、firstLineExceedsLimit 等元信息。剥离原因:
- 防止截断内容的原始数据意外泄露给 LLM 从而污染上下文
content字段是上游内部实现细节,LLM 无需感知
图片 MIME 修正
normalizeReadImageResult 对读取的图片文件:
- 通过
sniffMimeFromBase64对 base64 数据做 file-magic 嗅探 - 如果嗅探结果与声明 MIME 不一致,修正 MIME 类型
- 同步更新图片头部文字中的 MIME 显示
- 如果内容不是图片格式,抛错阻止错误类型传递
路径安全守卫:wrapToolWorkspaceRootGuard
对所有路径参数做沙箱校验,防止路径穿越攻击:
const sandboxResult = await assertSandboxPath({
filePath: sandboxPath,
cwd: guardedRoot,
root: guardedRoot,
});核心机制:
- 遍历所有
pathParamKeys(默认["path"])的路径参数 - 对每个路径调用
assertSandboxPath校验是否在 workspace root 内 - 支持
normalizeGuardedPathParams选项,将校验后的路径写回参数
容器路径映射
mapContainerPathToRoot 支持路径格式转换:
| 来源格式 | 转换方式 | 场景 |
|---|---|---|
/workspace/xxx | → path.resolve(root, xxx) | Docker container |
file:///workspace/xxx | 解析 URL pathname 后映射 | DevContainer |
@/workspace/xxx | 去掉 @ 前缀后映射 | 编辑器协议 |
| Windows 盘符 | 用 path.win32.normalize | Windows host |
mapContainerPathToWorkspaceRoot 是外层 DSL,调用 mapContainerPathToRoot 后取 .filePath。
resolveContainerPathCandidate 处理 @ 前缀和 file:// URL 的解码。
附加挂载点支持
additionalContainerMounts 配置允许映射多个容器路径到不同宿主机目录:
const mounts = [
{ containerRoot: "/data", hostRoot: "/home/user/data" },
];遍历挂载点,优先匹配第一个成功映射的。
Sandbox 模式:createSandboxedReadTool
在 createOpenClawReadTool 外层再包一层,把文件 I/O 替换为 SandboxFsBridge:
createSandboxReadOperations(params) {
return {
readFile: (absolutePath) =>
params.bridge.readFile({ filePath: absolutePath, cwd: params.root }),
access: async (absolutePath) => {
const stat = await params.bridge.stat({ filePath: absolutePath, cwd: params.root });
if (!stat) throw createFsAccessError("ENOENT", absolutePath);
},
detectImageMimeType: async (absolutePath) => {
const buffer = await params.bridge.readFile({ ... });
const mime = await detectMime({ buffer, filePath: absolutePath });
return mime?.startsWith("image/") ? mime : undefined;
},
};
}Host 模式:直接使用 createOpenClawReadTool
Host 模式下 Agent 直接访问宿主机文件系统(无沙箱),因此不需要路径重定向。对应的 Read 工具没有独立包装函数,流程为:
pi-coding-agent.createReadTool() → createOpenClawReadTool() ← 无需沙箱层
Host 模式有独立的 createHostWorkspaceWriteTool 和 createHostWorkspaceEditTool(因涉及文件写入和恢复,需要 workspace 边界校验),但 Read 只需要读取权限,上游 createReadTool 已包含基本路径访问控制,Host 模式下无需额外包装。
Memory Flush Append-Only Write
pi-tools.read.ts 中还包含了 Write 工具的一个特殊变体 wrapToolMemoryFlushAppendOnlyWrite。
用途:memory flush 场景下,Agent 需要写入记忆文件。该包装将 Write 工具限制为只允许 append 到指定文件,防止 Agent 越界修改其他文件。
实现机制:
- 通过
resolveToolPathAgainstWorkspaceRoot解析路径 - 校验
resolvedPath !== allowedAbsolutePath,不匹配则抛错 - 读取已有内容,追加新内容后写回(不是单纯的 replace)
- 支持 Sandbox 和 Host 两种模式
- 在执行成功后返回
追加内容到 xxx.的确认消息
与普通 Write 工具的差异:
| 维度 | 普通 Write | Memory Flush Append-Only |
|---|---|---|
| 操作限制 | 任意路径 | 仅限指定文件 |
| 写入模式 | 覆盖 | append |
| 适用场景 | 一般文件操作 | Agent 记忆持久化 |
| 路径校验 | 沙箱路径守卫 | 精确路径匹配 |
关联
- src/agents/edit-tool — Edit 工具实现(含 Recovery 机制)
- src/agents/sandbox-bridge — SandboxFsBridge 接口与实现
- src/agents/pi-tools-overview — Pi 工具体系总览
- agent-workspace — 工作区路径与安全策略