来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
本集属于技术侧(S2)模块 T2「解剖 Grok Build:Rust 写的生产级 Coding Agent」,取材自 3 节课:
这三节课有一条共同的主线:凡是听起来像一个「等级」的东西,拆开看往往是一组正交维度。隔离如此,沙箱 Profile 如此,多 Agent 的组织方式也是如此。
读完本集,你应该能够:
| 能力 | 具体表现 |
|---|---|
| 拆解隔离维度 | 不再用「隔离强弱」一把尺子,而是分别判断上下文来源、恢复定位、文件空间、沙箱能力 |
| 选择上下文来源 | 知道何时用 New、何时用 Resumed,以及 Resumed 会复制什么、不复制什么 |
| 预判恢复失败 | 能提前判断哪些情况会触发「失败关闭」而不是「尽力而为」 |
| 选择隔离模式 | 区分 None 与 Worktree,知道 worktree 可被快照重新水化 |
| 设计多 Agent 组织 | 先看任务图(依赖 / 上下文 / 文件冲突 / 汇总成本),再选产品机制 |
| 读懂 Profile | 能按 capability set 而非名称判断 Workspace / Devbox / ReadOnly / Strict / Off 的真实边界 |
| 配置 custom | 会写 extends 与 deny,并知道项目无法覆盖全局同名定义 |
| 诚实评估沙箱 | 知道哪些环节会退化,以及为什么「无法绕过」不能承诺 |
隔离不是一个等级,而是一组正交的维度。把它们合成一个「隔离等级」,容易误读恢复和 worktree 的真实行为。
同一个 Resumed 子 Agent,可以复用 worktree,也可以继承普通 cwd。上下文连续 ≠ 文件空间隔离,两者必须分别判断。
| 维度 | 控制什么 | 源码载体 | 取值 |
|---|---|---|---|
| 上下文来源 | 信息是否从父会话/同级带过来 | ContextSource | New / Resumed |
| 恢复定位 | 这次恢复靠什么找回来 | ResumeSourceData | 身份 / 目录 / 会话 / 快照 |
| 文件空间 | 改动落在哪个目录树 | SubagentIsolationMode | None / Worktree |
| 能力边界 | 能读什么、写什么、能否联网 | ProfileName + capability set | 五种 + Custom |
ContextSource::New新会话,不继承历史。spawn 流程建立新的 system prompt 与 prompt context,再接收当前任务输入。
关键澄清:New 不代表独立文件空间。文件空间仍取决于 isolation 与 cwd。这是第一个高频误读。
ContextSource::Resumed从已完成的 peer subagent 继续。源码复制原始 transcript 与 tool state,模型沿用 source model;system prompt 与 prompt context 根据当前 AgentDefinition 重新渲染。
组合起来是:历史是旧的,包装是新的。你复活的是一个有记忆的角色,但它当前的身份与行为约束由这次的定义决定。
公开枚举只有 New 与 Resumed。shell 内部还有 InitialContextSource::Forked,用于从父会话镜像或摘要上下文,属于另一条 bootstrap 分支,不应被伪装成公开枚举成员。
写技术文档或做架构描述时,严格区分「公开枚举」与「内部实现分支」。内部类型会变,公开枚举是契约。把 Forked 塞进 ContextSource 去描述,等于给了一个随时会被打破的承诺。
ResumeSourceData 携带四类信息| 类别 | 字段 | 用途 |
|---|---|---|
| 身份 | subagent_id、type、persona、model_id | 校验与沿用 |
| 目录 | child_cwd、worktree_path | 定位工作空间 |
| 会话数据 | child_session_id | 定位 transcript 与 tool state |
| 快照 | snapshot_ref | 重建已删除的 worktree |
subagent_type 必须与 source 一致;Persona 必须与 source 一致;未显式给出则沿用 source;model 不参与身份 gate——请求中的 model override 会被软忽略,并 pin 到 source model。第三条最反直觉:你没法在恢复一个子 Agent 时顺手给它换脑子。这是有意设计。
这三条限制背后是一致的设计态度:要么成,要么败。80% 那条尤其值得抄进自己的系统——恢复完马上就要溢出,不如早点拒绝。给你的恢复流程加上同样的阈值检查与显式的失败返回,比事后排查「为什么 Agent 忽然失忆」要便宜得多。
SubagentIsolationMode 只有两个成员:
| 成员 | 语义 | 常见误读 |
|---|---|---|
None | 使用解析后的 cwd,通常与父工作区相同 | ✗ 误以为等于共享对话历史。实际上上下文窗口依然是独立子会话 |
Worktree | 使用独立 worktree 路径 | ✗ 误以为枚举里还有 sandbox 成员 |
Worktree 的恢复:优先复用 source worktree;路径已移除且存在 snapshot_ref 时,可从持久 git ref 重新水化。
沙箱不在这个枚举里——它由 Profile 控制,是另一层机制。
AgentDefinition + PersonaAgentDefinition:prompt、工具、权限、模型、MCP 继承、可 spawn 类型——一份合同Persona:行为指令、I/O 契约、部分运行时默认值解析顺序:type → definition → role/persona runtime config
两者合起来解决一个问题:让每个子 Agent 有可观察的身份与能力边界。不是一句「你是 reviewer」就完事,而是工具、权限、外部连接全部可枚举、可审查。
SubagentEvent
start_subagent_coordinator → 只启动一次 drain task
每个 Spawn 事件 → 启动本地异步任务 → handle_subagent_request
六类事件:Spawn · Query · Cancel · ListActive · Completions · Outstanding
| 关注点 | 机制 |
|---|---|
| 并行 | Spawn 独立进入 spawn_local,协调器登记 pending / active / completed。并行度来自异步任务,不受 Persona 数量限制 |
| 等待 | Query 可立即返回快照,也可注册 block wait slot 并轮询 |
| 完成 | Completions drain 待通知项,按 suppress_ids 过滤 |
| 取消 | Cancel 支持按 subagent ID 或 parent prompt ID;协调器淘汰过期记录并记录显式 kill |
可对照的是产品表面能力:Claude Code 公开支持自定义 subagents,每个可拥有独立上下文、system prompt、工具权限与模型,主会话可自动委派或由用户显式调用;公开的 Agent Teams 描述包含共享任务、成员间消息与独立上下文。
不把 Coordinator 或 Swarm 当作其内部类型,也不推断调度器实现。
| 策略 | 适用 |
|---|---|
| 单主会话委派 | 一个 owner 统一拆解、串联依赖并汇总 |
| 主会话 + subagents | 子任务互不依赖(如检索 8 个模块后汇总风险清单) |
| 多成员共享任务 | 成员需要彼此通信、认领共享任务(如三服务并行迁移需同步接口变更) |
判据:任务依赖 · 上下文需求 · 文件冲突 · 结果汇总成本。
顺序不能反。先选了机制再硬套任务,是多数多 Agent 项目跑偏的起点。实操上,把任务画成一张有向图:有边就有依赖,就串行或等待;无边就并行派生;只有在「成员之间需要互相同步」时才引入共享任务与成员通信——这一步的成本最高,不该默认开启。
名称只提供方向,真实边界要看解析后的 capability set。
| Profile | 默认读 | 可写 | 子进程网络 |
|---|---|---|---|
Workspace | 全文件系统可读 | workspace、GROK_HOME、临时目录 | 不限制 |
Devbox | 全文件系统可读 | 枚举根目录广授(/data 与 VFS 除外) | 不限制 |
ReadOnly | 全文件系统可读 | GROK_HOME、临时目录、必要设备(workspace 不可写) | 限制 |
Strict | 关闭全局默认读,只开放系统运行目录与 workspace | workspace、GROK_HOME、临时目录 | 限制 |
Off | 跳过 capability set 应用,仅记录「Sandbox disabled」;接受别名 none;不可作为 custom 基类 | — | — |
workspace 仍允许读取工作区之外的文件——它不是「只读工作区」的笼子;strict 仍允许写 workspace——strict 严格的是读,不是写。read-only 也保留了运行所必需的最小写目录。
看到 read-only 就以为哪儿都写不了,看到 strict 就以为哪儿都不能动,这两种推断都是错的。凡是靠字面补全的规则,都是错的规则。评估一个沙箱时,一定去读它的 resolve() 结果、可写路径清单与 deny 列表,而不是读它的名字。
extends 四条规则custom 默认从 workspace 开始;extends:workspace / devbox / read-only / strict;extends off / none;extends 另一个 custom。read_only、read_write、deny 追加到基类之上。需要限制子进程网络时,必须在 custom 中显式设置 restrict_network=true——继承不会替你做。
读取顺序:~/.grok/sandbox.toml → .grok/sandbox.toml
项目配置:只能新增 profile 名称
同名冲突:merge 使用 entry.or_insert,全局定义保持生效
项目无法悄悄削弱用户全局已经定下的同名策略。 这堵住了一个很现实的攻击面:恶意仓库往项目配置里塞一个同名但更宽松的沙箱定义。
| 层面 | 实现 |
|---|---|
| 文件系统 | 启用 enforce 且运行在 Unix 时,capability set 应用到 Landlock 或 Seatbelt;macOS deny 用 Seatbelt 规则;Linux 子路径 read-deny 还需 bwrap bind-over |
| 网络 | 主进程网络保持开放以访问模型 API;restrict_network 通过子进程过滤表达;源码中的 seccomp 实现在 Linux 生效,非 Linux 为空操作 |
若平台不支持、构建未启用 enforce,或内核层 Sandbox::apply 失败,源码会记录警告并继续运行。
但要注意区分:能力集与 profile 解析这一步是用 ? 上抛的,失败时 apply 直接返回 Err,不是静默降级。因此只有 is_active() 才能反映是否实际应用。
结论:不能承诺所有环境都「无法绕过」。这句话不性感,但它是诚实的。
New 当成「独立文件空间」?把 None 当成「共享对话历史」?type 与 Persona,且不对 model 做放行?restrict_network(如需要)?is_active() 之类的运行时状态来判定沙箱真的生效,而不是看配置写了什么?本集内容受以下约束限制:
xai-grok-subagent-resolution、xai-grok-shell、xai-grok-sandbox、xai-tool-types),实现可能随版本演进,落地前请以当前版本源码为准。工程上真正可靠的安全,不来自某个听起来很厉害的名字,而来自一组被明确定义、并且可以被逐条验证的边界。
把压缩成一个等级的东西拆开,往往会发现之前想不明白的问题突然就清楚了。
*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*
*本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版*