当 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 和用途说明。模型只能通过这些入口访问本地资源;未注册的能力,对模型而言并不存在。与直接提供脚本执行环境相比,这种方式的权限边界更清晰,也便于在界面中展示「正在读取」或「正在写入」等状态。

典型的多步循环是:

  1. 客户端通过聊天传输层发起 streamText(或同类流式调用)。
  2. 模型决定是否调用某个工具。
  3. 本地 execute 完成执行,并将结果返回给模型。
  4. 模型继续推理,直到步数上限或自然结束。

权限检查可以集中放在 execute 内部,而不必分散到各处。

2.2 工作区绑定

所有路径都应是相对工作区根目录的相对路径。执行前:

  1. 解析工作区根目录并取得规范化路径(canonical path)。
  2. 禁止绝对路径。
  3. 逐段拼接,处理 . / ..,一旦越过根目录立即失败。
  4. 对已存在的路径再次规范化,确认其仍位于工作区内;对于尚未创建的文件,则先校验最近的已存在父目录,再拼接出目标路径。

这样,即使模型传入 ../../.ssh/id_rsa,请求也会在应用层直接被拒绝,不会进入用户确认流程。

2.3 写入审批(Write Gate)

读取操作通常可以直接执行;写入操作在沙箱模式下应先征得用户同意。可以用一个简单的审批器实现这套流程:

  1. write_fileexecute 在真正落盘前调用 waitForApproval(path, content)
  2. 审批器挂起一个 Promise,并通知界面显示待确认的写入请求。
  3. 用户选择「允许」或「拒绝」后,respond(id, approved) 结束等待。
  4. 拒绝则返回工具错误(例如「用户拒绝了此次文件写入」);允许再调用底层写入。

这比「先写入、再查看差异」更安全,因为文件在获得批准前不会发生任何变化。等待确认期间,工具调用保持未完成状态;界面可以暂时禁用输入并展示内容预览,降低用户忽略高风险写入的可能性。

2.4 模式切换:沙箱 vs 完全访问

同一套工具可以支持两种权限模式:

  • 沙箱模式:每次写入都需要用户确认,适合日常使用。
  • 完全访问模式:跳过确认并直接写入,适合用户明确授权且希望减少打断的场景。

可以在 systeminstructions 中告知模型当前权限模式,让模型了解写入是否需要确认;真正的强制逻辑仍应放在 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. 实现时值得注意的细节

  1. 让异步审批兼容流式调用
    工具的 execute 可以是异步函数。等待用户确认不会打断 AI SDK 的多步调用,只会让当前步骤保持未完成。界面应订阅审批状态,并在用户停止生成时取消尚未处理的写入请求,避免 Promise 一直处于等待状态。

  2. 同一时间只处理一条写入请求
    如果模型连续发起多次写入,应明确规定排队、合并或拒绝策略,避免界面展示的请求与实际等待的 Promise 不一致。

  3. 返回模型可以理解的错误
    「路径越出工作区」「用户拒绝写入」「Seatbelt 拒绝写文件」都应转换为明确的工具错误,让模型能够调整路径或执行方式,而不是只得到一次原因不明的失败。

  4. 设置调用步数上限
    使用 stopWhen: stepCountIs(N) 一类限制,防止模型陷入工具调用循环。多数沙箱任务并不需要很大的步数上限。

  5. 最小权限默认值
    默认模式用沙箱;网络默认关闭;完全访问要在 UI 上有醒目提示。

  6. 确认前展示写入预览
    审批界面应展示相对路径和写入内容预览。相比只询问「是否允许写文件」,这些上下文能帮助用户作出更准确的判断。


6. 小结

AI SDK 在这套方案中的作用,是缩小 Agent 的能力范围,并将危险操作封装为需要审批的工具。Seatbelt 的作用,则是在命令真正运行后,仍由系统阻止它访问不该触及的资源

两者叠在一起时:

  • 应用层提供清晰的读写工具、权限模式和用户确认流程;
  • 系统层提供不可绕过的文件写入与网络访问限制;
  • 路径归一化在应用层阻止目录穿越和符号链接逃逸。

这套方案不依赖特定的应用框架。只要运行环境能够调用本地工具并启动子进程,就可以按照「AI SDK 划定能力边界,Seatbelt 强制系统边界」的思路实现。还可以进一步细化读取权限、按目录配置白名单,或将 Shell 命令纳入同一套审批流程。无论如何扩展,核心原则都不变:提示词和交互负责引导,应用校验与系统沙箱负责安全。