
如今大多数 Agent 框架都默认运行在桌面环境:一个用户、一台机器、一个进程。笔记本电脑打开时 Agent 运行,写入本地文件系统,API 密钥存在环境变量里,终端关闭后进程便终止。出错了用户重试,Agent 需要包时 pip install 直接装进用户的 Python 环境。状态、密钥、生命周期——所有这些都挤在一个可信边界内。
云上的 Agent 基础设施可没有这些奢侈条件。
Agent 运行在一个每次启动都刷新干净的沙箱里,硬件与陌生人共享,调用方是用户从未谋面的东西:定时任务、HTTP 请求、另一个 Agent。运行时用户通常在睡觉。沙箱里的代码可能是恶意的。文件系统必须扛得住部署迭代。凭证不能和 Agent 住在同一个地方。桌面环境白送给你的一切——持久性、身份、网络信任、重试——在云上都要重新建成显式系统。
过去几个月我们在 CREAO 一直在加固这一层。得出两个教训。如果你曾发布过桌面 Agent,并好奇它搬到云上会有什么不同,这就是答案。
教训 1:把变化慢的东西和变化快的东西分开

在桌面上,用户的环境和 Agent 的运行时是同一回事,由同一个人按同样的节奏更新。在云上,则不然。
一个 Agent 应用会在平台侧积累状态。股票分析师安装 matplotlib,下载市场数据,编写图表脚本。那个环境就是 Agent 的肌肉记忆。我们在用户对环境满意的那一刻把它冻结成沙箱快照,然后一直保持冻结,直到用户再次编辑环境。每次运行都从同一个镜像启动。同样的包、同样的文件、同样的版本。周一的运行和周五的运行行为一致,因为底层没有任何东西变化过。
这是桌面框架没法免费给你的特性。六个月前的 pip install 在今天就解析成了不同版本。而一个云快照永远解析为相同的字节。可复现性是平台欠用户的,而冻结快照是实现它的最廉价方式。
然后耦合问题就暴露出来了。
冻结用户环境的同一个镜像里还包含了运行时代码——即我们开发的、负责管理每次 Agent 运行的小型 harness 库。用户希望自己的环境静止不动;我们则希望运行时代码一天能发布很多次。一个制品,两个相反的需求。
我们的第一个修复方案很粗暴。启动时检查快照里的运行时代码是否与我们刚部署的版本匹配。不匹配就扔掉快照,从干净模板启动。这个方案奏效了,没人抱怨。损失只影响部署后的第一次运行。
无人值守的运行打破了这种保护。周一早上 9 点的定时任务不应该因为我们在 8:55 部署了一个版本就丢失它的环境。我们悄悄违反的约定——"你的环境在你主动更改之前一直冻结"——在这里暴露无遗。
这个修复花的时间比我们预期的要长。用户环境和运行时代码的变化速率完全不同。用户按自己的意愿编辑 Agent;我们一天内向平台部署多次。把它们当作同一个制品,每次部署都迫使你做出选择:要么保留陈旧的时代码,要么销毁用户明确要求我们保留的冻结环境。
我们最终采用的模型借鉴了操作系统处理更新的方式。内核变了,但你的家目录不会变。你不会为了安装安全补丁而格式化磁盘。
我们划出了同样的边界。沙箱从用户的冻结快照启动,原封不动。然后我们只热替换运行时代码。流程如下:
- 在沙箱内的临时目录中准备新版运行时代码。
- 用
node --check验证它,确保任何语法错误在触碰任何活跃内容之前就被捕获。 - 原子性替换:解除旧运行时代码上的不可变标志,复制新版覆盖上去,再用
chattr +i重新锁定,然后隐藏chattr二进制本身,防止沙箱代码解除锁定。 - 清除 V8 的编译缓存(
/home/user/.cache/v8-compile-cache/*),确保新文件真正加载,而不是运行过时的字节码。 - 若任何步骤失败,杀死沙箱并用新沙箱重试。绝不允许半升级状态运行 Agent。
整个替换过程大约需要 300 毫秒。只有在运行时代码被替换后,我们才在成功运行后重新创建快照,将更新的代码烘焙进用户镜像,这样下一次运行就完全跳过替换过程。平台部署从不丢弃用户状态,而是把新的运行时代码融入其中。用户的包、文件和自定义内容原封不动地保留下来。
如果你想从这个教训里带点什么走,那就是这个诊断性问题:对于你在云平台上持久化的任何东西,问一问——谁控制着这个制品的变化节奏?如果用户和平台都拥有它,你迟早要为耦合付出代价。沿着所有权边界拆分制品,让各自按自己的时钟更新。
教训 2:把密钥挡在执行边界之外

这个教训把云 Agent 基础设施和其他一切区分开来。
桌面 Agent 以用户身份运行。它用用户的密钥,在用户的机器上,面向用户的网络。云 Agent 以无名氏身份运行,在共享硬件上,面向开放的互联网,执行 LLM 根据可能是恶意的提示词写出的代码。安全模型必须假设沙箱内的代码已经被攻破,而不是祈祷它不会。
我们的规则很简单。任何长期有效的凭证绝不允许存在于沙箱内部。
当 Agent 需要调用一个已认证的服务——Slack、GitHub、用户自己的 API——它不持有令牌。它向运行在沙箱外部的 API 桥发送本地 HTTP 请求。桥在宿主机一侧附加 OAuth 令牌并转发请求。响应返回时,令牌从未进入沙箱的内存或环境。
有趣的部分是桥如何知道沙箱有权发起请求。两层检查,故意叠加。
第一层,IP 白名单。桥仅接受来自我们沙箱宿主机所在内部网络范围的连接。来自其他任何地方——开发者的笔记本、泄露的 URL、公共互联网——的调用在网络层就被丢弃,应用代码根本不执行。这把桥固定在一套物理基础设施上,对外部任何人都毫无价值。
第二层,每次运行时铸造的短期 JWT。沙箱启动时,平台签署一个令牌,作用于该次特定运行:哪个用户、哪个应用、哪个会话,有效期仅覆盖运行窗口。沙箱在每次桥调用时出示该令牌。桥验证签名、检查有效期,然后才解析用户存储的凭证并在服务器端附加。如果沙箱被劫持,攻击者继承的令牌随运行结束而失效,并且只授权该会话范围内的调用。没有主凭据可窃取。
同一个桥也向外传输计费扣减、日志和指标,因此它是穿越沙箱边界双向交互的唯一接口。沙箱内的其他一切默认被视为已被攻破。
如果明天一次提示注入让 Agent 把 process.env 倾倒到某个 webhook,攻击者只会得到一个短命 JWT,仅能从我们内部网络使用,并且伴随运行结束而失效。正是这个属性让我们能在共享基础设施上运行不可信的用户代码而不用担心。
底层模式
可靠、安全的云 Agent 基础设施并不是什么新奇系统。它只是几个毫不妥协地坚持的属性:
- 状态存在沙箱里,冻结直到用户改变它。
- 代码可热替换,与状态无关。
- 凭证活在宿主机侧,永远不在 Agent 内部。
- 一条执行管道服务于所有调用方,无论触发者是人、调度器还是其他软件。
最后一个属性是整个设计的关键。一个 executeAgent 函数处理 UI 点击、定时运行和 API 调用。计费系统、积分扣减日志、可观测性信号——无论触发者是用户点击了 Run、定时任务触发,还是脚本调用了 API,全都一样。增加一个新的触发面只是路由变更,不是架构变更。Agent 自己并不关心是谁扣动了扳机。
这正是桌面框架无法给你的,也是值得构建云版本的原因。笔记本上的 Agent 受限于笔记本。云上的 Agent 是一个你整个技术栈都能调用的函数。用户编写一次。平台让它经得起部署、在共享硬件上安全运行、接受用户从未预料到的调用方。
Agent 是一个带自然语言接口的函数。它的实现属于用户;它的触发面、运行时、安全边界属于平台。要做的就是构建各层,让每一层按自己的时钟演进,并把时间花在别人攻破之前找出系统间的裂缝。
这就是让我们能廉价且安全地交付下一个触发面的原因。





