
今天的大多数 Agent 框架都默认自己运行在桌面环境里。一个用户,一台机器,一个进程。Agent 在笔记本打开时运行,写入本地文件系统,把 API key 放在环境变量里,然后在终端关闭时一起消失。出问题了,用户自己重试。Agent 需要某个包时,pip install 会把它装进用户自己的 Python。状态、密钥和生命周期,全都待在同一个可信边界里。
云端 Agent 基础设施没有这些便利。
Agent 运行在一个每次全新启动的沙箱里,底层硬件和陌生人共享,并且由用户从未见过的调用方触发:一个定时任务、一个 HTTP 请求,或者另一个 Agent。运行发生时,用户通常正在睡觉。沙箱里的代码可能是对抗性的。文件系统必须跨部署存活。凭据不能和 Agent 住在同一个地方。桌面环境免费给你的每一项保证,包括持久化、身份、网络信任和重试,都必须被重新构建成显式的系统。
过去几个月,我们在 CREAO 一直在收紧这一层。最后得到了两个教训。如果你曾经交付过桌面 Agent,并且想知道它搬到云端后到底会变什么,那变化就在这里。
Lesson 1:把变化慢的东西和变化快的东西分开

在桌面上,用户的环境和 Agent 的运行时是同一回事,由同一个人以同样的节奏更新。在云端,它们不是。
一个 Agent 应用会在平台侧积累状态。比如一个股票分析 Agent 安装了 matplotlib,下载了市场数据,写了画图脚本。这个环境就是 Agent 的肌肉记忆。我们会在用户满意的那一刻把它冻结成一个沙箱快照,并且在用户再次编辑环境之前一直保持冻结。每一次运行都从同一个镜像启动。相同的包、相同的文件、相同的版本。周一的运行会像周五一样,因为底下没有任何东西移动过。
这是桌面框架无法免费提供的属性。六个月前的一次 pip install,今天解析出来的版本可能已经不同了。云端快照则永远解析成同一组字节。可复现性是平台欠用户的一项能力,而冻结快照是交付它成本最低的方式。
然后,耦合问题出现了。
冻结用户环境的同一个镜像,也包含 runner 代码,也就是我们开发的一小段 harness 库,用来在每次运行中管理 Agent。用户希望自己的环境保持不动。我们希望 runner 一天发布很多次。一个产物,两个相反的需求。
我们的第一个修复很粗暴。启动时检查快照里的 runner 是否匹配我们刚刚部署的版本。如果不匹配,就丢弃这个快照,从干净模板启动。它能工作,也没人抱怨。损失只会发生在部署后的第一次运行。
无人值守的运行让这个遮羞布失效了。周一早上 9 点的 cron job,不应该因为我们 8 点 55 分部署了一次,就失去它的环境。我们悄悄违反的契约其实是:「你的环境会一直冻结,直到你自己修改它。」
这个修复花了我们比预期更久才看清楚。用户环境和 runner 代码的变化速率完全不同。用户在自己选择的时候编辑 Agent。我们一天多次部署平台。把它们当成同一个产物,会迫使每次部署都做一次选择:要么保留过期的 runner 代码,要么摧毁用户明确要求保留的冻结环境。
我们最终采用的模型,借鉴了操作系统处理更新的方式。内核会变。你的 home 目录不会。你不会为了安装一个安全补丁而擦掉整块磁盘。
我们画出了同样的边界。沙箱从用户被冻结的快照启动,快照本身不动。然后只热替换 runner。流程是:
- 在沙箱内的临时目录中暂存新的 runner。
- 用
node --check验证它,这样任何语法错误都会在碰到线上文件之前被发现。 - 原子替换:解除旧 runner 的 immutable flag,把新文件复制过去,再用
chattr +i重新加锁,然后把chattr二进制本身藏起来,让沙箱代码无法反向解除锁定。 - 清理 V8 的编译缓存(
/home/user/.cache/v8-compile-cache/*),确保加载的是新文件,而不是旧字节码。 - 如果任何一步失败,杀掉沙箱并用新的沙箱重试。任何半升级状态都不会真正运行 Agent。
整个替换大约需要 300 毫秒。一次成功运行后,只有在 runner 代码发生过替换时,我们才会重新生成快照,把更新后的代码烘进用户镜像里,这样下一次运行就可以完全跳过替换。平台部署永远不会丢弃用户状态;它们只是把新的 runner 合入其中。用户的包、文件和自定义内容都会原样保留下来。
如果你只从这一课里带走一件事,那就是这个诊断问题:对于云平台中任何需要持久化的东西,问问自己:这个产物的变化节奏由谁控制?如果用户和平台都拥有它,你迟早会为这种耦合付出代价。沿着所有权边界拆分产物,让双方各自按照自己的时钟更新。
Lesson 2:让密钥留在执行边界之外

这是把云端 Agent 基础设施和其他一切区分开的教训。
桌面 Agent 以用户身份运行。它使用用户的 key,在用户的机器上,访问用户的网络。云端 Agent 则是在共享硬件上、面向开放互联网、以「无人」身份运行由 LLM 根据 prompt 写出的代码,而这个 prompt 可能本身就是对抗性的。安全模型必须假设沙箱内的代码已经被攻破,而不是祈祷它不会被攻破。
我们坚持的规则很简单:任何长期凭据都绝不进入沙箱。
当 Agent 需要调用一个需要认证的服务,比如 Slack、GitHub,或者用户自己的 API,它并不持有 token。它会向沙箱外部运行的 API bridge 发起一个本地 HTTP 请求。bridge 在宿主机侧附加 OAuth token,然后转发调用。响应再返回给沙箱,而 token 从未进入过沙箱的内存或环境变量。
有意思的部分是:bridge 如何知道这个沙箱有权请求?我们故意叠了两层检查。
第一层是 IP allowlist。bridge 只接受来自沙箱宿主机所在内部网络范围的连接。来自其他任何地方的调用,无论是开发者笔记本、泄漏的 URL,还是公网,都会在网络层被丢弃,甚至不会进入应用代码。这把 bridge 锚定在一块特定的物理基础设施上,也让它对外部任何人都没有用。
第二层是每次运行生成的短期 JWT。沙箱启动时,平台会签发一个限定在本次运行范围内的 token:哪个用户、哪个应用、哪个 session,以及只覆盖本次运行窗口的过期时间。沙箱每次调用 bridge 都要带上它。bridge 验证签名,检查过期时间,然后才解析用户存储的凭据,并在服务端附加它们。如果沙箱被劫持,攻击者拿到的也只是一个随本次运行一起死亡、并且只授权这个 session 调用的 token。没有可以偷走的主凭据。
同一个 bridge 也负责把计费扣减、日志和指标带出去,所以它是唯一一个跨越沙箱边界的双向接口。除此之外,沙箱内的一切都默认按已被攻破处理。
如果明天某个 prompt injection 诱导 Agent 把 process.env dump 到一个 webhook,攻击者拿到的也只是一个只能从我们内部网络使用、并且会随运行结束而过期的短期 JWT。正是这个属性,让我们可以在共享基础设施上运行不可信用户代码,同时不至于睡不着。
底层模式
可靠、安全的云端 Agent 基础设施并不是什么全新的系统。它只是几条属性被毫无例外地坚持了下来:
- 状态生活在沙箱里,并且冻结到用户主动修改为止。
- 代码可以热替换,并且独立于状态。
- 凭据生活在宿主机侧,永远不进入 Agent。
- 同一条执行流水线服务所有调用方,无论触发者是人、调度器,还是另一段软件。
最后一条就是整个设计的点睛之笔。一个 executeAgent 函数处理 UI 点击、定时运行和 API 调用。计费系统、credit 扣减日志、可观测性信号,无论是人点击了 Run、cron 被触发,还是脚本调用了 API,都完全一样。增加一个新的触发表面只是路由变化,而不是架构变化。Agent 自己并不知道,也不关心是谁扣动了扳机。
这就是桌面框架无法给你的东西,也是云端版本值得构建的原因。笔记本上的 Agent 被绑定在笔记本上。云端 Agent 则是你的技术栈中其他部分可以调用的一个函数。用户只写一次。平台让它跨部署存活,在共享硬件上安全运行,并接受用户从未预料过的调用方。
Agent 是一个带自然语言接口的函数。它的实现属于用户。它的触发表面、运行时和安全边界属于平台。真正的纪律,是把这些层构建到可以各自按自己的节奏演进,并且花时间在别人发现裂缝之前,先把系统之间的裂缝找出来。
这才是让下一个表面既便宜、又安全地交付的原因。