Small World:每个 agent 都活在自己的投影里

AFS 是我们为 agent 做的文件系统抽象:数据库、API、本地文件、工具、记忆,都变成 agent 可以挂载和读写的路径。它里面有一条我们越想越觉得重要的设计原则,叫 Small World。一句话说清楚:每个 agent、每个应用、每个进程,看到的都不是全局世界,而是专门为它投影出来的一个小世界。不在这个世界里的东西,对它来说不是「没权限」,而是根本不存在。
传统权限模型的回答是 Permission Denied:我知道那里有东西,但不给你看。Small World 的回答是 Not Found:你问的这个东西,在你的世界里没有。
这两个回答的差别,比看上去大得多。前者意味着 agent 知道墙外有东西,可以枚举、可以推理、可以想办法绕;后者意味着它要找的东西在它的世界里根本不存在。越界访问不是被拦住了,是问了也只会得到 Not Found,连「墙后面有东西」这个信息都拿不到。这篇讲讲这个理念从哪来、为什么它不只是一个安全机制,以及你怎么亲手试一试。
现实世界本来就没有 god view
你的医生知道你的病历,银行知道你的账户,税务局知道你的收入,朋友知道你周末去了哪。没有任何一个人、任何一个机构,看到关于你的全部真相。每个人都只能看到以自己为中心的那个小世界,而世界照样运转,运转了几千年。
反倒是计算机系统里,我们习惯了另一套东西:root、admin、super user。总有一个角色能看到一切。这套习惯搬到 agent 时代会出问题,因为 agent 不是一个两个,是成百上千个,每个都可能被劫持、被注入、被社会工程。给每个 agent 都留一条通往「上帝模式」的路,等于给攻击者留了成百上千扇门。
所以 Small World 的立场是:去中心化不只是数据没有中心,观察世界的方式也没有中心。每个 observer,不管它是人、agent、应用还是进程,都活在自己的投影里。这不是限制,这是现实世界本来的样子。
从 Everything is a File 到 Everything is a Projection
这个想法不是凭空来的。Unix 在 1970 年代给出了一个伟大的抽象:everything is a file。到了九十年代,贝尔实验室的 Plan 9 更进一步:文件系统的 namespace 不是全系统唯一的,每个 process 可以拥有自己的一份。同一个路径 /dev/mouse,在不同窗口里的程序看来是不同的鼠标输入流。程序不知道真实硬件在哪,也不知道别的窗口存在。从程序自己的视角看,它的 namespace 就是全世界。
AFS 深受这两个系统影响,Small World 的技术祖先可以清楚地追溯到 Plan 9 的 per-process namespace。但我们做的不是复刻,是推广:从 process 推广到任何 observer:agent、应用、blocklet(我们平台上的应用单元)、workflow,还有人。从文件资源推广到数据、能力、工具、记忆、身份。AFS 自己也是沿这条线走过来的:先是 everything is a path,数据库、API、本地文件都能用同一套路径找到;Small World 再往前一步,everything is a projection,连「你能看到什么」本身,也由投影决定。
Projection 这个词是认真选的。它不是 copy,不持有数据;它只是同一份数据被某个 observer 看到的方式。数据库里同一个 customer,CRM 看到的是客户关系,财务看到的是应收账款,客服看到的是工单历史。Reality 只有一份,投影可以有无限个。
现实一份;观察者各自拿到投影,不是各自拷贝一份宇宙。
我一开始把它当安全机制
老实说,这篇文章的初稿不长这样。写这篇之前,我先在 issue 里记了一段草稿,讲的全是隔离:blocklet 之间互相看不见、权限不会渗透、不可能溢出。都对,但都是从「防」的角度说的。
后来把这段草稿丢给 AI 接着聊,它把我顶了回来:你这段讲的全是隔离,但我们之前聊出来的东西,比隔离还要往前一层。隔离是结果,不是本质。本质是 reality 如何被观察:AFS 描述的是 reality,Small World 描述的是每个 observer 所看到的 reality。
它说得对。这也是我们做内容的常态:判断是我的,但 AI 有时候会拿着我自己说过的话来纠正我 🤣。
想通这一层之后,很多东西的位置就变了。对一个 observer 来说,projection 不是「被裁剪过的全局文件系统」,它就是全部的 reality;AFS 给 agent 的,就是它的整个宇宙。安全不再是目的,只是这个设计顺便带来的性质。
它不是理论,是跑在代码里的
今年 4 月,我们在做多 agent 协作(父 agent 把活分给一组子 agent 那种场景)的时候,撞上一个具体问题:子 agent 拿到的路径,在它自己的环境里解析不通。修这个问题的时候,我们没有给它打一个专门的补丁,而是把 Small World 的一条性质写进了实现:小世界是递归的。父 agent 的小世界,再投影一次,就是子 agent 的小世界。对子 agent 来说,父亲的世界就是「全局」,但它连父亲的存在都不知道,更碰不到真正的全局。
小世界是递归的:子 agent 看不到真正的全局。
Blocklet 也是一样。同一个用户、同一台机器、同一个 runtime 里跑的两个 blocklet,一个看到自己的 /data,另一个也看到自己的 /data,底层数据可能有共享的部分,但各自都只活在自己的世界里。这不是传统 sandbox 那种「围起来」,是每个 blocklet 的世界从一开始就是分别投影出来的。
机制本身其实很小。一个投影就是几个很小的算子组合出来的:filter 裁掉看不见的路径,rewrite 改写路径的形状,restrict 收紧读写权限,再用 compose 串起来。算子的组合是声明出来的,不是散落在代码各处的特例逻辑。这套东西在 @aigne/afs 里有完整的实现和测试。
想试试的话
用发布在 npm 上的包就能复现开头说的那个差别。装两个包:
npm install @aigne/afs@2.0.0-beta.15 @aigne/afs-json@1.1.0准备一份「reality」,里面同时有 mail 和 notes 两块数据,然后给 mail agent 投影一个只含 /mail 的只读世界:
import { compose, filter, restrict } from "@aigne/afs";
import { AFSJSON } from "@aigne/afs-json";
// 底层 reality:一份数据,mail 和 notes 都在里面
const reality = new AFSJSON({ jsonPath: "./reality.json" });
// mail agent 的 small world:只看得见 /mail,而且只读
const world = compose(
filter({ allow: ["/mail", "/mail/**"] }),
restrict({ mode: "readonly" }),
)(reality);
await world.list("/"); // 只有 /mail,/notes 不存在
await world.read("/mail/inbox.md"); // 正常读到内容
await world.read("/notes/todo.md"); // 抛错
await world.write("/mail/inbox.md", { content: "x" }); // 抛错实际跑出来的两个报错值得看一眼:
read /notes/todo.md → Path not found: /notes/todo.md
write /mail/inbox.md → Operation "write" denied — mount access mode is "readonly"注意第一个报错。它不是 Permission Denied,是 Path not found。对这个 agent 来说,/notes 压根不在它的世界里。这就是 Small World 和 ACL 的区别,浓缩在一行报错里。第二个报错则说明另一半:世界之内,读写权限依然存在,denied 只出现在这里。世界外谈存在,世界内才谈权限。
边界:这个设计放弃了什么
Small World 不是免费的,有几件事值得先说清楚。
第一,没有免费的全局视图。放弃 god view 之后,审计、调试、运维这些原来「顺手就有」的能力,都得专门建设。AFS 的做法是给每个投影留了反射接口,世界可以查到自己是怎么被投影出来的:自己的投影链、当前生效的策略。投影之间创建关系的管理员审计视图,设计里画好了,还没打通。但有一点现在就能说清楚:管理员也只是一个拿到更宽投影的 observer。更准确的表述是,全局视图可以作为实现细节存在,但它不是任何 observer 的默认世界。
第二,排错的方式变了。报错是 Not Found 而不是 Permission Denied,意味着「为什么我看不到这个路径」这个问题,不能再靠查权限表来回答,得先问「我的世界是怎么投影出来的」。习惯 ACL 的人第一次遇到会觉得绕。
第三,它收的是爆炸半径,不是骗术。agent 在自己的世界内部,仍然可能被 prompt injection 骗着做坏事。Small World 保证的是:被骗的 agent 也拿不到世界之外的任何东西,损失被限制在这个小世界的边界内。而边界画在哪,是投影时声明出来的。比如子 agent 的世界默认是父亲世界的只读投影,父亲愿意共享的路径它默认看得见;想让它活在更小的世界里,就得声明更窄的投影。指望它防住所有攻击,是指望错了地方。
另外坦白讲,这套东西的工程细节还有没定的部分,比如投影的缓存策略、跨系统联邦的场景。算子的组合顺序也不满足交换律,先裁剪再改名和先改名再裁剪,结果可能完全不同,声明的时候要想清楚。
写到这里,可以把开头那句话换个说法再说一遍。传统模型问的是「你有没有权限看」,Small World 问的是「你的世界里有没有这个东西」。前者永远在门口检查证件,后者直接决定门后面有什么。我们觉得后者才是 agent 时代该有的默认,不是因为它更安全(虽然它确实更安全),而是因为它更接近世界本来的样子。
没有人活在 god view 里。agent 也不必。
参考
本页涉及
术语
-
AFS
AFS(Agentic File System)把与任务有关的文件、服务和正在进行的工作组织成可查看的资源视图。它不是把一整台机器或一堆 API 交给 agent,而是给任务一块有名字、有边界的工作范围。
-
Agent
当 agent 代表人或服务完成一项工作,真正需要说明的是三件事:谁在行动、代表谁、这次行动被允许做什么。把它们分开,授权和后续核验才有明确对象。
-
Small World
每个 agent、每个应用、每个进程,看到的都不是全局世界,而是专门为它投影出来的一个小世界。不在这个世界里的东西,对它来说不是「没权限」,而是根本不存在。
-
上下文
一项任务的 context 包括相关资料、可用工具和允许的操作。它们被放进清楚的范围里,agent 才知道该从哪里找信息、能用什么完成工作。