# workspace 抽象 · 工具怎么触达文件、又怎么被放行

> 进 Part 3。一次工具调用从主循环出来，经过 WorkspaceOps 派发、AsyncFileSystem 触达文件，中间还要过权限管理器那道「判定瀑布」。这一章拆 workspace 的三件事：派发缝、文件系统抽象、以及决定放行/询问/拒绝的顺序。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/workspace-abstraction
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# workspace 抽象 · 工具怎么触达文件、又怎么被放行

Part 1、2 看了循环和工具本身。这一部分看它们脚下的地基——**工作区**(`xai-grok-workspace`)：一次工具调用怎么真正落到文件上，以及落之前怎么被「放行/询问/拒绝」。

## 什么是 workspace

`WorkspaceHandle` 是「一个工作目录」的宿主级抽象，把文件系统、VCS、工具执行、权限、[hunk 追踪](https://daiw.org/manual/grok-build/hunk-tracker)、[代码库图谱](https://daiw.org/manual/grok-build/codebase-graph-build)、[文件监听](https://daiw.org/manual/grok-build/fsnotify)全都拴在一起。每个 agent 会话对应一个 `WorkspaceSession`，持有该会话的 `FinalizedToolset`、hunk tracker、`AsyncFsWrapper` 等。

## WorkspaceOps：工具派发缝

[主循环](https://daiw.org/manual/grok-build/agent-loop)执行工具时调的 `WorkspaceOps::call_tool`，就在这里。它是个二选一的枚举：

- **`Local`**(进程内)：`handle.session(sid).toolset().call(name, args, ...)`——直接走会话的 [FinalizedToolset](https://daiw.org/manual/grok-build/tool-registry) 派发。
- **`Proxy`**(远程)：通过 hub WebSocket RPC 调远端 workspace 服务器，再把流式结果收回来。

这层抽象的价值：同一套上层逻辑，工具既能在本地进程跑，也能跑在远端 workspace(云端/容器)——和[工具协议那章](https://daiw.org/manual/grok-build/tool-protocol)「一套契约两种传输」一脉相承。

它的 RPC 契约值得学：`WorkspaceRpc` trait 带 `const METHOD` + `type Response`，**客户端和服务端共用同一个请求结构体**——改名字编译期就报错，不会一端漏改。六十多个 `WorkspaceOp`(git、hunk、code-nav、worktree、fs……)都这么定。

## AsyncFileSystem：工具眼里的文件系统

工具不直接碰 `std::fs`，而是拿一个 `Arc<dyn AsyncFileSystem>`(`file_system/fs.rs`):`read_file`/`write_file`/`try_read_file`……实现有 `LocalFs`(本地 tokio::fs)、`MockFs`(测试)、`AcpFsAdapter`(远程客户端 fs)。有界读是硬约束：`MAX_READ_BYTES = 4 MiB`(压在 8 MiB 服务器帧上限之下)、`MAX_LIST_COLLECT = 50000`，还能把 walk 限制在规范根内防符号链接逃逸。**把文件系统抽象成 trait**，才能让同一个工具在本地、远程、测试三种后端下都跑。

## 权限判定瀑布

工具要动文件之前，得先过权限管理器(`permission/manager/mod.rs`，已从单文件拆成一个模块目录)。它是个 actor(mpsc 命令 + oneshot 回复)，一次 `request(PermissionRequest)` 最终落到一个 `Decision`(`Allow`/`Ask`/`Reject`/`PolicyDeny`/`Cancelled`……)。判定是一条**顺序敏感的瀑布**——顺序本身就是安全设计：

```mermaid
flowchart TB
  A[请求方还在吗?] -->|没了| X[Cancelled]
  A -->|在| P[受管策略 evaluate]
  P --> PD{Policy Deny?}
  PD -->|是| DENY["PolicyDeny(在 YOLO 之前!)"]
  PD -->|否| Y{YOLO 模式?}
  Y -->|是| AL1[Allow]
  Y -->|否| SG{会话授权?<br/>MCP/域名/编辑/bash}
  SG -->|命中| AL2[Allow]
  SG -->|否| AU{Auto 模式?<br/>分类器判定}
  AU -->|Allow| AL3[Allow]
  AU -->|否/Block| SB{sandbox_auto?<br/>沙箱内的安全 bash}
  SB -->|是| AL4[Allow]
  SB -->|否| PRE{Read/Grep?<br/>只读自动放}
  PRE -->|是| AL5[Allow]
  PRE -->|否| PROMPT[交互式询问用户]
```

几个关键设计：

- **Policy Deny 排在 YOLO 之前**：哪怕你开了「全部放行(YOLO)」，受管策略的拒绝仍然拦得住——企业管控不被用户开关绕过。
- **bash 用 tree-sitter 拆解**再判：`evaluate_bash` 把命令按 `&&`/`;`/管道拆段，优先级 **禁止 > 危险 > 安全白名单 > 询问**；危险命令(`is_dangerous_command_words`)即使在白名单里也强制询问。
- **sandbox_auto**([沙箱那章](https://daiw.org/manual/grok-build/sandbox-permissions)埋的线，需打开 `[sandbox] auto_allow_bash`)：沙箱激活时，只读或只写「安全 sink」的 bash 自动放行，但**写真实文件、设危险环境变量仍要问**——OS 硬隔离 + 命令下限，双保险。
- **只读默认放**：`Read`/`Grep`/`WebSearch` 走 `pre_decision` 自动允许(除非策略明确要问)；但 `MCPTool` **不在此列**——没有会话授权的 MCP 工具一律要问，用户拒过的直接驳回。
- **受管策略成了独立引擎**：`permission/managed_policy/` 把 `managed-settings.json` 与各层 `managed_config.toml`/`requirements.toml` 按「最严者胜」合成 MCP/插件市场白名单；每次请求先由 `gate_preflight.rs` 一次算好策略规则和两道 bash 安全闸的结论，瀑布各处只读这一个结果。

## 和《从 LLM 到 Coding Agent》对照

对照 [permissions](https://daiw.org/manual/llm-to-agent/permissions)：最小实现里权限就是「危险操作前问一句 y/n」。grok 的增量是一整套**分层判定**：受管策略、YOLO、会话授权、LLM 分类器 auto 模式、沙箱自动、只读快放——按精心设计的顺序层层过。这是「玩具靠一个 if，产品靠一条瀑布」的典型。下一章看工作区怎么感知文件被改。

---

上一篇：[与别家 agent 互通](https://daiw.org/manual/grok-build/agent-interop) · 下一篇：[文件监听 · fsnotify](https://daiw.org/manual/grok-build/fsnotify)
