工具注册表与权限面
从 capability filter、ToolConfig、standard tool pack 看工具如何被发现、授权和 dispatch。
能力过滤
权限面从 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 的第一站。
▸模式偏序
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}
▸工具分类
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}
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
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
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(®istry_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 则说明输出压缩策略有回归测试。
标准工具包
标准工具包入口集中在 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 输出,是“同一工具不同展示策略”的例子。
▸工具包入口
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
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}