GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-05-01-tool-pluggable-operations-design.md
2026-05-20 21:39:12 +08:00

348 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Neta 工具层 Pluggable Operations 架构改造设计
## 背景
Neta 的 Agent 工具系统(`bash` / `read_file` / `write_file` / `edit` / `patch` / `grep` / `find_files` / `list_dir`)当前直接调用 `fs/promises``spawn``exec` 等底层系统 API。工具的业务逻辑参数校验、模糊匹配、diff 计算、输出截断、错误格式化)和执行后端(本地文件系统 / 本地子进程)耦合在同一段代码里。
这种耦合在系统能力较小时没有问题,但随着以下场景出现,已经显现出多个限制:
1. **测试困难** —— 工具单元测试必须真实读写本地文件系统,无法 mock 文件操作做行为级测试
2. **无远程/沙箱执行能力** —— 当前没有把工具调用路由到 SSH、Docker 容器、隔离环境的能力,所有命令都在后端进程的宿主机上执行
3. **Crew 多 Agent 文件并发风险** —— `file_mutation_queue.ts` 提供了单进程内的文件写串行化,但 Crew 多 Agent / subagent worker 跨进程时无法共享这个队列
4. **跨切关注点散落** —— ANSI 输出剥离、命令重试、超时处理、操作审计等共性逻辑无法在统一的层做拦截,必须每个工具单独实现
5. **权限控制粗放** —— 当前 `tools/manifest.ts``worker-local` / `main-process-proxy` 路由是工具粒度的 yes/no无法表达"允许读但禁止写"或"只能写白名单目录"这类细粒度策略
但 Neta 现在已经有几块"差一步就能升级"的基础设施:
- `tools/builtin/bash.ts:24-42` 已经定义了 `BashOperations` 接口和 `createLocalBashOperations()` 工厂——这是 Pluggable Operations 模式在 Neta 内部最早的实践,但没有推广到其他工具
- `tools/file_mutation_queue.ts` 已经把文件写串行化抽出来,调用方只需 `withFileMutationQueue(filePath, fn)`
- `tools/runtime_context.ts` 通过 `_netaRuntime` 字段把 `sessionCwd` / `workspaceRoots` 注入到工具参数里,是"会话级上下文"的现成传递通道
- `tools/catalog.ts`schema 元数据)和 `tools/manifest.ts`worker 路由策略)已经分离——新引入的 Operations 层是第三个独立维度(执行后端),不与它们冲突
- `comm/child_process.ts` 已经统一了子进程创建(自动注入 `windowsHide: true`Operations 实现层可以直接复用
参考 Pi`pi-mono-main/packages/coding-agent`)的工具系统设计,本次改造把这个模式系统性地引入 Neta。
## 目标
通过引入 **Operations 接口层**,把"文件 I/O"和"进程执行"这两类系统调用从工具业务代码中抽出来,使工具的业务逻辑不再依赖 `fs/promises``child_process`
具体目标:
1. 定义 `FileOperations` / `ProcessOperations` / `SearchOperations` 三个接口,聚合为 `ToolOperations`
2. 提供 `LocalToolOperations` 默认实现,封装现有的本地文件系统和子进程执行逻辑
3. 把所有 builtin 工具迁移到 Operations 接口,不再直接 import `fs``child_process`
4.`tool_resolver.ts` 注册阶段统一注入 Operations 实例,未来可根据会话策略注入不同实现
5. Subagent worker 端可以注入 `WorkerProxyOperations`,把需要主进程执行的操作通过 IPC 代理回主进程
6. 完成后,**主进程行为零变化**,但解锁工具单元测试、远程执行、沙箱化、跨切拦截等后续能力
## 非目标
本次改造不包含以下内容:
- **不动 Agent 循环** —— `runtime/attempt.ts``runtime/agent.ts` 完全不改
- **不动 catalog 和 manifest** —— `tools/catalog.ts``tools/manifest.ts``tools/common.ts` 不改
- **不动 Hook 契约** —— `beforeToolCall` / `afterToolCall` 的接口和语义保持
- **不动 Session Tree** —— 会话树、压缩、subagent projection 不动
- **不动 LLM Provider 适配层** —— `llm-providers` 不涉及
- **不改工具的 schema 和参数定义** —— LLM 看到的工具描述和参数完全一致
- **不改 memory / skill / delegate / escalate / todo / clarify 工具** —— 这些工具不直接做 fs/spawn 操作,本次不需要迁移
- **不重构现有 manifest 的 worker 路由** —— Phase 3 只做"通过 Operations 注入实现路由",不打散按工具粒度的现有策略
- **不实现远程后端、沙箱后端** —— 本次只做接口和本地实现,远程/沙箱实现作为未来工作
- **不引入服务端权限模型** —— 细粒度权限作为未来工作
## 架构设计
### 三层模型
```
┌─────────────────────────────────────────────────────┐
│ Tool 业务层(不变) │
│ schema 定义、参数校验、模糊匹配、diff 计算、 │
│ 截断逻辑、错误格式化、路径解析 │
├─────────────────────────────────────────────────────┤
│ Operations 接口层(新增) │
│ FileOperations、ProcessOperations、SearchOperations │
├─────────────────────────────────────────────────────┤
│ Implementation 实现层(替换点) │
│ LocalToolOperations默认
│ WorkerProxyOperationssubagent worker → main proxy
│ 未来DockerOperations / SshOperations / MockOps │
└─────────────────────────────────────────────────────┘
```
工具业务层只看到 Operations 接口,不知道底层实现。注入点在 `tool_resolver.ts` 构建工具列表时。
### Operations 接口定义
```typescript
// tools/operations/types.ts
export interface FileOperations {
/** 读文件,返回 Buffer由上层决定编码 */
readFile(absolutePath: string): Promise<Buffer>;
/** 写文件 */
writeFile(absolutePath: string, content: string | Buffer): Promise<void>;
/** 追加写入 */
appendFile(absolutePath: string, content: string | Buffer): Promise<void>;
/** 可读性 / 可写性检查,失败抛出 */
access(absolutePath: string, mode: 'read' | 'write' | 'readwrite'): Promise<void>;
/** 判断是否目录,不存在抛出 */
isDirectory(absolutePath: string): Promise<boolean>;
/** 列目录 */
readDir(absolutePath: string): Promise<Array<{ name: string; isDirectory: boolean; size: number }>>;
/** 创建目录(递归) */
mkdir(absolutePath: string): Promise<void>;
/** 解析 realpath用于 file_mutation_queue 的 key */
realpath(absolutePath: string): Promise<string>;
}
export interface ProcessOperations {
/**
* 执行 shell 命令并流式回传输出。
* 行为契约(必须保持与现有 bash 一致):
* - 通过 onData 流式回调输出
* - signal 触发时 kill 进程树
* - timeout 到时 kill 并 reject
* - cwd / env 必须传透
* - Windows 下自动隐藏控制台窗口(由实现保证)
*/
exec(
command: string,
cwd: string,
options: {
onData: (chunk: Buffer, stream: 'stdout' | 'stderr') => void;
signal?: AbortSignal;
timeout?: number;
env?: NodeJS.ProcessEnv;
},
): Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>;
}
export interface SearchOperations {
/** 等价 ripgrepgrep 工具用) */
ripgrep(args: string[], cwd: string, signal?: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number | null }>;
/** 等价 fdfind_files 工具用) */
fd(args: string[], cwd: string, signal?: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number | null }>;
}
/** 三接口聚合 */
export interface ToolOperations {
file: FileOperations;
process: ProcessOperations;
search: SearchOperations;
}
```
### 设计权衡
#### 为什么聚合成一个 `ToolOperations`而不是每个工具一个独立接口Pi 的做法)
Pi 给每个工具定义了独立接口(`ReadOperations` / `EditOperations` / `WriteOperations` / `BashOperations` / `GrepOperations` / `FindOperations`。Neta 选择聚合的原因:
- Neta 工具数量多于 Pi含 memory、skill、delegate 等),独立接口意味着注入点要写 N 个 options 字段,调用方负担大
- 同一套 Operations 实现服务于所有工具更自然——主进程的 LocalOperations、worker 的 ProxyOperations 都是一整套
- 失去"一个工具只看到自己需要的方法"的最小权限好处。但 Neta 已经在 manifest 层做工具粒度的策略控制,权限边界已经在更上层覆盖,下沉到接口层是冗余
#### 为什么把 `realpath` 放在 FileOperations
`withFileMutationQueue` 当前用 `fs.realpathSync.native()` 取 key。改造后 queue 也要走 Operations否则远程后端的"同一文件"判断就错了——远程符号链接和本地的解析结果不一样。
#### 为什么 `ProcessOperations.exec` 用 `onData` 流式回调
- 与现有 bash 工具实现一致(`tools/builtin/bash.ts:46`),减少改动
- 大输出场景下避免内存爆炸——上层决定截断或落盘
#### 为什么单独抽出 `SearchOperations`
- ripgrep / fd 有"二进制可能不存在"的特殊语义和 fallback 逻辑,是高层概念,不适合塞进 ProcessOperations
- 远程后端可以直接调远程的 ripgrep / fd性能比把整个目录拉回本地再搜要好几个数量级
## 改造步骤
按风险从低到高分四个 Phase每个 Phase 独立可提交可回滚。
### Phase 1建立 Operations 基础设施(无业务影响)
**1.1 新建 `tools/operations/types.ts`** —— 定义三个接口和聚合接口,仅类型,无运行时依赖。
**1.2 新建 `tools/operations/local.ts`** —— 实现 `LocalToolOperations`
- `FileOperations``fs/promises`
- `ProcessOperations``comm/child_process.spawn`(已封装 windowsHide。把现有 `tools/builtin/bash.ts``createLocalBashOperations` 的实现迁移过来
- `SearchOperations``comm/child_process.spawn` 调 ripgrep/fd。把现有 `tools/builtin/search.ts` 里 ripgrep/fd 的调用逻辑迁移过来
**1.3 新建 `tools/operations/index.ts`** —— 导出 `getDefaultOperations(): ToolOperations`,使用模块级单例,避免重复创建。
**1.4 给 `tools/file_mutation_queue.ts` 加新签名**
```typescript
// 保留原签名(兼容现有调用方)
export async function withFileMutationQueue<T>(
filePath: string,
fn: () => Promise<T>,
): Promise<T>;
// 新签名:用注入的 file ops 来 realpath
export async function withFileMutationQueueOps<T>(
filePath: string,
fileOps: FileOperations,
fn: () => Promise<T>,
): Promise<T>;
```
老签名保留,避免一次性迁移所有调用方。
**1.5 新增契约测试 `test/tool_operations.test.ts`** —— 验证 `LocalToolOperations` 的每个方法语义正确读写文件、列目录、exec 流式输出、ripgrep/fd 调用)。
**Phase 1 验证点**
- `npx tsc --noEmit` 通过
- 现有所有测试通过
- 新增的契约测试通过
- `getDefaultOperations()` 不被任何工具引用(说明业务代码未动)
### Phase 2迁移工具到 Operations一次一个独立提交
按以下顺序,每个工具一个独立 commit
| 顺序 | 工具 | 替换点 |
|-----|------|--------|
| 2.1 | `read_file` | `fs.readFile``ops.file.readFile``fs.access``ops.file.access` |
| 2.2 | `list_dir` | `fs.readdir` / `fs.stat``ops.file.readDir` |
| 2.3 | `write_file` | `fs.writeFile` / `fs.mkdir``ops.file.*`queue 改用 `withFileMutationQueueOps` |
| 2.4 | `edit` | `fs.readFile` / `fs.writeFile``ops.file.*` |
| 2.5 | `patch` | 同 edit |
| 2.6 | `find_files` | fd 子进程 → `ops.search.fd` |
| 2.7 | `grep` | ripgrep 子进程 → `ops.search.ripgrep` |
| 2.8 | `bash` | 自有 `BashOperations` 删除,改用 `ops.process.exec` |
**注入方式**:每个工具的 factory 增加可选 `operations` 参数,默认走 `getDefaultOperations()`
```typescript
// 当前
export const readFileTool: AgentToolWithMeta<...> = { ... };
// 改造后
export function createReadFileTool(opts?: { operations?: ToolOperations }): AgentToolWithMeta<...> {
const ops = opts?.operations ?? getDefaultOperations();
return { ...; execute: async (id, params) => { /* 用 ops.file.readFile */ } };
}
// 保留 catalog 注册兼容:
export const readFileTool = createReadFileTool();
```
**Phase 2 每个工具的验证点**
- 该工具的 isolation 测试用 Mock Operations 跑通
- `npx tsc --noEmit` 通过
- 启动后端Agent 实际调用该工具,输出与改造前完全一致(人工验证一次)
- `git diff` 仅涉及该工具自己的文件
**Phase 2 全部完成的验证点**
- `git grep "from 'fs/promises'" packages/backend/src/modules/netaclaw/tools/builtin/` 应为 0 命中
- `git grep "child_process" packages/backend/src/modules/netaclaw/tools/builtin/` 应为 0 命中
- 跑一次 e2e Agent 会话覆盖所有工具read_file → edit → bash → grep → write_file
### Phase 3在工具注册处统一注入
**3.1 修改 `service/tool_resolver.ts`**
`ResolveToolParams` 增加 `operations?: ToolOperations`
- 主进程默认 `getDefaultOperations()`
- 后续可以根据 `runtimePolicy.workspaceRoots` 注入 sandboxed/scoped 实现
工具列表构造时把 operations 传给每个工具的 factory。
**3.2 修改 subagent worker 的工具构建逻辑**
Worker 进程构造工具时注入 `WorkerProxyOperations`
- `file.writeFile` 等需要主进程代理的方法 → 通过 IPC 发 `proxy_tool_call` 给主进程
- `process.exec` 由 worker 端本地执行(已经被 worker policy 限制),无需代理
**注**:这一步**只做接口对接,沿用现有 manifest 路由策略**,不打散按工具粒度的现有路由。等后续真有需求(比如 worker 想读文件但写文件代理)再细化到操作粒度。
**Phase 3 验证点**
- 主进程 Agent 行为不变
- subagent worker 行为不变
- file_mutation_queue 在跨 worker 写同一文件时行为可被观测到(写一个 e2e 测试)
### Phase 4清理遗留代码
**4.1 删除 `tools/builtin/bash.ts` 中的 `BashOperations` 接口和 `createLocalBashOperations`** —— 已经被 `ProcessOperations` 完全替代
**4.2 删除 `withFileMutationQueue` 老签名** —— Phase 2 全部完成后,所有调用方都用新签名
**4.3 清理 `search.ts` 里 `spawnAndCollect`、`collectRipgrepMatches` 等已经迁到 Operations 实现的辅助函数**
## 关键文件
### 新增
- `packages/backend/src/modules/netaclaw/tools/operations/types.ts` —— 接口定义
- `packages/backend/src/modules/netaclaw/tools/operations/local.ts` —— 默认本地实现
- `packages/backend/src/modules/netaclaw/tools/operations/index.ts` —— 单例导出
- `packages/backend/test/tool_operations.test.ts` —— 接口契约测试
### 修改
- `packages/backend/src/modules/netaclaw/tools/file_mutation_queue.ts` —— 增加 ops 版签名
- `packages/backend/src/modules/netaclaw/tools/builtin/file.ts` —— read/write/list_dir 接 Operations
- `packages/backend/src/modules/netaclaw/tools/builtin/edit.ts` —— 接 Operations
- `packages/backend/src/modules/netaclaw/tools/builtin/patch.ts` —— 接 Operations
- `packages/backend/src/modules/netaclaw/tools/builtin/search.ts` —— 接 SearchOperations
- `packages/backend/src/modules/netaclaw/tools/builtin/bash.ts` —— 删除自有 BashOperations接 ProcessOperations
- `packages/backend/src/modules/netaclaw/service/tool_resolver.ts` —— 增加 operations 参数
- `packages/backend/src/modules/netaclaw/subagent/` 下相关文件Phase 3 时确认)—— 注入 ProxyOperations
### 不动
- `tools/catalog.ts``tools/common.ts``tools/manifest.ts`
- `runtime/attempt.ts``runtime/agent.ts`
- 所有 memory / skill / delegate / escalate / todo / clarify 工具
- `tools/path_utils.ts``tools/runtime_context.ts``tools/edit_diff.ts``tools/truncate.ts``tools/process_utils.ts`
- `comm/child_process.ts`(被 LocalOperations 复用)
## 风险与缓解
| 风险 | 等级 | 缓解 |
|------|------|------|
| 大改动一次提交后回归难定位 | 高 | Phase 2 每个工具独立提交,每次仅迁一个工具 |
| Operations 接口设计不当,未来扩展受限 | 中 | Phase 1 落地后先观察 1-2 周再开始 Phase 2期间允许接口微调 |
| Subagent worker 路由复杂度上升 | 中 | Phase 3 先只做接口对接,不重构现有 manifest 路由;等真实需求出现再细化 |
| 性能下降(多一层接口跳转) | 低 | 接口都是直接函数调用V8 会内联;本地实现零额外开销 |
| 测试覆盖不足 | 中 | Phase 1 强制为 LocalOperations 写契约测试Phase 2 每个工具迁移时跑一次完整 Agent 会话 |
## 价值兑现路径
完成 Phase 1+2 后立即可以做:
1. **工具单元测试** —— Mock 一个 Operations 就能跑,不需要真实文件系统
2. **bash 输出清理** —— 在 LocalProcessOperations 的 exec 实现里加 ANSI strip所有 bash 调用点零修改自动受益
3. **Skill 沙箱化** —— 给 Skill 注入受限的 FileOperations白名单目录无需改工具代码
完成 Phase 3 后可以做:
4. **远程工作区** —— 实现 SshToolOperationsAgent 可以操作远程主机上的代码
5. **Crew 隔离** —— 不同子 Agent 注入不同的 workspace-scoped Operations硬隔离写权限
6. **细粒度审计** —— 在 Operations 层记录所有文件读写、所有命令执行,统一日志/审计
## 时间预估
| Phase | 工作量 | 说明 |
|-------|--------|------|
| Phase 1 | 1 天 | 建立基础设施,零业务影响 |
| Phase 2 | 3-4 天 | 8 个工具,每个 0.5 天 |
| Phase 3 | 1-2 天 | tool_resolver 注入 + worker proxy 对接 |
| Phase 4 | 0.5 天 | 清理 |
| **合计** | **6-8 天** | |
建议节奏Phase 1 合并后先稳定运行 1 周再开始 Phase 2Phase 2 每个工具一个 PR便于 code review 和回滚。
## 进度跟踪
- `[done]` Phase 1建立 Operations 基础设施(`6eb15f3`
- `[done]` Phase 2.1-2.3:迁移 read_file / write_file / list_dir`a383c43`
- `[done]` Phase 2.4-2.5:迁移 edit / patch`009945c`
- `[done]` Phase 2.6-2.8:迁移 find_files / grep / bash`69487ae`
- `[done]` Phase 3tool_resolver 统一注入 + worker_tools 统一注入(`0e92ed9`
- `[done]` Phase 4删除 withFileMutationQueue 旧签名并完成接口收口(`7c4626f`
- `[done]` P1 测试补齐LocalToolOperations 契约测试 + tool_resolver 注入测试 + worker_tools 注入测试(本次提交)