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_BYTES32KB默认单次读取上限
MAX_ADAPTIVE_READ_MAX_BYTES128KB自适应读取硬上限
MAX_ADAPTIVE_READ_PAGES4最多自动翻页次数
ADAPTIVE_READ_CONTEXT_SHARE0.1context window 的 10% 用于读取
CHARS_PER_TOKEN_ESTIMATE4token → 字符估算系数

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 对读取的图片文件:

  1. 通过 sniffMimeFromBase64 对 base64 数据做 file-magic 嗅探
  2. 如果嗅探结果与声明 MIME 不一致,修正 MIME 类型
  3. 同步更新图片头部文字中的 MIME 显示
  4. 如果内容不是图片格式,抛错阻止错误类型传递

路径安全守卫: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.normalizeWindows 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 工具的差异:

维度普通 WriteMemory Flush Append-Only
操作限制任意路径仅限指定文件
写入模式覆盖append
适用场景一般文件操作Agent 记忆持久化
路径校验沙箱路径守卫精确路径匹配

关联

  • src/agents/edit-tool — Edit 工具实现(含 Recovery 机制)
  • src/agents/sandbox-bridge — SandboxFsBridge 接口与实现
  • src/agents/pi-tools-overview — Pi 工具体系总览
  • agent-workspace — 工作区路径与安全策略