文件系统不是一种策略语言
全盘访问是对一棵文件树的二元授权。它只回答一个问题:这个进程能否打开这个路径?它无法回答对自主 agent 而言真正重要的问题:这个进程为什么要读这个文件,而这个目的能否为这次读取提供正当性?
设想对同一路径的两次读取。一个解析依赖的 agent 读取配置文件,以发现某个包注册表的位置。一个准备支持摘要的 agent 读取凭据存储,以提取一个 API token。在文件系统层面,这两者是相同的操作。权限模型没有表达意图的词汇,因此无法区分它们。授权要么存在,要么不存在,而一旦存在,它便适用于该 agent 在进程生命周期内执行的每一次读取。
Apple 决定收紧 macOS 对 AI agent 的全盘访问,是对风险状况真实变化的合理回应。Agent 不是人类操作员。它以机器速度运行,可以不停下来做判断就串联工具调用,而且它可能依据与它所读取的数据来自同一渠道的指令行事。然而,缓解措施仍然是同一原语的更粗糙版本:一个提示、一个同意对话框、一个更窄的授权。它收窄了谁可以读取,但仍然无法表达这次读取是为了什么。真正的信任单元是 agent、数据类别和目的这三元组。没有任何文件系统权限模型能表达这个三元组,因为文件系统并不知道 agent 的任务。
以数据类别作为范围划定单元
一个可行的替代方案是:不再把文件系统当作策略边界,而是开始把数据类别当作范围划定单元。在任何 agent 运行之前,将可读数据划分为若干层级:
- 公开:文档、已发布的参考资料、开放数据集。
- 运营:服务配置、部署清单、非敏感日志。
- 个人:用户记录、支持对话记录、任何可识别个人的内容。
- 机密:凭据、token、私钥、签名材料。
Agent 在其运行清单中声明它所需的层级。一个总结公开文档的检索 agent 声明 public。一个读取支持对话记录的分诊 agent 声明 public 和 personal。一个轮换凭据的 agent 声明 secret,而这种声明应当足够罕见,以触发审查。
执行应归属于工具层,而非操作系统层。Agent 不会收到一个挂载卷和一个路径。它收到的是函数:read_public_doc(id)、read_support_ticket(id)、read_credential_ref(name)。每个函数都会将 agent 声明的层级集合与它即将返回的数据所属的层级进行核对。如果 agent 没有声明 secret,凭据函数会返回拒绝,而不是文件句柄。由于 agent 自身的代码永远看不到文件系统路径,它就无法通过直接打开文件来提权。边界是接口的属性,而不是进程特权级别的属性。

这还会产生一条文件系统日志无法重建的审计轨迹。文件系统日志记录的是进程 4821 打开了 /etc/credentials/store。工具层日志记录的是 agent triage-v3,声明目的为 summarise open support tickets,请求了 read_support_ticket(88213) 并收到了一条属于 personal 层级的记录。当发生跨层级读取时,日志会连同声明的目的一起记录。这就是知道某个文件被打开与知道该 agent 试图完成什么之间的区别。
一个具体机制
该模式是一个带层级检查和目的标记的能力包装器。概要如下:
TIERS = {"public": 0, "operational": 1, "personal": 2, "secret": 3}
def guarded_read(agent, resource, tier):
if TIERS[tier] > TIERS[agent.declared_max_tier]:
audit.deny(agent.id, resource, tier, agent.purpose)
raise ScopeError(f"{agent.id} is not scoped for {tier}")
audit.allow(agent.id, resource, tier, agent.purpose)
return resource.fetch()
Agent 声明的最高层级在运行开始时即固定。在运行中途将其放宽不是一次配置变更;它是一次带有新清单和新审批的新运行。审计流是仅追加的,并以 agent 身份和声明的目的为键,因此审查者不仅能重建读取了什么,还能重建每次读取是由哪个任务驱动的。
这不是操作系统意义上的沙箱
层级分类是人工的,而且会漂移。一个新的数据源到来;有人给它分配了一个层级;六个月后,该数据源包含了一个它此前不包含的字段,而层级现在错了。分类需要一个负责人和一套审查节奏,而这位负责人是一项真实的持续成本。没有自动方法能从字段内容推断出其正确的层级,而尝试这样做往往要么过于宽松,要么噪声太大而不可信。
工具层执行的前提是 agent 无法绕过工具层。在一个没有直接系统调用访问、也没有经由被包装接口之外的网络出口的沙箱内,这是成立的。一旦 agent 运行在一个可以打开文件、派生子进程或直接访问网络的进程中,这就不成立了。策略层的强度仅取决于其下方的隔离,这意味着该设计要求两者兼备:分层工具接口以及一个真正无法逃逸它们的运行时。
这种取舍是刻意的。更多的前期分类工作、一位负责层级漂移的负责人,以及一个强制执行隔离的运行时——换来的是一个可读、可执行、可审计的暴露面。文件系统授权签发起来很廉价,事后却无法推理。层级声明维护起来很昂贵,但在审查时清晰可读。
一条设计规则
绝不要授予 agent 与你会授予人类操作员相同的访问原语。人类拥有 agent 所不具备的判断力;agent 拥有人类所不具备的吞吐量。一个拥有全盘访问权限的人类会读取任务所需的内容然后停下。一个拥有全盘访问权限的 agent 会读取其策略或其 prompt 引导它去读取的内容,其速率是任何人工审查都无法匹敌的。原语应当反映这种不对称,而不是忽视它。
将读取范围限定为满足任务的最窄数据类别。将任何放宽视为需要重新审批的变更,而不是一个配置开关。如果某个 agent 在流水线的某一步需要 personal 数据,那一步应当是一次带有自己清单的独立运行,而不是对整个流水线的更宽泛授权。
优先选择查询形态的接口,而非数据形态的接口。一个返回某个货币对当前汇率的函数是查询形态的:agent 说明它需要什么,并恰好收到那个。一个挂载的 CSV 文件卷是数据形态的:agent 收到一切,并且必须自行决定读取什么。查询形态的接口在调用点就使目的变得明确,而这正是策略需要所在之处。数据形态的接口把决策推给 agent,在那里它对权限模型不可见,而且往往对审计日志也不可见。
Apple 收紧全盘访问是一个信号,表明业界已经注意到 agent 是一类不同的主体。下一步是停止扩展文件系统原语,转而开始表达真正支配暴露的三元组:哪个 agent,读取哪一类数据,出于什么目的。这个三元组就是策略。文件系统只是字节恰好所在之处。
封面照片:Jakub Pabis / Pexels。
