Grok Build▹
Deep Dive · Grok Build

工具注册表与权限面

从 capability filter、ToolConfig、standard tool pack 看工具如何被发现、授权和 dispatch。

ToolKind → capability mode → filtered tools
§ 00 · DETAIL

能力过滤

权限面从 CapabilityMode 开始。sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:6 定义模式,sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:33 过滤工具集合,sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:70 放置 compile-time guard,sources/grok-build/crates/codegen/xai-grok-workspace/src/capability.rs:116 将 ToolKind 映射到读写执行类别。这是审计 destructive tools 的第一站。

▸模式偏序

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-workspace/src/capability.rs:116-163Rust
116/// Maps `(CapabilityMode, ToolKind)` -> kept-or-dropped.
117///
118/// This `match` is intentionally exhaustive: when `ToolKind` gains a
119/// new variant the compiler errors here, forcing a triage decision.
120pub(crate) fn kind_allowed(mode: CapabilityMode, kind: ToolKind) -> bool {
121    use CapabilityMode as M;
122    use ToolKind::*;
123
124    if matches!(mode, M::All) {
125        return true;
126    }
127
128    match kind {
129        // Meta tools: always allowed.
130        Plan | EnterPlan | ExitPlan | AskUser | Skill | SearchTool | GoalUpdate => true,
131
132        // Read class.
133        Read | MemoryGet | MemorySearch => {
134            matches!(mode, M::ReadOnly | M::ReadWrite | M::Execute)
135        }
136
137        // Search class.
138        Search | WebSearch | WebFetch => {
139            matches!(mode, M::ReadOnly | M::ReadWrite | M::Execute)
140        }
141
142        // Inspect class.
143        Lsp | ListDir | List => matches!(mode, M::ReadOnly | M::ReadWrite | M::Execute),
144
145        // Edit class.
146        Edit | Write | Delete | Move | ImageGen | VideoGen | ImageToVideo | ReferenceToVideo
147        | DeployApp => matches!(mode, M::ReadWrite),
148
149        // Bash / shell.
150        Execute => matches!(mode, M::Execute),
151
152        // Process control (background tasks, monitors).
153        BackgroundTaskAction | WaitTasksAction | KillTaskAction | Task | Monitor => {
154            matches!(mode, M::Execute)
155        }
156
157        // Integration dispatch.
158        UseTool => matches!(mode, M::ReadWrite | M::Execute),
159
160        // Catch-all -- only `All` mode keeps it (early-return above).
161        Other => false,
162    }
163}
§ 01 · DETAIL

Registry dispatch

工具 registry 把工具定义、session context 和 dispatch 分开。sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:27 说明 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 进入 call_raw,sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:1433 分出同步和 streaming 调用。这个结构让后续工具添加不必改 prompt 编排器。

▸ToolConfig

sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:27-92Rust
27/// Process-global registry of external "tool packs" — functions that
28/// contribute additional tool registrations into every
29/// [`ToolRegistryBuilder::new`].
30///
31/// This inverts the dependency for harness code that must live outside
32/// this crate: instead of `xai-grok-tools` referencing an out-of-tree tool
33/// pack, the pack calls
34/// [`register_tool_pack`] at startup and registers itself here.
35///
36/// # Ordering contract
37/// [`register_tool_pack`] MUST run before the FIRST `ToolRegistryBuilder::new()`
38/// in the process. Packs registered after a builder
39/// has been constructed do not retroactively apply to that builder.
40/// A tool pack: a function that contributes registrations to a builder.
41pub type ToolPack = fn(&mut ToolRegistryBuilder);
42static TOOL_PACKS: OnceLock<Mutex<Vec<ToolPack>>> = OnceLock::new();
43fn tool_packs() -> &'static Mutex<Vec<ToolPack>> {
44    TOOL_PACKS.get_or_init(|| Mutex::new(Vec::new()))
45}
46/// Register an out-of-tree tool pack. See [`TOOL_PACKS`] for the ordering
47/// contract. Idempotency is the caller's responsibility (a pack registered
48/// twice registers its tools twice).
49pub fn register_tool_pack(pack: ToolPack) {
50    tool_packs().lock().push(pack);
51}
52#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
53pub struct ToolConfig {
54    pub id: String,
55    /// tool params, keyed by fully qualified name.
56    pub params: Option<serde_json::Map<String, serde_json::Value>>,
57    /// tool_id → client-facing name.
58    pub name_override: Option<String>,
59    /// { canonical param → client-facing param }.
60    pub params_name_overrides: Option<HashMap<String, String>>,
61    /// When `Some`, replaces the tool's `description_template()` entirely.
62    ///
63    /// Use this when the same built-in tool needs a context-specific
64    /// description — e.g. `run_terminal_cmd` in a container environment
65    /// vs. the default host-shell description.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub description_override: Option<String>,
68    /// Per-tool behavior version override. Wins over `ToolServerConfig::behavior_preset`.
69    /// Only valid for version-managed tools (see `versions::MANAGED_TOOLS`).
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub behavior_version: Option<String>,
72    /// The tool's capability category. Populated automatically by
73    /// `for_tool::<T>()` / `From<&T: Tool>` and used by capability-mode
74    /// enforcement to filter tools without a hardcoded ID mapping.
75    ///
76    /// `None` means the tool's kind is unknown (e.g. MCP/custom tools
77    /// created via `ToolConfig::from_id()`). Capability-mode filtering
78    /// preserves tools with `kind: None` — this is intentional to avoid
79    /// breaking extensibility.
80    ///
81    /// `ToolKind` is `#[serde(other)]`, so an unknown deserialized `kind` becomes
82    /// `Some(Other)` (dropped by restrictive modes) rather than an error — not a
83    /// live path today since `kind` is auto-populated and `from_id` leaves it
84    /// `None`. [`deserialize_config_kind`] warns on that sink so a config typo
85    /// doesn't silently demote the tool.
86    #[serde(
87        default,
88        skip_serializing_if = "Option::is_none",
89        deserialize_with = "deserialize_config_kind"
90    )]
91    pub kind: Option<ToolKind>,
92}

▸Dispatch

sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:1375-1499Rust
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    }
1433    /// Dispatch a tool call by client-facing name with client-facing params.
1434    ///
1435    /// `cwd_override` — optional per-call working directory. When `Some`, tools
1436    /// will use this instead of the session `Cwd` from Resources. This is
1437    /// stack-local (not shared state), so concurrent calls with different
1438    /// overrides don't race.
1439    pub async fn call(
1440        self: &Arc<Self>,
1441        tool_name: &str,
1442        tool_args: serde_json::Value,
1443        tool_call_id: &str,
1444        cwd_override: Option<std::path::PathBuf>,
1445    ) -> Result<ToolRunResult, xai_tool_runtime::ToolError> {
1446        use futures::StreamExt;
1447        let mut stream = self.call_streaming(tool_name, tool_args, tool_call_id, cwd_override);
1448        while let Some(item) = stream.next().await {
1449            match item {
1450                xai_tool_runtime::ToolStreamItem::Progress(_) => continue,
1451                xai_tool_runtime::ToolStreamItem::Terminal(result) => return result,
1452            }
1453        }
1454        Err(stream_no_terminal_error())
1455    }
1456    /// Streaming sibling of [`call`].
1457    ///
1458    /// Forwards every inner [`ToolStreamItem::Progress`] unchanged, and when the
1459    /// inner dispatch stream reaches its terminal, runs the shared
1460    /// post-processing tail ([`finalize_output`]) on it and yields the resulting
1461    /// [`ToolRunResult`] as the single terminal of the outer stream.
1462    ///
1463    /// This is a non-`async` inherent method: it synchronously builds and
1464    /// returns a `'static` boxed stream. Because [`ToolStream`] is `'static`,
1465    /// all `.await` and `Arc::clone(self)` happen *inside* the stream block so
1466    /// nothing borrows `self` across the stream.
1467    ///
1468    /// [`ToolStream`]: xai_tool_runtime::ToolStream
1469    /// [`ToolStreamItem::Progress`]: xai_tool_runtime::ToolStreamItem::Progress
1470    pub fn call_streaming(
1471        self: &Arc<Self>,
1472        tool_name: &str,
1473        tool_args: serde_json::Value,
1474        tool_call_id: &str,
1475        cwd_override: Option<std::path::PathBuf>,
1476    ) -> xai_tool_runtime::ToolStream<ToolRunResult> {
1477        use futures::StreamExt;
1478        let this = Arc::clone(self);
1479        let tool_name = tool_name.to_owned();
1480        let tool_call_id = tool_call_id.to_owned();
1481        Box::pin(async_stream::stream! {
1482            let parts = match this.prepare_dispatch(& tool_name, tool_args, &
1483            tool_call_id, cwd_override,) { Ok(parts) => parts, Err(e) => { yield
1484            xai_tool_runtime::ToolStreamItem::Terminal(Err(e)); return; } }; let
1485            DispatchParts { lr_handle, ctx, canonical_params, output_converter,
1486            effective_tool_name, } = parts; let mut inner = lr_handle.execute(ctx,
1487            canonical_params). await; while let Some(item) = inner.next(). await {
1488            match item { xai_tool_runtime::ToolStreamItem::Progress(p) => { yield
1489            xai_tool_runtime::ToolStreamItem::Progress(p); }
1490            xai_tool_runtime::ToolStreamItem::Terminal(Err(e)) => { yield
1491            xai_tool_runtime::ToolStreamItem::Terminal(Err(e)); return; }
1492            xai_tool_runtime::ToolStreamItem::Terminal(Ok(typed)) => { let run_result
1493            = this.finalize_output(typed.value, & output_converter,
1494            effective_tool_name). await; yield
1495            xai_tool_runtime::ToolStreamItem::Terminal(run_result); return; } } }
1496            yield
1497            xai_tool_runtime::ToolStreamItem::Terminal(Err(stream_no_terminal_error()));
1498        })
1499    }

如果要审计工具链完整性,继续读 sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:112、sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:215、sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:1500、sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:1525。这些行把 ToolConfig、SessionContext、dispatch 准备和调用边界连起来,能解释为什么新增工具必须先经过 registry,而不能绕过权限模型。

测试与元数据也有信号: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 查找,sources/grok-build/crates/codegen/xai-grok-tools/src/registry/types.rs:2255 给工具元数据补充 x.ai/tool 标记;concise bash 的 sources/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build_concise/bash.rs:189 到 sources/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build_concise/bash.rs:259 则说明输出压缩策略有回归测试。

§ 02 · DETAIL

标准工具包

标准工具包入口集中在 sources/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build/mod.rs:1 到 sources/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build/mod.rs:67,能看到 bash、grep、list dir、LSP、search replace、task、todo、goal、web、image/video 等工具边界。concise bash 在 sources/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build_concise/bash.rs:117 包装 BashTool 并覆盖 prompt 输出,是“同一工具不同展示策略”的例子。

▸工具包入口

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;

▸Concise Bash

sources/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build_concise/bash.rs:117-165Rust
117impl xai_tool_runtime::Tool for BashConciseTool {
118    type Args = BashToolInput;
119    type Output = BashToolOutput;
120
121    fn id(&self) -> xai_tool_protocol::ToolId {
122        xai_tool_protocol::ToolId::new("run_terminal_cmd").expect("valid tool id")
123    }
124
125    fn description(
126        &self,
127        _ctx: &::xai_tool_runtime::ListToolsContext,
128    ) -> xai_tool_types::ToolDescription {
129        xai_tool_types::ToolDescription::new(
130            "run_terminal_cmd",
131            crate::types::tool_metadata::ToolMetadata::description_template(self),
132        )
133    }
134
135    fn capabilities(&self) -> xai_tool_protocol::ToolCapabilities {
136        xai_tool_protocol::ToolCapabilities {
137            is_read_only: false,
138            tool_scope: Some(xai_tool_protocol::ToolScope::Write),
139            ..Default::default()
140        }
141    }
142
143    #[tracing::instrument(name = "tool.run_terminal_cmd_concise", skip_all)]
144    async fn run(
145        &self,
146        ctx: xai_tool_runtime::ToolCallContext,
147        input: BashToolInput,
148    ) -> Result<BashToolOutput, xai_tool_runtime::ToolError> {
149        let result = xai_tool_runtime::Tool::run(&BashTool, ctx, input).await?;
150
151        match result {
152            // TODO: Add different concise message for auto backgrounded terminal task
153            BashToolOutput::Foreground(mut bash) => {
154                let is_backgrounded = bash.signal.as_deref() == Some("backgrounded");
155                bash.output_for_prompt = if is_backgrounded {
156                    format_concise_background_prompt(&bash)
157                } else {
158                    format_concise_foreground_prompt(&bash)
159                };
160                Ok(BashToolOutput::Foreground(bash))
161            }
162            bg @ BashToolOutput::Background(_) => Ok(bg),
163        }
164    }
165}
Generated · 2026-07-16xai-org/grok-build · source-level teardown