M03 · SOURCE-GROUNDED TUTORIAL
Grok Build从源码学会它怎么工作 Actor 化运行时、内核沙箱和插件能力包组合先进,但沙箱失败时会降级继续。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。
Rust · Actor + Secure Worktree Agent Apache-2.0 02d9359435d0 17 个结论 · 42 处引用
这门课怎么读
先建立直觉,再沿一条任务链钻进代码 参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Grok Build 的 17 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。
你会得到 一张可复述的架构地图 一次完整任务的链路追踪 能迁移到自研 Harness 的设计判断
你不会得到 把 README 功能当成已验证事实 把 prompt 约束说成 OS 沙箱 把一次双模型调用夸成多 Agent 平台
M00 · MAP
先看全景:这个 Agent 的控制面在哪里 下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。
核心机制 长期 SessionActor;prepare → parallel dispatch → post-flight
M00.5 · TRACE
跟踪一个任务:从输入到交付 把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。
01 发送任务事件 任务进入
↓
02 冻结 PromptContext 把结果交给下一层
↓
03 流式采样 把结果交给下一层
↓
04 解析并修复工具参数 把结果交给下一层
↓
05 PreToolUse 与权限判定 把结果交给下一层
↓
06 沙箱内/降级后执行 把结果交给下一层
↓
07 post-flight 归并结果 把结果交给下一层
↓
08 两阶段压缩任务状态 把结果交给下一层
↓
09 输出结构化事件 交付/续跑
读图提醒 箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。
M01 · ORIENTATION
先把 Agent 看成一台会交付的机器 如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。
先用一个生活比喻 把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 1 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M02 · LOOP
主循环:模型为什么会继续动 一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?
先用一个生活比喻 像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。
这套实现先回答了什么? 它不像一个简单 while 循环,更像一间控制室:用户输入、工具结果、文件变化、后台任务、模型切换都从不同通道进来,由同一个会话 Actor 排队处理。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 SessionActor 是事件驱动的长期存活 Actor crates/codegen/xai-grok-shell/src/session/acp_session_impl/run_loop.rs:120它不像一个简单 while 循环,更像一间控制室:用户输入、工具结果、文件变化、后台任务、模型切换都从不同通道进来,由同一个会话 Actor 排队处理。
01 L1 · fact · grok-loop-001
SessionActor 是事件驱动的长期存活 Actor
先看源码事实 run_session 同时监听命令、chat-state 事件、内部事件、模型切换、任务完成和多个定时器;启动时还建立文件监听、MCP liveness dispatcher 与 replay buffer。
翻译成白话 它不像一个简单 while 循环,更像一间控制室:用户输入、工具结果、文件变化、后台任务、模型切换都从不同通道进来,由同一个会话 Actor 排队处理。
为什么这对自研重要 这套 Harness 面向长会话、后台工作和 IDE/ACP 集成,控制面复杂度显著高于纯 CLI Agent。
固定提交源码摘录 复制
120 pub(super) async fn run_session(
121 session: Arc<SessionActor>,
122 mut cmd_rx: mpsc::UnboundedReceiver<SessionCommand>,
123 mut chat_state_event_rx: mpsc::UnboundedReceiver<xai_chat_state::ChatStateEvent>,
124 mut event_rx: mpsc::UnboundedReceiver<SessionEvent>,
125 fs_notify_config: Option<ClientFsConfig>,
126 codebase_indexes: std::sync::Arc<parking_lot::Mutex<CodebaseIndexManager>>,
127 index_root: std::path::PathBuf,
128 fs_watch_caps: fs_watch::FsWatchCapabilities,
129 ) {
130 let (completion_tx, mut completion_rx) =
131 mpsc::unbounded_channel::<(String, PromptTurnResult)>();
132 tracing::debug!("fs_notify_config: {:?}", fs_notify_config);
133 let mut replay_buffer = ReplayBuffer::new(session.buffering_settings.clone());
134 let event_tx_for_flush_timer = session.event_tx.clone();
135 let buffering_flush_interval = replay_buffer.max_wait_duration_ms();
136 if let Some(buffering_flush_interval) = buffering_flush_interval {
137 tokio::task::spawn_local(async move {
138 let mut interval = tokio::time::interval(Duration::from_millis(std::cmp::max(
139 20,
140 buffering_flush_interval * 2,
141 )));
142 loop {
143 interval.tick().await;
144 let _ =
… 28 lines omitted; exact range 120–183 …
173 index_root.clone(),
174 );
175 tracing::debug!(?fs_watch_caps, "fs-notify: spawning");
176 Some(fs_watch::spawn(fs_watch::FsWatchPlan::build(
177 fs_watch_caps,
178 deps,
179 )))
180 } else {
181 tracing::debug!("fs-notify: skipped (no consumers)");
182 None
183 };
为什么相信这条结论?查看 3 处证据
小练习 2 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M03 · MODEL
模型调用:流式输出如何变成可执行步骤 模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?
先用一个生活比喻 模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 3 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M05 · CONTEXT
上下文:有限窗口怎样装下长任务 当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?
先用一个生活比喻 上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。
这套实现先回答了什么? 不是等窗口爆了才临时总结:它会提前准备摘要草稿,到红线时再完成第二遍;若摘要输入也太大,就逐级减料。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 压缩是一条带预热、两阶段和恢复梯子的子系统 crates/codegen/xai-grok-shell/src/session/compaction.rs:3不是等窗口爆了才临时总结:它会提前准备摘要草稿,到红线时再完成第二遍;若摘要输入也太大,就逐级减料。 压缩后重建的是“任务状态”,不是纯聊天摘要 crates/codegen/xai-grok-shell/src/session/compaction.rs:1282总结完以后还会把“哪些子任务在跑、待办是什么、插件有哪些、当前计划阶段”重新装回去,避免只剩一段模糊回忆。
05 L1 · fact · grok-context-001
压缩是一条带预热、两阶段和恢复梯子的子系统
先看源码事实 会话在阈值前若干百分点预先生成 pass-1;真正压缩支持 two-pass/full-replace,输入溢出时逐级缩小 verbatim/normal conversation,记录多类拒绝原因,并在完成后自动继续。
翻译成白话 不是等窗口爆了才临时总结:它会提前准备摘要草稿,到红线时再完成第二遍;若摘要输入也太大,就逐级减料。
为什么这对自研重要 长任务连续性被当作独立可靠性系统,而非一个 prompt helper。
固定提交源码摘录 复制
3 //! This module contains all compaction-related methods: manual `/compact`,
4 //! auto-compact threshold checks, inline auto-compact with auto-continue,
5 //! error-recovery compaction, preflight overflow detection, and checkpoint
6 //! persistence. These methods form a second `impl SessionActor` block that
7 //! lives alongside the primary one in `acp_session.rs`.
8 use super::SessionActor;
9 use super::is_project_instructions;
10 use crate::remote::DEFAULT_CONTEXT_WINDOW;
11 use crate::session::compaction_config::{
12 AsyncCompactionCache, SUPPRESS_AUTH, SUPPRESS_NONE, SUPPRESS_STICKY, SUPPRESS_TURN,
13 SUPPRESS_UNTIL_SUCCESS,
14 };
15 use crate::session::helpers::CompactionStateContext;
16 use crate::session::helpers::compaction_context::CompactionInputs;
17 use crate::session::helpers::compaction_context::to_system_reminder;
18 use crate::session::helpers::session_compact::{
19 CompactOutput, CompactionOutcome, build_compaction_chat_history,
20 build_two_pass_compaction_prompt, generate_session_compact, is_context_length_error,
21 };
22 use crate::session::persistence::PersistenceMsg;
23 use crate::session::two_pass::{
24 TWO_PASS_DEFAULT_SPLIT_FRACTION, build_two_pass_pass1_history, build_two_pass_pass2_history,
25 note_for_two_pass_pass2, split_conversation_for_two_pass,
26 };
27 use agent_client_protocol as acp;
28 use std::sync::Arc;
29 use xai_chat_state::compaction_utils::{
30 CompactedHistoryInput, CompactionAttempt, build_compacted_history, is_degenerate_summary,
31 prepare_conversation_for_verbatim_summarization, sanitize_compacted_history,
32 validate_compacted_history,
33 };
34 use xai_grok_sampling_types::{ApiBackend, ConversationItem};
35 /// Default percentage points below the auto-compact threshold at which prefire
为什么相信这条结论?查看 3 处证据
06 L1 · fact · grok-context-002
压缩后重建的是“任务状态”,不是纯聊天摘要
先看源码事实 压缩会把 AGENTS.md、skills、编辑路径、运行中子 Agent、MCP server、Todo、Plan mode 和 memory 状态重新注入;清理孤儿 ToolResult,验证失败则退到无 recent_messages 的最小历史,并持久化 segment/checkpoint。
翻译成白话 总结完以后还会把“哪些子任务在跑、待办是什么、插件有哪些、当前计划阶段”重新装回去,避免只剩一段模糊回忆。
为什么这对自研重要 可靠 compaction 必须有结构化状态源,不能只依赖模型自由文本。
固定提交源码摘录 复制
1282 let generate_session_compact = compact_output.content.clone();
1283 let user_message_prefix = self.build_user_message_prefix().await;
1284 let conversation = self.chat_state_handle.get_conversation().await;
1285 let (discovered_agents_md, all_skills_for_compaction, _agent_edited_paths, state_context) =
1286 if use_short_prompt {
1287 let empty_edited: std::collections::BTreeSet<String> = Default::default();
1288 let ctx =
1289 CompactionStateContext::build(&conversation, CompactionInputs::default()).await;
1290 (Vec::<std::path::PathBuf>::new(), vec![], empty_edited, ctx)
1291 } else {
1292 let agents_md: Vec<std::path::PathBuf> = self
1293 .agent
1294 .borrow()
1295 .tool_bridge()
1296 .agents_md_reminded_paths()
1297 .await
1298 .into_iter()
1299 .collect();
1300 let bridge_for_skills = self.agent.borrow().tool_bridge().clone();
1301 let skills = bridge_for_skills.slash_skills().await;
1302 let edited_paths = self.chat_state_handle.get_agent_edited_paths().await;
1303 let ctx = {
1304 let bridge_tasks = self
1305 .agent
1306 .borrow()
… 143 lines omitted; exact range 1282–1460 …
1450 session_id = %self.session_info.id.0,
1451 poll_resolved = poll.is_some(),
1452 cancel_resolved = cancel.is_some(),
1453 "could not resolve subagent tool names, \
1454 omitting subagent reminder from compacted conversation"
1455 );
1456 None
1457 }
1458 }
1459 };
1460 use crate::session::helpers::compaction_context::McpToolNames;
为什么相信这条结论?查看 2 处证据
小练习 5 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M06 · SECURITY
权限与沙箱:能做什么,在哪里做 审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?
先用一个生活比喻 审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。
这套实现先回答了什么? “全部自动批准”也不等于“计划阶段可以乱改代码”。计划模式另有一把更早、更硬的锁。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 Plan Mode 的只读约束独立于 Always Approve crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:157“全部自动批准”也不等于“计划阶段可以乱改代码”。计划模式另有一把更早、更硬的锁。 权限判断理解访问类型和会话上下文 crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:1035它不是只看工具名,而是知道“这是读哪个路径、改哪个文件、跑什么命令、访问哪个网站”,自动模式还会参考最近几轮对话。 PreToolUse Hook 可在权限前阻断 crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:977企业策略脚本可以比用户批准更早说“不”,避免用户误点同意覆盖组织规则。 提供真正的内核级文件系统沙箱 crates/codegen/xai-grok-sandbox/src/lib.rs:8这不只是“执行前问一下”,操作系统内核会真的挡住不允许的文件访问。
07 L1 · fact · grok-plan-001
Plan Mode 的只读约束独立于 Always Approve
先看源码事实 plan_mode_edit_gate 在权限管理器之前执行,即使 YOLO/always-approve 也不能绕过;普通编辑只允许计划文件,apply_patch 因无法预知目标而保守拒绝,未知退出结果默认取消。
翻译成白话 “全部自动批准”也不等于“计划阶段可以乱改代码”。计划模式另有一把更早、更硬的锁。
为什么这对自研重要 工作流阶段约束不能依赖通用权限模式,否则高权限模式会破坏阶段不变量。
固定提交源码摘录 复制
157 /// Verdict for a tool call evaluated against the plan-mode edit gate.
158 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
159 pub(super) enum PlanEditGate {
160 /// Execute normally (plan mode inactive, not an edit, or allowed target).
161 Allow,
162 /// Grok-toolset edit outside the plan file (plan-file-only rule).
163 RejectNonPlanFile,
164 }
165 /// Gate edit-class tool calls while plan mode is active.
166 ///
167 /// Plan mode is read-only **in every permission mode, including
168 /// always-approve**: the permission manager's YOLO fast path deliberately
169 /// knows nothing about plan mode, so this gate — not the permission system —
170 /// is what enforces it. Two rules, matching the two toolsets' contracts:
171 ///
172 /// - **Compat-toolset `Write`/`StrReplace`**: any markdown
173 /// file is editable in plan mode (plan docs are written with these
174 /// same tools); everything else is rejected. Pre-existing behavior.
175 /// - **Compat-toolset `Delete`** is **not** on the markdown carve-out: it maps to
176 /// `AccessKind::Edit` and is plan-file-only (same as grok edits). Deleting
177 /// an arbitrary `.md` in plan mode must not pass.
178 /// - **Every other edit tool** (`AccessKind::Edit`) is restricted to the plan
179 /// file itself, via the same predicate that auto-approves plan-file edits
180 /// ([`PlanModeTracker::should_auto_approve_edit`]) so the gate and the
181 /// permission bypass can never disagree.
… 13 lines omitted; exact range 157–205 …
195 if !tracker.is_active() {
196 return PlanEditGate::Allow;
197 }
198 let _ = tool_input;
199 match access_kind {
200 AccessKind::Edit(path) if !tracker.should_auto_approve_edit(Path::new(path)) => {
201 PlanEditGate::RejectNonPlanFile
202 }
203 _ => PlanEditGate::Allow,
204 }
205 }
为什么相信这条结论?查看 2 处证据
08 L1 · fact · grok-permission-001
权限判断理解访问类型和会话上下文
先看源码事实 工具被分类为 Read、Edit、Bash、Grep、MCP、Web;Ask/Auto/Always Approve 三种模式分别走交互、带最近对话的分类器或快速放行,决定来源与等待时间进入 trace 和 telemetry。
翻译成白话 它不是只看工具名,而是知道“这是读哪个路径、改哪个文件、跑什么命令、访问哪个网站”,自动模式还会参考最近几轮对话。
为什么这对自研重要 权限系统既是策略引擎,也是可观测的决策服务。
固定提交源码摘录 复制
1035 let plan_file_auto_approve = if let AccessKind::Edit(ref path) = access_kind {
1036 self.plan_mode
1037 .lock()
1038 .should_auto_approve_edit(std::path::Path::new(path))
1039 } else {
1040 false
1041 };
1042 if plan_file_auto_approve {
1043 tracing::info_span!(
1044 "tool.decision",
1045 tool_name = %call.function.name,
1046 tool_use_id = %call.id,
1047 decision = "allow",
1048 source = "config",
1049 wait_ms = 0_i64,
1050 )
1051 .in_scope(|| {});
1052 }
1053 if !plan_file_auto_approve {
1054 let (perm_title, perm_kind, perm_raw_input) = tool_call_display
1055 .as_ref()
1056 .map(|(t, k, r)| (Some(t.clone()), Some(*k), Some(r.clone())))
1057 .unwrap_or((None, None, None));
1058 let tool_call_update = acp::ToolCallUpdate::new(
1059 tool_call_id.clone(),
… 32 lines omitted; exact range 1035–1102 …
1092 Some(self.session_id_string())
1093 } else {
1094 None
1095 };
1096 let perm_mode = if self.permissions.is_yolo_mode() {
1097 xai_grok_telemetry::enums::PermissionMode::AlwaysApprove
1098 } else if self.permissions.is_auto_mode() {
1099 xai_grok_telemetry::enums::PermissionMode::Auto
1100 } else {
1101 xai_grok_telemetry::enums::PermissionMode::Ask
1102 };
为什么相信这条结论?查看 2 处证据
09 L1 · fact · grok-hooks-001
PreToolUse Hook 可在权限前阻断
先看源码事实 工具解析和 Plan gate 后,运行服务端/客户端 PreToolUse hooks;任一 hook 的 Deny 会产生 HookDenied,不再进入权限请求和 dispatch。
翻译成白话 企业策略脚本可以比用户批准更早说“不”,避免用户误点同意覆盖组织规则。
为什么这对自研重要 Hook 是治理层而不仅是通知回调。
固定提交源码摘录 复制
977 let dispatch_target_name = tool_input.dispatch_target_name();
978 let resolved_tool_name = dispatch_target_name
979 .clone()
980 .unwrap_or_else(|| call.function.name.clone());
981 if self.hook_event_active(xai_grok_hooks::event::HookEventName::PreToolUse) {
982 let (hook_tool_input, hook_tool_input_truncated) =
983 xai_grok_hooks::event::truncate_payload(raw_input.clone());
984 let envelope = self.make_hook_envelope(
985 xai_grok_hooks::event::HookEventName::PreToolUse,
986 None,
987 xai_grok_hooks::event::HookPayload::PreToolUse {
988 tool_name: resolved_tool_name.clone(),
989 tool_use_id: call.id.clone(),
990 tool_input: hook_tool_input,
991 tool_input_truncated: hook_tool_input_truncated,
992 subagent_type: self.subagent_type_label(),
993 },
994 );
995 let hook_registry_snapshot = self.hook_registry.borrow().clone();
996 if let Some(registry) = hook_registry_snapshot {
997 let ctx = self.hook_run_ctx();
998 let pre_result =
999 xai_grok_hooks::dispatcher::dispatch_pre_tool_use(®istry, &envelope, &ctx)
1000 .await;
1001 self.send_hook_execution(
… 22 lines omitted; exact range 977–1034 …
1024 )
1025 .await?));
1026 }
1027 }
1028 if let Some(denied) = self
1029 .run_pre_tool_use_client_hook(&call, &tool_call_id, &envelope)
1030 .await?
1031 {
1032 return Ok(Err(denied));
1033 }
1034 }
为什么相信这条结论?查看 1 处证据
10 L1 · fact · grok-sandbox-001
提供真正的内核级文件系统沙箱
先看源码事实 默认 enforce feature 在 Unix 使用 nono,映射到 Landlock/Seatbelt;Profile capability set 控制全盘读、显式只读/读写、deny,应用后不可逆并覆盖进程内文件 API 与子进程。
翻译成白话 这不只是“执行前问一下”,操作系统内核会真的挡住不允许的文件访问。
为什么这对自研重要 在这 14 个项目里,Grok Build 的安全边界属于更强的系统级设计。
固定提交源码摘录 复制
8 //! OS-level sandboxing for Grok Build via [nono](https://crates.io/crates/nono).
9 //!
10 //! Applied once at process startup. Covers in-process `tokio::fs` calls
11 //! and child processes. Network is left open at the process level (agent
12 //! needs LLM API); child network is blocked per-subprocess via seccomp.
13 //!
14 //! The `enforce` feature (on by default) pulls in `nono` for
15 //! kernel-enforced sandboxing (Landlock/Seatbelt). When disabled, the
16 //! crate still provides lightweight helpers (`log_violation`,
17 //! `should_restrict_child_network`, `child_net`) that compile on all
18 //! targets including musl.
为什么相信这条结论?查看 3 处证据
11 L1 · limitation · grok-sandbox-002
沙箱不支持或应用失败时会降级继续
先看源码事实 若平台不支持、apply 失败或构建关闭 enforce feature,SandboxManager 记录警告和 apply_failed 事件后返回 Ok,进程继续运行;is_active 可用于区分实际生效与仅请求。
翻译成白话 配置了沙箱不等于一定锁住;系统不支持时默认是“报警后继续”,不是整个 Agent 拒绝启动。
为什么这对自研重要 企业部署必须把“sandbox requested”和“sandbox active”分开做启动门禁;需要 fail-closed 时应由外层策略强制。
固定提交源码摘录 复制
107 /// The non-`off` sandbox profile this process was **requested** with, if any.
108 ///
109 /// This is the configured request, not a report that enforcement succeeded —
110 /// `is_active()` can be false while the process is still confined (e.g. some
111 /// Linux bwrap paths), and a requested-but-unapplied profile already warns the
112 /// user. Keying on the request is the fail-closed choice.
113 pub fn requested_confinement_profile() -> Option<&'static str> {
114 configured_profile_name().filter(|name| profile_confines(name))
115 }
116 fn profile_confines(name: &str) -> bool {
117 name.parse::<ProfileName>()
118 .is_ok_and(|profile| profile != ProfileName::Off)
119 }
120 /// Whether the sandbox was successfully applied to this process.
121 pub fn is_active() -> bool {
122 SANDBOX.get().is_some_and(|s| s.applied)
123 }
124 /// The active sandbox profile name, or `None` if sandbox is not applied.
125 pub fn profile_name() -> Option<&'static str> {
126 SANDBOX
127 .get()
128 .filter(|s| s.applied)
129 .map(|s| s.profile.as_str())
为什么相信这条结论?查看 2 处证据
12 L1 · fact · grok-sandbox-003
子进程网络隔离与主进程网络分离
先看源码事实 主进程保留网络供 LLM API;ReadOnly/Strict profile 在已生效的 Linux 沙箱中,对已知子进程启动路径安装 seccomp 网络过滤,并额外锁定 namespace 逃逸 syscall。
翻译成白话 Agent 自己可以连模型服务,但它启动的 bash 不一定能上网,减少 curl 下载执行或数据外传风险。
为什么这对自研重要 网络隔离需要按进程角色拆分;完全断主进程网络会破坏云模型调用。
固定提交源码摘录 复制
8 //! OS-level sandboxing for Grok Build via [nono](https://crates.io/crates/nono).
9 //!
10 //! Applied once at process startup. Covers in-process `tokio::fs` calls
11 //! and child processes. Network is left open at the process level (agent
12 //! needs LLM API); child network is blocked per-subprocess via seccomp.
13 //!
14 //! The `enforce` feature (on by default) pulls in `nono` for
15 //! kernel-enforced sandboxing (Landlock/Seatbelt). When disabled, the
16 //! crate still provides lightweight helpers (`log_violation`,
17 //! `should_restrict_child_network`, `child_net`) that compile on all
18 //! targets including musl.
为什么相信这条结论?查看 3 处证据
13 L1 · fact · grok-sandbox-004
项目不能覆写同名全局安全 Profile
先看源码事实 全局 ~/.grok/sandbox.toml 先加载;项目 .grok/sandbox.toml 只能新增自定义 profile,若名称已存在则忽略项目定义。
翻译成白话 恶意仓库不能悄悄做一个同名“strict”配置,把你信任的全局规则掏空。
为什么这对自研重要 配置合并本身也是信任边界,不能简单 last-write-wins。
固定提交源码摘录 复制
113 /// Load sandbox config from `~/.grok/sandbox.toml` and `.grok/sandbox.toml`.
114 ///
115 /// Project config may **add** new profile names only. It cannot redefine a
116 /// name already present in the global config — last-write-wins would let a
117 /// malicious workspace hollow out a user/enterprise custom profile (e.g.
118 /// empty `deny` / broad `read_write`) while keeping the trusted name.
119 pub fn load_sandbox_config(workspace: &Path) -> SandboxConfig {
120 let mut config = SandboxConfig::default();
121
122 // Global config: ~/.grok/sandbox.toml
123 let global_path = grok_home().join("sandbox.toml");
124 if let Some(global) = load_config_file(&global_path) {
125 config = global;
126 }
127
128 // Project config: <workspace>/.grok/sandbox.toml (additive only)
129 let project_path = workspace.join(".grok").join("sandbox.toml");
130 if let Some(project) = load_config_file(&project_path) {
131 merge_project_profiles(&mut config, project);
132 }
133
134 config
135 }
136
137 pub fn sandbox_profile_conflicts(workspace: &Path) -> Vec<String> {
… 19 lines omitted; exact range 113–167 …
157 names.sort_unstable();
158 names
159 }
160
161 /// Merge project profiles into `config`. Names already defined globally are
162 /// ignored so a workspace cannot replace a global custom profile's policy.
163 fn merge_project_profiles(config: &mut SandboxConfig, project: SandboxConfig) {
164 for (name, profile) in project.profiles {
165 config.profiles.entry(name).or_insert(profile);
166 }
167 }
为什么相信这条结论?查看 1 处证据
小练习 6 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M07 · ECOSYSTEM
指令、MCP、Skills 与插件:能力如何接进来 一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?
先用一个生活比喻 这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。
这套实现先回答了什么? 它能读懂多个编辑器生态留下的项目规则,并按“越靠近当前目录越具体”叠加。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 项目指令跨 Grok、Claude、Cursor 生态发现 crates/codegen/xai-grok-agent/src/prompt/agents_md.rs:1它能读懂多个编辑器生态留下的项目规则,并按“越靠近当前目录越具体”叠加。 PromptContext 是可序列化、可检查的一等契约 crates/codegen/xai-grok-agent/src/prompt/context.rs:80系统提示词的输入不是散落变量,而是一张可以导出检查的配置表。
14 L1 · fact · grok-instructions-001
项目指令跨 Grok、Claude、Cursor 生态发现
先看源码事实 从 home、仓库根到 cwd 分层发现 AGENTS/Claude 指令与 .grok/.claude/.cursor rules,按兼容开关和 gitignore 过滤、canonical path 去重;更深目录排在后面。
翻译成白话 它能读懂多个编辑器生态留下的项目规则,并按“越靠近当前目录越具体”叠加。
为什么这对自研重要 兼容性高,但必须在报告中展示来源和优先级,避免用户不知道哪条规则生效。
固定提交源码摘录 复制
1 //! AGENTS.md / Claude.md / rules directory discovery and loading.
2 //!
3 //! Searches from cwd to repo root, plus `~/.grok/`. Also discovers
4 //! `*.md` files in rules directories: vendor-prefixed `.grok/rules/`,
5 //! `.claude/rules/`, and `.cursor/rules/` in project directories, and a
6 //! plain `rules/` directly under the vendor-qualified home-scope roots
7 //! (`~/.grok/rules/`, `~/.claude/rules/`, `~/.cursor/rules/`).
为什么相信这条结论?查看 2 处证据
15 L2 · fact · grok-prompt-001
PromptContext 是可序列化、可检查的一等契约
先看源码事实 PromptContext 保存版本、模式、受众、模板、指令文件、memory、角色、OS/shell/cwd/date 与交互模式;Primary/Subagent 选择不同基础模板,但子 Agent 仍拿到完整 AGENTS.md。
翻译成白话 系统提示词的输入不是散落变量,而是一张可以导出检查的配置表。
为什么这对自研重要 Prompt provenance 和可重放性明显优于只在运行时拼字符串。
固定提交源码摘录 复制
80 /// Agent-specific inputs for system prompt rendering.
81 ///
82 /// Serializable (JSON/YAML) so users can dump it and inspect fields.
83 /// Rendering goes through `ToolBridge::render_prompt()`.
84 #[derive(Debug, Clone, Serialize, Deserialize)]
85 pub struct PromptContext {
86 /// Schema version for forward-compatible persistence.
87 pub version: u32,
88 /// Which prompt mode produced this context.
89 pub prompt_mode: PromptMode,
90 /// Whether this is a primary (parent) or subagent (child) session.
91 /// Controls base template choice and catalog section rendering.
92 #[serde(default)]
93 pub audience: PromptAudience,
94 /// Custom body: appended after base template (Extend) or the entire
95 /// prompt (Full). `None` = base template only.
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub prompt_body: Option<String>,
98 /// Which base template to use for `Extend` mode.
99 /// `TemplateOverride::None` = standard base/subagent template.
100 /// `TemplateOverride::Codex` = apply-patch profile template (decrypted on demand).
101 /// `TemplateOverride::Custom` = caller-provided template string.
102 #[serde(default, skip_serializing_if = "is_template_override_none")]
103 pub system_prompt: TemplateOverride,
104 /// AGENTS.md files discovered during build, in precedence order
… 37 lines omitted; exact range 80–152 …
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub current_date: Option<String>,
144 /// Whether the agent is running in a non-interactive (headless / SDK /
145 /// stdio / generic-ACP).
146 #[serde(default)]
147 pub is_non_interactive: bool,
148 /// Identity in the primary grok-build system prompt (`You are <label>…`).
149 /// Not the UI picker name. Defaults to [`DEFAULT_SYSTEM_PROMPT_LABEL`].
150 #[serde(default = "default_system_prompt_label")]
151 pub system_prompt_label: String,
152 }
为什么相信这条结论?查看 3 处证据
小练习 7 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M08 · COLLABORATION
子 Agent:把一个大任务拆成可治理的协作 什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?
先用一个生活比喻 不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。
这套实现先回答了什么? 它是真正的多 Agent 调度:子任务可以后台跑、以后接着跑、在独立分支目录里改代码,但不能无限生孩子。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 子 Agent 支持后台执行、恢复、深度限制与 worktree 隔离 crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:811它是真正的多 Agent 调度:子任务可以后台跑、以后接着跑、在独立分支目录里改代码,但不能无限生孩子。
16 L1 · fact · grok-subagent-001
子 Agent 支持后台执行、恢复、深度限制与 worktree 隔离
先看源码事实 task/spawn_subagent 默认后台运行;启动前校验类型白名单和 toggle,能力取父子交集;可选创建独立 Git worktree,持久化子会话并从 snapshot 恢复;达到 max depth 后移除子 Agent 的 task 工具。
翻译成白话 它是真正的多 Agent 调度:子任务可以后台跑、以后接着跑、在独立分支目录里改代码,但不能无限生孩子。
为什么这对自研重要 并行协作能力强,同时引入 worktree 生命周期、结果合并、成本与递归治理的高复杂度。
固定提交源码摘录 复制
811 {
812 let _span = tracing::info_span!("tool.register").entered();
813 let early_raw_input =
814 serde_json::from_str::<serde_json::Value>(&call.function.arguments).ok();
815 let subagent_background = matches!(
816 call.function.name.as_str(),
817 "task" | "Task" | "spawn_subagent"
818 )
819 .then(|| {
820 early_raw_input
821 .as_ref()
822 .and_then(|v| v.get("run_in_background").or_else(|| v.get("background")))
823 .and_then(serde_json::Value::as_bool)
824 .unwrap_or(true)
825 });
826 let mut meta = self.stamp_tool_meta(None, &call.function.name, None);
827 if let Some(bg) = subagent_background {
828 meta.get_or_insert_with(serde_json::Map::new).insert(
829 "subagentBackground".to_string(),
830 serde_json::Value::Bool(bg),
831 );
832 }
833 self.send_update(
834 acp::SessionUpdate::ToolCall(
835 acp::ToolCall::new(tool_call_id.clone(), call.function.name.clone())
836 .kind(acp::ToolKind::Other)
837 .status(acp::ToolCallStatus::Pending)
838 .raw_input(early_raw_input)
839 .meta(meta),
为什么相信这条结论?查看 4 处证据
小练习 8 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M09 · STATE
会话、持久化与观测:让一次运行变成可追溯事实 如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?
先用一个生活比喻 内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。
这套实现先回答了什么? 不仅能看到“失败了”,还能回答失败在哪个关、谁批准的、等了多久、压缩试了几次、沙箱挡了什么。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 工具、权限、压缩和沙箱均产出结构化事件 crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:392不仅能看到“失败了”,还能回答失败在哪个关、谁批准的、等了多久、压缩试了几次、沙箱挡了什么。
17 L1 · fact · grok-observe-001
工具、权限、压缩和沙箱均产出结构化事件
先看源码事实 工具开始/完成进入 ACP 与统一 SessionEvent,权限记录模式、来源、等待时间,压缩 span 记录 token/尝试/拒绝/TTFT,沙箱事件立即追加 ~/.grok/sandbox-events.jsonl。
翻译成白话 不仅能看到“失败了”,还能回答失败在哪个关、谁批准的、等了多久、压缩试了几次、沙箱挡了什么。
为什么这对自研重要 具备平台级诊断基础,但事件面很大,需要统一 trace/session/tool/subagent ID 规范。
固定提交源码摘录 复制
392 self.emit_event(crate::session::events::Event::ToolStarted {
393 tool_name: call.function.name.clone(),
394 });
395 self.observability_bridge
396 .emit(
397 xai_tool_protocol::session_event::SessionEvent::ToolCallStarted {
398 tool_call_id: call.id.clone(),
399 tool_name: call.function.name.clone(),
400 turn_number: self.current_turn_number.get(),
401 },
402 )
403 .await;
404 let call_name = call.function.name.clone();
405 match self.prepare_tool_call(call, deferred_followups).await? {
406 Ok(prepared) => approved.push(prepared),
407 Err(tool_loop) => {
408 self.events.tool_finished();
409 if let Some((server, tool)) =
410 crate::session::mcp_servers::parse_mcp_tool_name(&call_name)
411 {
412 let error_reason = match &tool_loop {
413 ToolLoop::PermissionReject { reason, .. } => reason.clone(),
414 ToolLoop::Cancelled => "cancelled".to_string(),
415 ToolLoop::FollowupMessage(_) => "followup".to_string(),
416 ToolLoop::HookDenied { hook_name, .. } => {
… 9 lines omitted; exact range 392–436 …
426 server,
427 crate::session::mcp_servers::MCP_TOOL_NAME_DELIMITER,
428 tool
429 ),
430 duration_ms: 0,
431 success: false,
432 is_timeout: false,
433 error: Some(error_reason),
434 reconnect_attempted: false,
435 auth_retry_attempted: false,
436 });
为什么相信这条结论?查看 4 处证据
小练习 9 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M10 · ENGINEERING
测试、恢复与工程取舍:把漂亮机制变成可靠产品 哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?
先用一个生活比喻 这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 10 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M11 · PRACTICE
把读懂变成会判断 下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。
Q1 SessionActor 是事件驱动的长期存活 Actor 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 这套 Harness 面向长会话、后台工作和 IDE/ACP 集成,控制面复杂度显著高于纯 CLI Agent。
证据:crates/codegen/xai-grok-shell/src/session/acp_session_impl/run_loop.rs:120 Q2 工具执行明确拆成 prepare、并发 dispatch、post-flight 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 并发策略不是简单 all-at-once,而是带资源锁的调度。
证据:crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:355 Q3 工具参数对模型瑕疵有恢复层 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 对模型输出做保守修复能提高成功率,但必须记录恢复行为,避免悄悄改变语义。
证据:crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:878 Q4 Plan Mode 的只读约束独立于 Always Approve 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 工作流阶段约束不能依赖通用权限模式,否则高权限模式会破坏阶段不变量。
证据:crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:157 Q5 权限判断理解访问类型和会话上下文 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 权限系统既是策略引擎,也是可观测的决策服务。
证据:crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rs:1035
APPENDIX · SOURCE INDEX
本课读过的实现文件 文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。
01 crates/codegen/xai-grok-shell/src/session/acp_session_impl/run_loop.rsL120–183, L188–246, L247–280 02 crates/codegen/xai-grok-shell/src/session/acp_session_impl/tool_calls.rsL355–449, L453–565, L878–950, L157–205, L206–215, L1035–1102, L1112–1227, L977–1034, L811–839, L392–436, L1198–1227 03 crates/codegen/xai-grok-sandbox/src/lib.rsL8–18, L173–219, L107–129, L190–243, L8–18, L83–95 04 crates/codegen/xai-grok-sandbox/src/profiles.rsL24–41, L113–167 05 crates/codegen/xai-grok-sandbox/src/child_net.rsL62–144 06 crates/codegen/xai-grok-shell/src/session/compaction.rsL3–35, L212–262, L1098–1197, L1282–1460, L1577–1655, L859–895 07 crates/codegen/xai-grok-agent/src/prompt/agents_md.rsL1–7, L177–307 08 crates/codegen/xai-grok-agent/src/prompt/context.rsL80–152, L199–232, L253–301 09 crates/codegen/xai-grok-agent/src/plugins/manifest.rsL103–170 10 crates/codegen/xai-grok-agent/src/plugins/registry.rsL12–78, L114–184, L274–279 11 crates/codegen/xai-grok-shell/src/agent/subagent/handle_request.rsL84–140, L149–343, L375–423 12 crates/codegen/xai-grok-sandbox/src/logging.rsL3–97