当 Agent 获得访问本地文件系统或执行 Shell 命令的能力后,一个问题也随之而来:模型需要足够的权限完成任务,但一次误操作或路径越界,就可能修改工作区之外的内容。本文介绍如何结合 AI SDK 的工具边界与 macOS Seatbelt 的系统级隔离,构建一套可落地的沙箱权限控制方案。
核心结论很简单:
- AI SDK 限定「能做什么」:模型只能调用已注册的工具,危险操作还可以先交由用户确认。
- Seatbelt 限定「实际允许做什么」:在进程层阻止工作区外写入,并可选择禁止网络访问。
- 两层相互补充:前者提供清晰的权限入口和交互流程,后者防止越权与绕过。
1. 为什么需要两层沙箱
只在 System Prompt 中要求模型「不要越权」,并不能形成真正的安全边界。仅检查路径字符串,也无法限制 Shell 命令产生的副作用。更稳妥的做法是分层控制:
| 层级 | 职责 | 失败时表现 |
|---|---|---|
| 应用层(AI SDK Tools) | 只开放必要能力;写入操作可暂停并等待用户确认 | 工具返回错误,对话仍可继续 |
| 应用层(路径归一化) | 只接受相对路径,阻止 .. 越界,并在规范化后再次校验 | API 直接拒绝 |
| 系统层(Seatbelt) | 子进程只能写工作区 / 临时目录等白名单路径 | 内核拒绝,命令非零退出 |
Agent 得到的是「工具调用失败」或「命令被拒绝」等明确结果;用户看到的则是易于理解的确认提示和权限模式。无论上层如何交互,底层都保留一条不可绕过的安全边界。
2. AI SDK:把权限封装为工具
2.1 工具即能力面
在 AI SDK 中,不要让 Agent 直接持有任意文件系统句柄,而应只允许它调用预先声明的一组工具,例如:
list_dir:列目录read_file:读文本write_file:创建或覆盖写入
每个工具都有明确的输入 Schema 和用途说明。模型只能通过这些入口访问本地资源;未注册的能力,对模型而言并不存在。与直接提供脚本执行环境相比,这种方式的权限边界更清晰,也便于在界面中展示「正在读取」或「正在写入」等状态。
典型的多步循环是:
- 客户端通过聊天传输层发起
streamText(或同类流式调用)。 - 模型决定是否调用某个工具。
- 本地
execute完成执行,并将结果返回给模型。 - 模型继续推理,直到步数上限或自然结束。
权限检查可以集中放在 execute 内部,而不必分散到各处。
2.2 工作区绑定
所有路径都应是相对工作区根目录的相对路径。执行前:
- 解析工作区根目录并取得规范化路径(canonical path)。
- 禁止绝对路径。
- 逐段拼接,处理
./..,一旦越过根目录立即失败。 - 对已存在的路径再次规范化,确认其仍位于工作区内;对于尚未创建的文件,则先校验最近的已存在父目录,再拼接出目标路径。
这样,即使模型传入 ../../.ssh/id_rsa,请求也会在应用层直接被拒绝,不会进入用户确认流程。
2.3 写入审批(Write Gate)
读取操作通常可以直接执行;写入操作在沙箱模式下应先征得用户同意。可以用一个简单的审批器实现这套流程:
write_file的execute在真正落盘前调用waitForApproval(path, content)。- 审批器挂起一个 Promise,并通知界面显示待确认的写入请求。
- 用户选择「允许」或「拒绝」后,
respond(id, approved)结束等待。 - 拒绝则返回工具错误(例如「用户拒绝了此次文件写入」);允许再调用底层写入。
这比「先写入、再查看差异」更安全,因为文件在获得批准前不会发生任何变化。等待确认期间,工具调用保持未完成状态;界面可以暂时禁用输入并展示内容预览,降低用户忽略高风险写入的可能性。
2.4 模式切换:沙箱 vs 完全访问
同一套工具可以支持两种权限模式:
- 沙箱模式:每次写入都需要用户确认,适合日常使用。
- 完全访问模式:跳过确认并直接写入,适合用户明确授权且希望减少打断的场景。
可以在 system 或 instructions 中告知模型当前权限模式,让模型了解写入是否需要确认;真正的强制逻辑仍应放在 execute 中。切换模式时不必重建整套工具,可通过 getter 或 ref 读取最新状态,避免闭包持有旧值。
2.5 把约束写进提示词,但不要只靠提示词
System Prompt 中可以重申:
- 只能通过工具访问选定工作区;
- 路径必须是相对路径;
- 写入前先列出目录或读取现有文件;
- 写入给出完整内容。
提示词只能降低误用概率,真正的安全边界仍由 Schema、路径校验和用户审批共同保证。
3. Seatbelt:限制 Shell 进程的系统权限
当 Agent 需要执行 Shell 命令时,仅过滤命令字符串远远不够。macOS 的 Seatbelt 可以通过 sandbox-exec 为子进程应用沙箱策略(profile),并由系统强制执行。
3.1 策略思路:默认允许,收紧写与网络
一种实用的 profile 形态是:
(version 1)
(allow default)
(deny file-write* (require-not (require-any
(subpath "<workspace>")
(subpath "<tmpdir>")
(subpath "/dev/null")
(subpath "/dev/tty"))))
(deny network*) ; 可选:禁止网络
含义是:
- 进程仍可正常读取文件和执行命令。
- 写文件被收紧:只有工作区、系统临时目录、以及少数设备节点可写;写到家目录等位置会被拒绝。
- 网络访问可以按需关闭,防止沙箱中的进程向外传输数据。
将路径写入 profile 前,需要转义反斜杠和双引号,并使用规范化后的绝对路径,避免通过符号链接绕过目录边界。
3.2 调用方式
沙箱模式下大致等价于:
sandbox-exec -p '<profile>' "$SHELL" -lc '<command>'
子进程的当前目录设为选定的工作区。完全访问模式可以直接启动 Shell,不经过 sandbox-exec。
演示效果通常很直观:
echo hi > ./ok.txt # 工作区内:成功
echo bye > ~/blocked.txt # 工作区外:失败
3.3 能力探测与降级
Seatbelt 依赖 macOS 提供的系统能力。应用启动时应检测沙箱是否可用;在不支持的平台上,沙箱模式必须明确报错,不能静默降级为完全访问。
4. 两层如何协作
可以把完整链路画成:
用户消息
→ AI SDK 流式推理
→ 选择 tool(list / read / write)或触发 Shell
├─ 应用层:工作区路径校验
├─ 应用层:沙箱模式下 write 等待用户批准
└─ 系统层:Shell 走 sandbox-exec + Seatbelt profile
→ 工具结果回到模型
→ 继续或结束
分工建议:
| 场景 | 主要防线 |
|---|---|
| 结构化读写文件 | AI SDK 工具 + 路径归一化 + 写入审批 |
| 自由 Shell / 构建脚本 | Seatbelt(写路径与网络) |
| 用户体验 | 模式选择、待写入预览、工具调用卡片 |
需要注意,Seatbelt 只约束由 sandbox-exec 启动的子进程。如果文件写入由宿主进程直接完成,而不是通过沙箱中的 Shell 执行,就必须依靠应用层的路径校验和写入审批。因此,工具边界与进程沙箱并非替代关系,而是两道互补的防线。
5. 实现时值得注意的细节
-
让异步审批兼容流式调用
工具的execute可以是异步函数。等待用户确认不会打断 AI SDK 的多步调用,只会让当前步骤保持未完成。界面应订阅审批状态,并在用户停止生成时取消尚未处理的写入请求,避免 Promise 一直处于等待状态。 -
同一时间只处理一条写入请求
如果模型连续发起多次写入,应明确规定排队、合并或拒绝策略,避免界面展示的请求与实际等待的 Promise 不一致。 -
返回模型可以理解的错误
「路径越出工作区」「用户拒绝写入」「Seatbelt 拒绝写文件」都应转换为明确的工具错误,让模型能够调整路径或执行方式,而不是只得到一次原因不明的失败。 -
设置调用步数上限
使用stopWhen: stepCountIs(N)一类限制,防止模型陷入工具调用循环。多数沙箱任务并不需要很大的步数上限。 -
最小权限默认值
默认模式用沙箱;网络默认关闭;完全访问要在 UI 上有醒目提示。 -
确认前展示写入预览
审批界面应展示相对路径和写入内容预览。相比只询问「是否允许写文件」,这些上下文能帮助用户作出更准确的判断。
6. 小结
AI SDK 在这套方案中的作用,是缩小 Agent 的能力范围,并将危险操作封装为需要审批的工具。Seatbelt 的作用,则是在命令真正运行后,仍由系统阻止它访问不该触及的资源。
两者叠在一起时:
- 应用层提供清晰的读写工具、权限模式和用户确认流程;
- 系统层提供不可绕过的文件写入与网络访问限制;
- 路径归一化在应用层阻止目录穿越和符号链接逃逸。
这套方案不依赖特定的应用框架。只要运行环境能够调用本地工具并启动子进程,就可以按照「AI SDK 划定能力边界,Seatbelt 强制系统边界」的思路实现。还可以进一步细化读取权限、按目录配置白名单,或将 Shell 命令纳入同一套审批流程。无论如何扩展,核心原则都不变:提示词和交互负责引导,应用校验与系统沙箱负责安全。
