Grok Build▹
§ 00 · Source-level Observatory

Grok Build 终端原生 Coding Agent 运行时

xAI 开源的 SpaceXAI coding-agent harness and TUI。源码显示它是一个 Rust workspace:TUI、headless 单轮、ACP 编辑器协议、MCP、插件和工具注册表共享同一批运行时边界。

★ 4414
GitHub Stars
2049
Codegen files
Rust
Primary language
Jul 15
Last commit
CLI / TUI / ACP │ main.rs 启动器 ├─ pager event loop ├─ mvp_agent sessions ├─ tool registry └─ MCP / plugin / sandbox
§ 00 · OVERVIEW

为什么它不是普通 CLI

Grok Build 值得拆的地方不在“又一个终端 coding agent”,而在它把三种运行形态放进同一套 Rust 控制面:入口从 sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1456 初始化渲染和内存钩子,到 sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1530 完成参数解析,再在 sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1777 切到 headless 单轮执行。这个路径说明它不是 demo CLI,而是把 TUI、单轮 agent、编辑器协议都接在同一个启动器下。

主循环没有把所有状态塞进事件处理函数。sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:97 先构造重连计划,sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:126 再汇总恢复结果,sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:195 处理子进程暂停时的 TTY 恢复。这样做的价值是:TUI 可以断线重连、让 shell 子进程临时接管终端,同时仍然保持 actor 状态可恢复。

▸四个边界

入口边界
main.rs 负责渲染、权限、沙箱、命令分派和 headless 分支。
状态边界
pager/app/agent.rs 把队列、后台任务、kill 语义写成数据结构。
工具边界
workspace capability + tools registry 决定哪些工具能被调度。
协议边界
ACP 与 MCP 是运行时能力入口,而不是独立 demo。
§ 01 · ARCHITECTURE

启动器到事件循环

Agent 状态被拆成可序列化、可排队、可展示的数据结构。sources/grok-build/crates/codegen/xai-grok-pager/src/app/agent.rs:17 定义 AgentId 与队列类型,sources/grok-build/crates/codegen/xai-grok-pager/src/app/agent.rs:43 把 prompt、图像、skill 展示和 schedule 放进 QueuedPrompt,sources/grok-build/crates/codegen/xai-grok-pager/src/app/agent.rs:161 则约束后台任务 stdout 上限和 kill 语义。它把 UI 展示和任务控制的边界写成普通数据,而不是散在 widgets 里。

工具能力边界不是靠提示词声明,而是有一层 capability filter。sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:6 定义 ReadOnly、ReadWrite、Execute、All 的偏序,sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:33 把声明后的工具集过滤,sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:116 用 ToolKind 映射读写执行类别。这个设计是安全边界里最可复用的一块。

▸启动器的关键分派

sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1456-1488Rust
1456fn main() {
1457    xai_grok_pager_minimal::install();
1458    #[cfg(all(feature = "jemalloc", unix))]
1459    xai_grok_pager::memory_release::install_release_hook(purge_jemalloc_retained_pages);
1460    #[cfg(all(feature = "jemalloc", unix))]
1461    {
1462        xai_grok_pager::memory_trace::install_allocator_stats_provider(jemalloc_allocator_stats);
1463        xai_grok_pager::memory_trace::install_allocator_dump_provider(jemalloc_stats_dump);
1464    }
1465    #[cfg(all(feature = "jemalloc", unix))]
1466    install_heap_profile_hooks();
1467    if let Some(code) = xai_grok_pager::app::mermaid_worker::maybe_run_render_subprocess() {
1468        std::process::exit(code);
1469    }
1470    xai_grok_pager::memory_trace::start(
1471        xai_grok_shell::util::grok_home::grok_home().join("memtrace"),
1472    );
1473    raise_fd_limit();
1474    if let Err(e) = xai_grok_config::validate_requirements() {
1475        eprintln!("Couldn't start Grok: {e}");
1476        eprintln!();
1477        eprintln!(
1478            "Update Grok to a version the policy allows, or ask your administrator \
1479             to fix the managed requirements."
1480        );
1481        std::process::exit(2);
1482    }
1483    let _sentry_guard = xai_grok_telemetry::sentry::init(xai_grok_telemetry::sentry::Config {
1484        client: "grok-pager",
1485        client_version: PAGER_CLIENT_VERSION,
1486        release: env!("VERSION_WITH_COMMIT"),
1487        disabled: xai_grok_shell::agent::config::is_error_reporting_disabled_sync(),
1488    });

▸命令与 headless 分支

sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1777-1825Rust
1777    let headless_prompt = xai_grok_pager::headless::HeadlessPrompt::from_args(
1778        args.single.as_deref(),
1779        args.prompt_json.as_deref(),
1780        args.prompt_file.as_deref(),
1781    )?;
1782    if let Some(prompt) = headless_prompt {
1783        init_tracing_simple(HEADLESS_ENTRYPOINT);
1784        let _otel_guard = xai_grok_telemetry::otel_layer::otel_guard();
1785        enforce_minimum_version_or_exit(&update_config).await;
1786        let launch_yolo = xai_grok_shell::util::config::effective_yolo_for_launch(
1787            args.yolo,
1788            args.permission_mode_flag.as_deref(),
1789            None,
1790        );
1791        if let Some(warning) = launch_yolo.blocked_warning {
1792            eprintln!("grok: {warning}");
1793        }
1794        let json_schema = args
1795            .json_schema
1796            .as_deref()
1797            .map(xai_grok_pager::headless::parse_json_schema)
1798            .transpose()?;
1799        if json_schema.is_some() {
1800            if args.output_format == xai_grok_pager::headless::OutputFormat::Plain {
1801                args.output_format = xai_grok_pager::headless::OutputFormat::Json;
1802            }
1803            if args.self_verify {
1804                anyhow::bail!(
1805                    "--json-schema and --self-verify cannot be used together: \
1806                     verification output would corrupt the structured response"
1807                );
1808            }
1809        }
1810        return xai_grok_pager::headless::run_single_turn(
1811            prompt,
1812            args.verbatim,
1813            xai_grok_pager::headless::HeadlessOptions {
1814                session_id: args.session_id.clone(),
1815                resume: args.resume_session.or(args.load_session),
1816                cwd: args.cwd,
1817                yolo: launch_yolo.yolo,
1818                trust: args.trust,
1819                output_format: args.output_format,
1820                json_schema,
1821                model: args.model,
1822                rules: args.rules,
1823                system_prompt_override: args.system_prompt_override.clone(),
1824                continue_last_session: args.continue_last_session,
1825                fork_session: args.fork_session,

▸TUI 重连计划

sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:97-150Rust
97fn plan_reconnect_load(
98    agent: &super::agent_view::AgentView,
99    fallback_cwd: &std::path::Path,
100) -> Option<ReconnectLoadPlan> {
101    let session_id = agent.session.session_id.clone()?;
102    let cwd = if agent.session.cwd.as_os_str().is_empty() {
103        fallback_cwd.to_path_buf()
104    } else {
105        agent.session.cwd.clone()
106    };
107    let yolo = agent.session.is_yolo();
108    // Set BOTH yoloMode and autoMode explicitly. The leader's capability injection
109    // only fills ABSENT keys, so omitting autoMode here lets a stale launch-time
110    // `ClientCapabilities.auto_mode` re-enable Auto after the user left it (e.g.
111    // Shift+Tab to Ask). Auto is per-agent (symmetric with yolo) — derive it from
112    // this agent's own `auto_mode` so a background tab reconnects with ITS mode,
113    // not the active tab's global `current_ui` mirror.
114    let auto = super::dispatch::effective_auto(yolo, agent.session.is_auto());
115    let mut meta = serde_json::json!({ "yoloMode": yolo, "autoMode": auto });
116    if let Some(ref cursor) = agent.last_seen_event_id {
117        meta["cursor"] = serde_json::Value::String(cursor.clone());
118    }
119    Some(ReconnectLoadPlan {
120        session_id,
121        cwd,
122        meta,
123    })
124}
125
126/// Resolve the two post-reconnect restore outcomes from the per-agent
127/// `session/load` results.
128///
129/// - `all_restored` (AND across every reloaded tab, plus `init_ok`) drives the
130///   user-facing toast: it reports whether the WHOLE reconnect came back.
131/// - `active_restored` is per-agent: the ACTIVE tab's OWN reload succeeded. It
132///   gates that tab's post-reconnect queue drain. Gating the drain on
133///   `all_restored` would let one failed background tab strand prompts queued
134///   on a healthy active tab — the drain (`dispatch_drain_queue`) only ever
135///   touches the active agent, so a background failure has no bearing on it.
136///
137/// `loads` maps each reloaded agent to `(success, running_prompt_id)`; an agent
138/// in `pending_agent_ids` but absent from `loads` is treated as failed
139/// (mirrors the `unwrap_or((false, _))` at the finalize site).
140fn reconnect_restore_outcome(
141    init_ok: bool,
142    pending_agent_ids: &[super::agent::AgentId],
143    loads: &std::collections::HashMap<super::agent::AgentId, (bool, Option<String>)>,
144    active_agent_id: Option<super::agent::AgentId>,
145) -> (bool, bool) {
146    let load_ok = |id: &super::agent::AgentId| -> bool { loads.get(id).is_some_and(|(ok, _)| *ok) };
147    let all_restored = init_ok && pending_agent_ids.iter().all(load_ok);
148    let active_restored = init_ok
149        && active_agent_id.is_some_and(|aid| pending_agent_ids.contains(&aid) && load_ok(&aid));
150    (all_restored, active_restored)
深度阅读 · 运行时与 TUI 入口
§ 02 · STATE

Agent 状态模型

ACP 编辑器模式不是旁路实现。sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/acp_agent.rs:7 直接实现 acp::Agent,sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/acp_agent.rs:25 在 initialize 时清理与 bootstrap 权限,sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/acp_agent.rs:77 读取客户端能力、code navigation 和 mcpApps。编辑器入口与终端入口共享信任、工具、MCP 这些底层抽象。

▸队列项与 Prompt

sources/grok-build/crates/codegen/xai-grok-pager/src/app/agent.rs:17-78Rust
17/// Unique local identifier for an agent.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct AgentId(pub usize);
20/// Whether a queue entry is a regular prompt or a slash command.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum QueueEntryKind {
23    /// Regular user prompt — sent via `PromptRequest`.
24    Prompt,
25    /// Slash command (e.g., `/compact`) — dispatched as `ExtRequest` or local action.
26    Command,
27    /// Direct bash command — bypasses agent loop, executed by shell directly.
28    BashCommand,
29    /// Scheduled (cron) prompt -- injected by the scheduler via ACP notification.
30    Cron,
31}
32impl QueueEntryKind {
33    /// Short, stable label for telemetry / profiling logs.
34    pub fn as_label(&self) -> &'static str {
35        match self {
36            Self::Prompt => "prompt",
37            Self::Command => "command",
38            Self::BashCommand => "bash_command",
39            Self::Cron => "cron",
40        }
41    }
42}
43/// An entry waiting in the queue to be sent to the agent.
44///
45/// Each entry gets a monotonically increasing `id` for stable tracking
46/// (e.g., when editing a queued prompt whose positional index shifts as
47/// earlier prompts drain). The user-facing display uses the 1-based
48/// positional index (`#1`, `#2`, …), never the internal `id`.
49#[derive(Debug, Clone)]
50pub struct QueuedPrompt {
51    /// Monotonic ID, unique within this agent's session. Never reused.
52    pub id: u64,
53    /// The prompt text (or command text, e.g. "/compact").
54    pub text: String,
55    /// Whether this is a prompt or a slash command.
56    pub kind: QueueEntryKind,
57    /// Optional separate payload for the wire. When `Some`, this is sent
58    /// instead of `text`. Used for skill injection where the display
59    /// shows `/commit args` but the wire carries the skill XML content.
60    pub wire_blocks: Option<Vec<acp::ContentBlock>>,
61    /// Images attached to this prompt. Drained from `PromptWidget` at
62    /// submission time. Preserved across queue text edits.
63    pub images: Vec<crate::prompt_images::PastedImage>,
64    /// Whether this prompt should display as a skill invocation (teal accent).
65    /// Only meaningful when `wire_blocks` is `Some`.
66    pub display_as_skill: bool,
67    /// Recognized slash-token byte ranges into `text`, captured from the
68    /// composer at submit time; empty = no token styling.
69    pub skill_token_ranges: Vec<std::ops::Range<usize>>,
70    /// Scheduler task ID for cron prompts. Used for per-task dedup.
71    pub task_id: Option<String>,
72    /// Human-readable schedule (e.g. "every 5 minutes") for cron prompts.
73    /// Threaded into the system-reminder framing sent to the model.
74    pub human_schedule: Option<String>,
75    /// All chip elements captured from the textarea at send time.
76    /// Threaded into `InFlightPrompt` so rewind restores collapsed chips.
77    pub chip_elements: Vec<ChipElement>,
78}

▸后台任务输出上限

sources/grok-build/crates/codegen/xai-grok-pager/src/app/agent.rs:161-222Rust
161/// Maximum in-memory stdout per background task (10 MB).
162pub const BG_TASK_MAX_STDOUT: usize = 10 * 1024 * 1024;
163/// How long to wait for a kill response before auto-clearing `pending_kill`
164/// so the user can retry. Applied to both bg tasks and subagents.
165pub const PENDING_KILL_TIMEOUT_SECS: u64 = 10;
166/// Status of a background task.
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum BgTaskStatus {
169    /// Currently running.
170    Running,
171    /// Completed successfully (exit 0).
172    Done,
173    /// Failed (non-zero exit, signal, timeout, OOM, etc.).
174    Failed,
175}
176/// Central state for a single background task.
177///
178/// Stored in `AgentSession::bg_tasks` keyed by `task_id`.
179/// Both the scrollback `BgTaskBlock` and the bg task pane read from this.
180#[derive(Debug, Clone)]
181pub struct BgTaskState {
182    pub task_id: String,
183    pub tool_call_id: String,
184    pub command: String,
185    pub description: Option<String>,
186    pub cwd: String,
187    pub output_file: String,
188    pub status: BgTaskStatus,
189    pub start_time: SystemTime,
190    pub end_time: Option<SystemTime>,
191    pub exit_code: Option<i32>,
192    pub signal: Option<String>,
193    /// Accumulated stdout (full cumulative buffer from shell, max BG_TASK_MAX_STDOUT).
194    ///
195    /// Mutate via [`BgTaskState::set_stdout`] / [`BgTaskState::append_stdout`]
196    /// so `stdout_line_count` and `truncated` stay in sync.
197    pub stdout: String,
198    /// Cached `stdout.lines().count()`. Kept in sync by [`Self::set_stdout`]
199    /// and [`Self::append_stdout`] so the tasks-pane overlay doesn't have to
200    /// memchr-scan up to `BG_TASK_MAX_STDOUT` bytes per visible task per
201    /// render frame.
202    pub stdout_line_count: usize,
203    /// Whether the rolling buffer has dropped data. Either the shell-side
204    /// `BashOutput.truncated` flag was set when the chunk arrived, or the
205    /// TUI itself trimmed the buffer to stay under `BG_TASK_MAX_STDOUT` (in
206    /// `set_stdout` / `append_stdout`). Once `true`, stays `true` — the
207    /// real line count is at least `stdout_line_count`, hence the `(N+)`
208    /// badge.
209    pub truncated: bool,
210    /// Kill request sent, awaiting task_completed.
211    pub pending_kill: bool,
212    /// When the kill request was sent. Used to auto-clear `pending_kill`
213    /// after a timeout so the user can retry if the response is lost.
214    pub kill_requested_at: Option<Instant>,
215    /// Scrollback entry ID for the "Task started" block (for finish_running).
216    pub scrollback_entry_id: Option<crate::scrollback::entry::EntryId>,
217    /// True when this background task is a monitor (the `monitor` tool). The
218    /// tasks pane renders monitors with a blue "Monitor" tag + neutral text
219    /// (mirroring scheduled `/loop` rows) instead of bash-highlighting the
220    /// command. Set from the `monitor_description` field of the
221    /// `TaskBackgrounded` notification.
222    pub is_monitor: bool,

▸TTY 暂停交接

sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:195-260Rust
195/// Suspend the inline TUI, run a blocking child that takes over the tty
196/// (`$EDITOR`, `$PAGER`, …), then restore. Shared by the editor and transcript
197/// suspend paths so the subtle reader-park + raw-mode + alt-screen handoff lives
198/// in one place.
199///
200/// The reader thread is parked first so the child (which inherits this tty)
201/// keeps every keystroke instead of racing the reader; on return, buffered
202/// terminal query replies and any pre-park keystroke are drained before the
203/// reader resumes.
204///
205/// The frame **writer** thread is then drained (bounded) before the child
206/// starts: frames are written to the tty asynchronously, and the frame that
207/// armed this suspend (e.g. minimal's final `/transcript` pump slice) is
208/// typically queued microseconds before we get here. Un-drained, those bytes
209/// race the child's own output — they can land on the child's alternate
210/// screen (so the main screen never receives them; on an inline viewport a
211/// commit's scroll then leaves every following row off by its height) or tear
212/// around the alt-screen switch, printing escape fragments (`[`…) that the
213/// renderer's diff can't see and thus never repairs.
214///
215/// In minimal mode the physical cursor is probed (`ESC[6n`) right before the
216/// child runs and right after it exits — while the reader thread is still
217/// parked, so the replies can't be stolen. The returned "cursor after the
218/// child, iff it moved" tells the caller whether the child *printed to the
219/// main screen* (cat-style pager: cursor left below its output) or *restored
220/// it* (less-style alt-screen pager: `rmcup` puts the cursor back exactly
221/// where it was): the caller re-anchors the live region below the new output
222/// in the first case and repaints in place in the second.
223fn suspend_for_child(
224    screen_mode: crate::app::ScreenMode,
225    writer_sync: &crate::render::draw::WriterSync,
226    input_paused: &std::sync::atomic::AtomicBool,
227    reader_parked: &std::sync::atomic::AtomicBool,
228    input_rx: &mut tokio::sync::mpsc::UnboundedReceiver<crossterm::event::Event>,
229    run_child: impl FnOnce(),
230) -> Option<(u16, u16)> {
231    use std::sync::atomic::Ordering;
232    // Pause the reader thread, then wait for a FRESH park (reader provably out of
233    // crossterm) so the main thread is the sole poll/read caller; bounded so a
234    // dead reader can't hang us.
235    input_paused.store(true, Ordering::Release);
236    reader_parked.store(false, Ordering::Release);
237    let park_deadline = std::time::Instant::now() + Duration::from_millis(500);
238    while !reader_parked.load(Ordering::Acquire) && std::time::Instant::now() < park_deadline {
239        std::thread::sleep(Duration::from_millis(5));
240    }
241    // Every queued frame must be ON the tty before the child takes it (and, in
242    // fullscreen, before LeaveAlternateScreen below — mirroring teardown's
243    // "no late frame after LeaveAlternateScreen" drain). Bounded like the
244    // reader park so a wedged pty can't hang the suspend.
245    if !writer_sync.wait_drained(Duration::from_millis(750)) {
246        tracing::warn!("suspend: frame writer not drained within 750ms; proceeding");
247    }
248    // Pre-child cursor probe (minimal only — minimal's startup already proved
249    // this terminal answers CPR). Reader is parked, so the reply is ours.
250    let pre_cursor = screen_mode
251        .is_minimal()
252        .then(|| crossterm::cursor::position().ok())
253        .flatten();
254    if screen_mode.is_fullscreen() {
255        xai_grok_shell::util::with_locked_stderr(|stderr| {
256            let _ = crossterm::execute!(stderr, crossterm::terminal::LeaveAlternateScreen);
257        });
258    }
259    let _ = crossterm::terminal::disable_raw_mode();
260    run_child();
§ 03 · TOOLS

工具注册与能力过滤

MCP 与插件延迟加载被放在 agent ops,而不是初始化阻塞路径。sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/agent_ops.rs:101 判定 managed MCP 是否可用,sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/agent_ops.rs:174 异步刷新 gateway catalog,sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/agent_ops.rs:240 把 launch MCP setup 迁出 initialize 关键路径。这让启动时延和外部目录服务失败不会直接拖垮主会话。

工具注册表是另一个核心控制点。sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:27 定义 process-global tool packs,sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:53 记录 ToolConfig 元数据,sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:1375 才进入原始 dispatch。也就是说,工具不是函数名硬编码,而是注册、元数据、session context、dispatch 四段组成的受控面。

▸Capability 偏序

sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:6-68Rust
6/// Capability mode applied to a session's toolset.
7///
8/// A partial order is defined via [`CapabilityMode::is_subset_of`]:
9/// `ReadOnly < ReadWrite < All` and `ReadOnly < Execute < All`.
10/// `ReadWrite` and `Execute` are *incomparable* (neither is a subset
11/// of the other). `fork_session` enforces `child <= parent`.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum CapabilityMode {
15    /// Reading and searching only. No edits, no shell, no background tasks.
16    ReadOnly,
17    /// Read + edit. No shell execution.
18    ReadWrite,
19    /// Read + shell execution + background-task control. No edits.
20    Execute,
21    /// Every tool kind allowed.
22    All,
23}
24
25impl Default for CapabilityMode {
26    /// Defaults to [`CapabilityMode::ReadWrite`] (subagent default;
27    /// the main/root session is always `All`).
28    fn default() -> Self {
29        Self::ReadWrite
30    }
31}
32
33impl CapabilityMode {
34    /// Filter `config.tools` by capability mode, returning a copy with
35    /// disallowed tools dropped.
36    ///
37    /// Tools whose `kind` is `None` (baseline, e.g. ad-hoc tools
38    /// declared via `ToolConfig::simple`) are preserved across all
39    /// modes. **MCP-origin** `kind: None` tools are NOT preserved by
40    /// this method; see `resolve_session_toolset` for the asymmetric
41    /// handling.
42    pub fn filter(self, config: &ToolServerConfig) -> ToolServerConfig {
43        let kept: Vec<ToolConfig> = config
44            .tools
45            .iter()
46            .filter(|tool| match tool.kind {
47                None => true,
48                Some(kind) => kind_allowed(self, kind),
49            })
50            .cloned()
51            .collect();
52        ToolServerConfig {
53            tools: kept,
54            behavior_preset: config.behavior_preset.clone(),
55        }
56    }
57
58    /// Whether every kind allowed by `self` is also allowed by `other`.
59    /// Used by `fork_session` to reject capability widening.
60    pub fn is_subset_of(self, other: CapabilityMode) -> bool {
61        for kind in ALL_TOOL_KINDS {
62            if kind_allowed(self, *kind) && !kind_allowed(other, *kind) {
63                return false;
64            }
65        }
66        true
67    }
68}

▸标准工具包入口

sources/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build/mod.rs:1-67Rust
1//! New-architecture tool implementations (NewTool trait).
2//!
3//! Each sub-module here contains a tool that implements `NewTool` instead
4//! of the old `Tool` trait. During migration, old implementations live in
5//! `implementations/<tool>/` and new implementations live in
6//! `implementations/grok_build/<tool>/`.
7//!
8//! The [`register_all()`] function is the single entry-point for wiring up
9//! the standard toolset. It inserts shared resources (`Terminal`,
10//! `AvailableSkills`, `BashParams`) and registers every built-in tool.
11pub mod ask_user_question;
12pub mod bash;
13#[path = "deploy_app_stub.rs"]
14pub mod deploy_app;
15pub mod enter_plan_mode;
16pub mod exit_plan_mode;
17pub mod grep;
18pub mod image_edit;
19pub mod image_gen;
20pub mod kill_task;
21pub mod list_dir;
22pub mod lsp;
23pub mod monitor;
24pub mod read_file;
25pub mod scheduler;
26pub mod search_replace;
27pub(crate) mod storage;
28pub mod task;
29pub mod task_output;
30pub mod todo;
31pub mod update_goal;
32pub mod video_gen;
33pub mod web_fetch;
34pub mod web_search;
35pub use ask_user_question::AskUserQuestionTool;
36pub use bash::BashTool;
37pub use deploy_app::{AppBuilderDeployerConfig, DEPLOY_APP_TOOL_NAME};
38pub use enter_plan_mode::EnterPlanModeTool;
39pub use exit_plan_mode::ExitPlanModeTool;
40pub use grep::GrepTool;
41pub use image_edit::{IMAGE_EDIT_TOOL_NAME, ImageEditTool};
42pub use image_gen::{
43    IMAGE_GEN_TOOL_NAME, IMAGINE_COMMAND_NAME, ImageGenTool, imagine_instruction,
44    imagine_usage_message,
45};
46pub use kill_task::{KillTaskTool, KillTerminalCommandTool};
47pub use list_dir::ListDirTool;
48pub use lsp::LspTool;
49pub use monitor::tool::MonitorTool;
50pub use read_file::ReadFileTool;
51pub use scheduler::create::{
52    SCHEDULER_CREATE_TOOL_NAME, SchedulerCreateTool, loop_schedule_instruction, loop_usage_message,
53};
54pub use scheduler::delete::{SCHEDULER_DELETE_TOOL_NAME, SchedulerDeleteTool};
55pub use scheduler::list::SchedulerListTool;
56pub use search_replace::SearchReplaceTool;
57pub use task::TaskTool;
58pub use task_output::{GetTerminalCommandOutputTool, TaskOutputTool, WaitTasksTool};
59pub use todo::TodoWriteTool;
60pub use update_goal::{UPDATE_GOAL_TOOL_NAME, UpdateGoalTool};
61pub use video_gen::{
62    IMAGE_TO_VIDEO_TOOL_NAME, IMAGINE_VIDEO_COMMAND_NAME, ImageToVideoTool,
63    REFERENCE_TO_VIDEO_TOOL_NAME, ReferenceToVideoTool, imagine_video_instruction,
64    imagine_video_usage_message,
65};
66pub use web_fetch::{WebFetchClient, WebFetchConfig, WebFetchParams, WebFetchTool};
67pub use web_search::WebSearchTool;

▸Registry Dispatch

sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:1375-1432Rust
1375    /// Execute a tool, returning only its raw output.
1376    ///
1377    /// Unlike [`call()`], this skips reminders and persistence. Used by
1378    /// `InnerDispatchForToolset` so that `use_tool`'s
1379    /// dispatch to a target tool does not double-run post-processing: the
1380    /// outer `call("use_tool")` does one round of post-processing over the
1381    /// target's output.
1382    ///
1383    /// Inner dispatch is intentionally **not** populated in the forwarded
1384    /// context. MCP tools (the targets of `use_tool`) are passthrough
1385    /// implementations that never call `use_tool` themselves, so they do
1386    /// not need inner dispatch.
1387    ///
1388    /// The `parent_ctx` carries call-id, cwd, resources, etc. from the
1389    /// outer call. A fresh child context is built from it — stripping
1390    /// `InnerDispatch` to prevent recursion.
1391    async fn call_raw(
1392        &self,
1393        tool_name: &str,
1394        tool_args: serde_json::Value,
1395        parent_ctx: xai_tool_runtime::ToolCallContext,
1396    ) -> Result<crate::types::output::ToolOutput, xai_tool_runtime::ToolError> {
1397        let (registry_id, output_converter, reverse_params) = {
1398            let tools = self.tools.read();
1399            let entry = tools
1400                .iter()
1401                .find(|t| t.client_name == tool_name)
1402                .ok_or_else(|| Self::tool_not_found_error(tool_name))?;
1403            (
1404                entry.registry_id.clone(),
1405                entry.output_converter.clone(),
1406                entry.reverse_params.clone(),
1407            )
1408        };
1409        let canonical_params = if reverse_params.is_empty() {
1410            tool_args
1411        } else {
1412            remap_json_keys(tool_args, &reverse_params)
1413        };
1414        let mut ctx = xai_tool_runtime::ToolCallContext::new(parent_ctx.call_id.clone());
1415        ctx.extensions.insert(self.resources.clone());
1416        ctx.extensions.insert_arc(Arc::clone(&self.renderer));
1417        if let Some(cwd) = parent_ctx.extensions.get::<xai_tool_runtime::Cwd>() {
1418            ctx.extensions.insert((*cwd).clone());
1419        }
1420        let tool_id = xai_tool_protocol::ToolId::new(&registry_id)
1421            .unwrap_or_else(|_| xai_tool_protocol::ToolId::new("unknown").expect("valid"));
1422        let lr_handle = self.local_registry.find(&tool_id).ok_or_else(|| {
1423            xai_tool_runtime::ToolError::not_found(
1424                tool_id,
1425                format!("Tool not found in LocalRegistry: {registry_id}"),
1426            )
1427        })?;
1428        let stream = lr_handle.execute(ctx, canonical_params).await;
1429        let value = drain_value_stream(stream).await?;
1430        (output_converter)(value)
1431            .map_err(|e| xai_tool_runtime::ToolError::custom("output_decoding", e.to_string()))
1432    }
深度阅读 · 工具注册表与权限面
§ 04 · PROTOCOL

ACP、MCP 和插件延迟加载

生命周期清理也有独立模块。sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/session_lifecycle.rs:6 写明 shutdown 与 replica finalize 合约,sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/session_lifecycle.rs:78 广播 roster 变化,sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/session_lifecycle.rs:254 清理 terminal session。对 agent 产品来说,这类“退出时谁负责收尸”的代码往往比模型调用更决定稳定性。

▸ACP initialize 契约

sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/acp_agent.rs:7-50Rust
7impl acp::Agent for MvpAgent {
8    /// In the meta, we provide
9    ///   - model_state: the model state, useful for the client to display available models and the default model.
10    ///
11    /// SINGLE-CALL INVARIANT: this method is the sole writer of
12    /// `self.auth_method_id` during initialization. It is called exactly once
13    /// per agent process by the ACP server before any session-creating
14    /// requests, while `auth_method_id` is still `None` (initialized at
15    /// `MvpAgent::new`). The auth-method block below relies on that
16    /// invariant when it unconditionally writes the default id returned by
17    /// `auth_method::build_auth_methods`. If you ever need to call
18    /// `initialize()` more than once, restore an `is_none()` guard around
19    /// the `auth_method_id` write at the call site so a re-init doesn't
20    /// silently downgrade an api-key user to a session-token user.
21    async fn initialize(
22        &self,
23        arguments: acp::InitializeRequest,
24    ) -> Result<acp::InitializeResponse, acp::Error> {
25        tracing::debug!(target : "sampling_log", "Received initialize request");
26        xai_grok_telemetry::unified_log::info("agent initialized", None, None);
27        self.start_subagent_coordinator();
28        tokio::task::spawn_blocking(|| {
29            crate::session::worktree_pool::cleanup_stale_pool_worktrees(None);
30        });
31        tokio::task::spawn_blocking(|| {
32            crate::session::persistence::cleanup_stale_sessions(None);
33        });
34        {
35            let root = crate::util::grok_home::grok_home();
36            crate::session::storage::search::SEARCH_INDEX_MANAGER.bootstrap_once(root);
37        }
38        const PERMISSION_CLEANUP_TTL_DAYS: u64 = 30;
39        static CLEANUP_PERMISSIONS_ONCE: std::sync::Once = std::sync::Once::new();
40        CLEANUP_PERMISSIONS_ONCE
41            .call_once(|| {
42                tokio::task::spawn(
43                    xai_grok_workspace::permission::cleanup_stale_permission_state(
44                        std::time::Duration::from_secs(
45                            PERMISSION_CLEANUP_TTL_DAYS * 24 * 60 * 60,
46                        ),
47                    ),
48                );
49            });
50        xai_grok_workspace::trust::migrate_legacy_hook_trust();

▸Managed MCP 刷新

sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/agent_ops.rs:101-151Rust
101    fn has_managed_mcp_auth(&self) -> bool {
102        self.auth_manager
103            .current_or_expired()
104            .is_some_and(|a| a.is_managed_mcp_eligible())
105    }
106    /// Requires feature flag AND xAI authentication (OIDC or legacy WebLogin).
107    pub(super) fn can_fetch_managed_mcps(&self) -> bool {
108        let cfg = self.cfg.borrow();
109        cfg.managed_mcps_enabled && !cfg.managed_mcp_gateway_tools_enabled
110            && self.has_managed_mcp_auth()
111    }
112    fn can_fetch_managed_mcp_gateway_tools(&self) -> bool {
113        self.cfg.borrow().managed_mcp_gateway_tools_enabled
114            && self.has_managed_mcp_auth()
115    }
116    pub async fn get_managed_mcp_configs(
117        &self,
118    ) -> Vec<crate::session::managed_mcp::ManagedMcpConfig> {
119        if !self.can_fetch_managed_mcps() {
120            return vec![];
121        }
122        let proxy_url = self.cfg.borrow().endpoints.proxy_url();
123        crate::session::managed_mcp::fetch_managed_mcp_configs(
124                &self.managed_mcp_cache,
125                &proxy_url,
126                &self.auth_manager,
127            )
128            .await
129    }
130    pub async fn get_managed_mcp_gateway_tool_catalog(
131        &self,
132    ) -> Option<crate::session::managed_mcp::GatewayToolCatalog> {
133        if !self.can_fetch_managed_mcp_gateway_tools() {
134            self.managed_mcp_cache.lock().await.disable_gateway_tools();
135            return None;
136        }
137        self.managed_mcp_cache.lock().await.enable_gateway_tools();
138        let proxy_url = self.cfg.borrow().endpoints.proxy_url();
139        let auth_key = self
140            .auth_manager
141            .get_valid_token()
142            .await
143            .ok()
144            .or_else(|| self.auth_manager.current_or_expired().map(|a| a.key));
145        crate::session::managed_mcp::get_or_fetch_gateway_tool_catalog(
146                &self.managed_mcp_cache,
147                &proxy_url,
148                auth_key.as_deref(),
149            )
150            .await
151    }

▸MCP over ACP wire key

sources/grok-build/crates/codegen/xai-grok-mcp/src/wire.rs:1-27Rust
1//! Single source of truth for the `x.ai/mcp/*` ACP wire strings.
2//!
3//! These method/`_meta` keys are part of the cross-language MCP-over-ACP
4//! protocol the SDK speaks (mirrors the SDK's `_mcp_wire.py` / `mcpWire.ts`).
5//! Reference these constants instead of re-typing the literals so the agent and
6//! SDK can't drift apart.
7
8/// Forward tool-invocation method (client -> agent): `x.ai/mcp/call`.
9///
10/// The pager/client asks the agent to invoke an MCP tool on a server the agent is
11/// connected to, outside the LLM loop. See `extensions::mcp::handle_call`.
12pub const MCP_CALL: &str = "x.ai/mcp/call";
13
14/// Reverse zero-IPC tool-invocation method (agent -> client): `x.ai/mcp/sdk_call`.
15///
16/// The agent invokes a tool that lives in the SDK's in-process MCP server by sending
17/// the MCP JSON-RPC message back to the client over the ACP reverse channel. Distinct
18/// from [`MCP_CALL`] so the two disjoint schemas don't share a method string for
19/// metrics/tracing. See the agent-side ACP invoker that handles this method.
20pub const MCP_SDK_CALL: &str = "x.ai/mcp/sdk_call";
21
22/// `session/new` `_meta` key listing in-process SDK MCP servers: `x.ai/mcp/servers`.
23pub const MCP_SERVERS: &str = "x.ai/mcp/servers";
24
25/// `initialize` `_meta` capability flag advertising in-process SDK MCP support
26/// (enables the SDK's `transport="acp"`): `x.ai/mcp/sdk`.
27pub const MCP_SDK: &str = "x.ai/mcp/sdk";
深度阅读 · 会话生命周期与 MCP 稳定性
§ 05 · VALIDATION

测试覆盖看到的稳定性重点

仓库里最有价值的测试不是 happy path,而是 MCP SSE flood 复现:sources/grok-build/crates/codegen/xai-grok-mcp/tests/repro_sse_flood.rs:1 明确测试重连风暴,sources/grok-build/crates/codegen/xai-grok-mcp/tests/repro_sse_flood.rs:110 复现零 backoff,sources/grok-build/crates/codegen/xai-grok-mcp/tests/repro_sse_flood.rs:137 断言节流客户端控制请求量。另一个验证面在 sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:182,它通过 exhaustive 测试守住 ToolKind 分类。

▸SSE flood 复现

sources/grok-build/crates/codegen/xai-grok-mcp/tests/repro_sse_flood.rs:110-170Rust
110/// The flood: body death on the standing GET -> zero-backoff reconnect loop,
111/// one WARN per GET (set RUST_LOG=rmcp=warn to see them).
112#[tokio::test(flavor = "multi_thread")]
113async fn repro_zero_backoff_reconnect_flood() {
114    let (url, gets) = spawn_fake_server(GetBehavior::AbnormalBodyDeath).await;
115    let transport = StreamableHttpClientTransport::from_uri(url.as_str());
116    let client = ().serve(transport).await.expect("handshake against fake server should succeed");
117
118    const OBSERVE: Duration = Duration::from_secs(3);
119    tokio::time::sleep(OBSERVE).await;
120    let n = gets.load(Ordering::Relaxed);
121    let _ = client.cancel().await;
122
123    eprintln!(
124        "[repro] {n} standing-GET reconnect attempts (= WARN log lines) in {OBSERVE:?} \
125         = {:.0}/sec",
126        n as f64 / OBSERVE.as_secs_f64()
127    );
128    // A backoff-respecting client would attempt ~3-5 in 3s; the bug produces
129    // hundreds-to-thousands.
130    assert!(
131        n > 20,
132        "expected a zero-backoff reconnect flood (>20 GETs in {OBSERVE:?}), got {n}; \
133         if this FAILS with a small n, the rmcp loop got fixed - delete mcp_http_client.rs"
134    );
135}
136
137/// The fix: same body-killing server, client wrapped in `McpHttpClient` —
138/// the backoff schedule allows only a handful of reconnects.
139#[tokio::test(flavor = "multi_thread")]
140async fn throttled_client_bounds_the_flood() {
141    let (url, gets) = spawn_fake_server(GetBehavior::AbnormalBodyDeath).await;
142    let throttled = McpHttpClient::new(
143        reqwest::Client::default(),
144        "fake-server",
145        WarnBudget::default(),
146    );
147    let transport = StreamableHttpClientTransport::with_client(
148        throttled,
149        StreamableHttpClientTransportConfig::with_uri(url.as_str()),
150    );
151    let client = ().serve(transport).await.expect("handshake against fake server should succeed");
152
153    // Checkpoint: backoff must engage early (instant, instant, 0.5s => 3-4
154    // GETs by 1.2s; an unthrottled client would be in the hundreds).
155    tokio::time::sleep(Duration::from_millis(1200)).await;
156    let early = gets.load(Ordering::Relaxed);
157    assert!(
158        early <= 4,
159        "backoff must engage early; got {early} GETs by 1.2s"
160    );
161
162    const OBSERVE: Duration = Duration::from_secs(4);
163    tokio::time::sleep(OBSERVE - Duration::from_millis(1200)).await;
164    let n = gets.load(Ordering::Relaxed);
165    let _ = client.cancel().await;
166
167    eprintln!("[repro] {n} throttled reconnect attempts in {OBSERVE:?} (schedule allows ~5-6)");
168    assert!(
169        (3..=8).contains(&n),
170        "throttled reconnects should follow the backoff schedule (~5-6 in {OBSERVE:?}), got {n}"

▸Capability 测试

sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:182-245Rust
182    #[test]
183    fn capability_mode_filter_table_is_exhaustive_per_kind() {
184        for &mode in &[
185            CapabilityMode::ReadOnly,
186            CapabilityMode::ReadWrite,
187            CapabilityMode::Execute,
188            CapabilityMode::All,
189        ] {
190            for &kind in ALL_TOOL_KINDS {
191                let id = format!("kind_{kind:?}");
192                let cfg = make_cfg(vec![test_support::tc(&id, Some(kind))]);
193                let out = mode.filter(&cfg);
194                let expected_present = kind_allowed(mode, kind);
195                let actually_present = out.tools.iter().any(|t| t.id == id);
196                assert_eq!(
197                    actually_present, expected_present,
198                    "({mode:?}, {kind:?}): expected present={expected_present}, got {actually_present}"
199                );
200            }
201        }
202    }
203
204    #[test]
205    fn capability_mode_filter_anchored_membership() {
206        let cfg = make_cfg(vec![
207            test_support::tc("read", Some(ToolKind::Read)),
208            test_support::tc("search", Some(ToolKind::Search)),
209            test_support::tc("inspect", Some(ToolKind::Lsp)),
210            test_support::tc("edit", Some(ToolKind::Edit)),
211            test_support::tc("write", Some(ToolKind::Write)),
212            test_support::tc("bash", Some(ToolKind::Execute)),
213            test_support::tc("bg", Some(ToolKind::BackgroundTaskAction)),
214            test_support::tc("plan", Some(ToolKind::Plan)),
215            test_support::tc("ask", Some(ToolKind::AskUser)),
216            test_support::tc("other", Some(ToolKind::Other)),
217        ]);
218
219        let names = |c: &ToolServerConfig| -> Vec<String> {
220            c.tools.iter().map(|t| t.id.clone()).collect()
221        };
222
223        let ro = CapabilityMode::ReadOnly.filter(&cfg);
224        assert_eq!(names(&ro), vec!["read", "search", "inspect", "plan", "ask"]);
225
226        let rw = CapabilityMode::ReadWrite.filter(&cfg);
227        assert_eq!(
228            names(&rw),
229            vec!["read", "search", "inspect", "edit", "write", "plan", "ask"]
230        );
231
232        let ex = CapabilityMode::Execute.filter(&cfg);
233        assert_eq!(
234            names(&ex),
235            vec!["read", "search", "inspect", "bash", "bg", "plan", "ask"]
236        );
237
238        let all = CapabilityMode::All.filter(&cfg);
239        assert_eq!(
240            names(&all),
241            vec![
242                "read", "search", "inspect", "edit", "write", "bash", "bg", "plan", "ask", "other"
243            ]
244        );
245    }
§ 06 · RISKS

亮点之外的风险

第一个风险是仓库由 monorepo 同步而来,README 说明这不是唯一真实开发源;源码路径如 sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1456 和 sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:27 能读到设计,但 issue/PR 上下文可能不完整。第二个风险是权限面很强但复杂,sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:116、sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/acp_agent.rs:189、sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/agent_ops.rs:303 都是安全审计需要长期盯住的点。

补充追踪启动路径时,sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1598 到 sources/grok-build/crates/codegen/xai-grok-pager-bin/src/main.rs:1775 覆盖 Version、Agent、MCP、Plugin、Models、Workspace、Sessions 等命令分派;结合 sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:260 和 sources/grok-build/crates/codegen/xai-grok-pager/src/app/event_loop.rs:411,可以看到命令模式与全屏 TUI 是同一个二进制下的两条运行路径。

工具层还有两处值得跟读:sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:1500 准备 dispatch 上下文,sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:2170 用测试固定内建工具清单,sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:2231 验证工具按 ToolKind 解析。MCP 侧则可以从 sources/grok-build/crates/codegen/xai-grok-mcp/tests/repro_sse_flood.rs:31、sources/grok-build/crates/codegen/xai-grok-mcp/tests/repro_sse_flood.rs:47、sources/grok-build/crates/codegen/xai-grok-mcp/tests/repro_sse_flood.rs:90 看出假服务如何制造重连压力。

最后,sources/grok-build/crates/codegen/xai-grok-shell/src/agent/mvp_agent/session_lifecycle.rs:316 说明 session supervisor 会独立启动,这是判断后台 actor 是否可长期运行的补充证据。

▸审计建议

优先读启动器
main.rs 告诉你真实运行形态和参数面。
优先测工具权限
capability.rs 与 registry/types.rs 是安全回归入口。
关注 MCP 重连
SSE flood 测试体现外部服务不稳定时的风险。
关注会话清理
session_lifecycle.rs 决定后台 actor 是否泄漏。
Generated · 2026-07-16xai-org/grok-build · source-level teardown