动态沙箱要保连续性
一句话结论
当 Agent runtime 从“固定工作目录”升级成“按请求动态分配沙箱”后,真正决定系统是否可用的,不只是隔离性本身,而是你能不能同时守住三件事:提示词稳定、后台任务连续、沙箱生命周期可追责。Mastra 这周把这三件事一起做进了 Workspace API,我觉得这是一个很强的工程信号:多租户 Agent 的难点,已经从“能不能跑起来”转向“隔离和连续性能不能同时成立”。
背景:为什么动态沙箱现在变重要了
过去很多 coding agent / tool agent 默认只有一个静态执行环境:同一个 workspace、同一套进程管理器、同一个文件系统视图。这样做实现简单,但一旦场景变成多租户、多项目、多权限级别,就会立刻碰到两个问题:
- 不同用户共用一个执行环境,隔离边界很脆。
- 一旦你把沙箱按请求动态分配,长任务、后台进程、流式会话又容易丢连续性。
Mastra @mastra/core@1.41.0 这次的更新很有代表性:Workspace.sandbox 不再只能是静态实例,而可以是 ({ requestContext }) => WorkspaceSandbox 这样的 resolver。也就是说,运行时终于承认“这次请求该进哪个沙箱”本身就是一个动态决策,而不是配置文件里一次性写死的东西。
这件事值得关注,不是因为 API 多了一个函数签名,而是因为它把 Agent runtime 的一个真实矛盾显式化了:你想按用户/线程/租户切沙箱,就必须重新设计 prompt、process continuity 和 ownership。
核心机制:不是“每次请求换个目录”这么简单
1. 沙箱选择进入 request context
Mastra 允许你在 resolver 里根据 requestContext 选择具体 sandbox,比如按 user-id 映射工作目录或权限集。这意味着沙箱不再是 agent 的静态附属物,而是一次执行上下文的一部分。
这背后的工程意义是:
- 隔离边界开始从“哪个 agent 实例”转到“哪个请求主体”。
- 运行时终于可以把多租户和权限域放进同一层抽象,而不是靠外部网关拼接。
- 对 OPC 这类内部 Agent 平台来说,
sandbox resolver实际上就是 capability resolver 的执行环境版本。
也就是说,未来真正稳的系统不是“一人一个 agent 进程”,而是“一个 runtime,根据主体和任务路由到不同能力边界”。
2. Prompt 不能因为动态沙箱而失稳
这里最容易被忽略的一点,是 Mastra 明确写了:构造 workspace instructions 时默认不会调用 sandbox resolver。相反,它只放一个稳定 placeholder;如果你真想把具体沙箱信息写进 prompt,要显式开启 instructions.dynamicSandbox。
这很关键,因为很多团队一做动态环境,第一反应就是把“当前目录、可写路径、挂载信息、租户上下文”全塞进 system prompt。这样短期看起来更透明,长期却会同时破坏三样东西:
- prompt cache 命中率下降;
- 相同任务无法稳定复现;
- 沙箱构建副作用被 prompt 生成阶段提前触发。
Mastra 的这个默认值其实在表达一个更成熟的 runtime 观念:执行环境可以是动态的,但给模型看的环境描述应该尽量稳定。 具体环境差异,只有在模型真的需要时再按受控方式注入。
对 Dewey/OPC 的启发很直接:如果以后要做 per-user workspace、临时 repo、隔离工具目录,应该先区分“模型需要知道的能力边界”和“运行时内部真正使用的物理环境”。这两者不是一回事。
3. 后台任务连续性必须显式建模
动态沙箱最麻烦的问题,往往不是首轮调用,而是第二轮。比如模型先 execute_command(background=true) 起了一个长任务,下一轮再来 get_process_output 或 kill_process。如果这两次请求碰巧被路由到不同 sandbox,那个 PID 在逻辑上就“失踪”了。
Mastra 为此新增了 sandboxCacheKey:让 follow-up 请求按一个稳定 key 重新绑定到同一个 sandbox,而不是依赖某个短命的 RequestContext 对象实例。这其实是在说:
- 长任务的连续性不能依赖连接对象;
- 也不能依赖“当前正好还在同一台 worker”;
- 必须依赖一个 runtime 级别、可重建、可审计的 continuity key。
这个设计和最近很多 Agent runtime 信号是一致的:连接会断、进程会迁移、流式响应会恢复,但后台任务必须有 stable identity。
为什么 untilIdle 也值得一起看
同一个版本里,Mastra 还把 streamUntilIdle() 折叠进普通 stream() / resumeStream(),变成一个统一的 untilIdle 选项。表面上这是 API 收敛,实际上它透露了另一个判断:“等待后台任务继续产出”不该被建模成另一套独立接口,而应被视为标准流式执行的一个模式。
这和动态沙箱放在一起看,就更有意思了:
sandboxCacheKey解决“我还能不能回到原来的执行环境”;untilIdle解决“流要不要为后台任务继续保持可恢复语义”。
两者组合后,runtime 对长任务的抽象才开始完整:同一条任务线不只是能启动,还要能继续观察、继续恢复、继续终止。
常见误区
误区一:动态沙箱只是安全隔离问题
不是。它同时是权限问题、调度问题、状态归属问题、可观测性问题。只做目录隔离,不做 continuity key、ownership 和 trace 绑定,最后会变成“隔离是有了,但任务无法恢复”。
误区二:把真实环境细节都写进 prompt 更安全
这通常更脆。模型知道得越多,不代表控制越强;反而容易把高基数、易漂移、带副作用的信息变成 prompt 噪声。更稳的做法是:prompt 只暴露稳定能力边界,具体物理环境在工具执行层处理。
误区三:后台任务状态跟着 session 走就够了
也不够。session 很可能只是交互容器,不是执行环境标识。真正可靠的连续性通常要绑定 thread、task、tenant 或 principal,而不是某次 HTTP 请求对象、某条 websocket 连接,甚至不是某个瞬时 session id。
如果 OPC 要落地,可以怎么做
我会把“动态沙箱连续性”拆成一个最小运行时合同:
- Resolver 层:输入
principal / project / thread / task type,产出 sandbox、工具白名单和权限域。 - Prompt 层:默认只注入稳定能力描述,不直接注入高波动的物理路径和挂载细节。
- Continuity 层:所有后台任务都拿到
continuity_key,后续读日志、取输出、终止任务都必须带它。 - Ownership 层:记录谁创建了这个 sandbox、谁能复用、何时销毁、失败后谁负责清理。
- Tracing 层:把 sandbox id / continuity key / tool name / thread id 绑定进 trace,而不是只留“agent step failed”。
如果这五层没分开,系统早晚会在两种失败里选一个:
- 要么为了连续性而偷共享环境,最后租户串线;
- 要么为了隔离而每次都新建环境,最后长任务不可恢复。
我对这条信号的核心判断
Mastra 这次更新的价值,不在于“又支持了一种 sandbox 配置方式”,而在于它把下一代 Agent runtime 的三个基本约束摆到台面上了:
- 隔离必须按请求主体生效;
- prompt 不能因此失去稳定性;
- 长任务连续性必须脱离连接对象,进入显式 runtime contract。
这比单纯讨论“本地跑还是远程跑”“一个 agent 一个容器还是一个线程一个容器”更接近真实工程问题。因为团队一旦进入多租户、多人协作、长任务和后台工具并存的阶段,真正难的从来不是启动一个沙箱,而是让它在安全、缓存、可恢复之间不互相打架。
留给 Dewey 的问题
如果 OPC 未来要支持“同一个 Agent runtime 服务多个项目成员,并允许长任务跨轮次继续执行”,你最想先固定下来的 continuity key 是什么:thread、task、principal,还是 project+principal 的组合?为什么?