Skip to main content

oxios_kernel/tools/
registry.rs

1//! Tool metadata registry — known tools catalog for the frontend.
2//!
3//! This module provides a static catalog of all Oxios kernel tools
4//! and their metadata (name, description, category). The frontend
5//! settings UI uses this via `GET /api/tools/registry` to render
6//! the `allowed_tools` multi-select widget.
7//!
8//! The catalog is a superset of the tools registered in
9//! [`super::registration::register_tools_from_cspace_gated`].
10//! It includes all always-on tools and all CSpace-driven tools.
11//!
12//! MCP tools are dynamically registered per-server and are NOT
13//! included here. Users can type MCP tool names manually when
14//! customising the `allowed_tools` list.
15
16use serde::Serialize;
17
18/// Phase D: per-tool human intervention requirement (LobeHub-aligned).
19/// Drives the frontend 4-tier tool render registry's `interventions` slot.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
21pub enum HumanIntervention {
22    /// Tool is safe to run without user confirmation.
23    #[default]
24    None,
25    /// Approval required only when args match certain criteria (path outside
26    /// sandbox, etc.). The existing AccessGate path-based check remains the
27    /// primary gate; this level is informational for the UI.
28    Conditional,
29    /// Approval always required before execution.
30    Required,
31}
32
33/// Metadata for a single tool in the registry.
34#[derive(Debug, Clone, Serialize)]
35pub struct ToolMeta {
36    /// Tool identifier (matches `AgentTool::name()`).
37    pub name: &'static str,
38    /// Human-readable description key (frontend translates via i18n).
39    pub description_key: &'static str,
40    /// Category slug for UI grouping.
41    pub category: &'static str,
42    /// Phase D: whether this tool requires human approval before execution.
43    #[serde(default)]
44    pub human_intervention: HumanIntervention,
45}
46
47impl ToolMeta {
48    pub const fn new(
49        name: &'static str,
50        description_key: &'static str,
51        category: &'static str,
52    ) -> Self {
53        Self {
54            name,
55            description_key,
56            category,
57            human_intervention: HumanIntervention::None,
58        }
59    }
60
61    /// Phase D: builder method to override the default intervention level.
62    pub const fn with_intervention(mut self, level: HumanIntervention) -> Self {
63        self.human_intervention = level;
64        self
65    }
66}
67
68/// Return the full static tool catalog.
69///
70/// This is the single source of truth for "which tools exist" shown
71/// in the frontend settings. The list mirrors
72/// [`super::registration`] — always-on tools + CSpace-driven tools.
73pub fn known_tools() -> &'static [ToolMeta] {
74    TOOL_CATALOG
75}
76
77const TOOL_CATALOG: &[ToolMeta] = &[
78    // ── Always-on tools (registered for every agent) ──────────────
79    ToolMeta::new("read", "tools.read", "fs"),
80    ToolMeta::new("write", "tools.write", "fs").with_intervention(HumanIntervention::Conditional),
81    ToolMeta::new("edit", "tools.edit", "fs").with_intervention(HumanIntervention::Conditional),
82    ToolMeta::new("grep", "tools.grep", "fs"),
83    ToolMeta::new("find", "tools.find", "fs"),
84    ToolMeta::new("ls", "tools.ls", "fs"),
85    ToolMeta::new("web_search", "tools.webSearch", "comms"),
86    ToolMeta::new("get_search_results", "tools.getSearchResults", "comms"),
87    // ── Kernel domain tools (CSpace-driven) ───────────────────────
88    ToolMeta::new("exec", "tools.exec", "exec").with_intervention(HumanIntervention::Required),
89    ToolMeta::new("browse", "tools.browse", "comms"),
90    ToolMeta::new("memory_read", "tools.memoryRead", "memory"),
91    ToolMeta::new("memory_write", "tools.memoryWrite", "memory"),
92    ToolMeta::new("memory_search", "tools.memorySearch", "memory"),
93    ToolMeta::new("project", "tools.project", "system"),
94    ToolMeta::new("kernel_agent", "tools.kernelAgent", "system"),
95    ToolMeta::new("a2a_delegate", "tools.a2aDelegate", "a2a"),
96    ToolMeta::new("a2a_send", "tools.a2aSend", "a2a"),
97    ToolMeta::new("a2a_query", "tools.a2aQuery", "a2a"),
98    ToolMeta::new("persona", "tools.persona", "system"),
99    ToolMeta::new("cron", "tools.cron", "system"),
100    ToolMeta::new("security", "tools.security", "system"),
101    ToolMeta::new("budget", "tools.budget", "system"),
102    ToolMeta::new("resource", "tools.resource", "system"),
103    ToolMeta::new("knowledge", "tools.knowledge", "system"),
104    ToolMeta::new("calendar", "tools.calendar", "system"),
105    ToolMeta::new("send_email", "tools.sendEmail", "comms"),
106];
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn catalog_is_populated() {
114        let tools = known_tools();
115        assert!(!tools.is_empty(), "tool catalog should not be empty");
116        assert!(
117            tools.iter().any(|t| t.name == "read"),
118            "read tool should be in catalog"
119        );
120        assert!(
121            tools.iter().any(|t| t.name == "exec"),
122            "exec tool should be in catalog"
123        );
124        assert!(
125            tools.iter().any(|t| t.name == "memory_read"),
126            "memory_read should be in catalog"
127        );
128    }
129
130    #[test]
131    fn all_tools_have_required_fields() {
132        for tool in known_tools() {
133            assert!(!tool.name.is_empty(), "tool name should not be empty");
134            assert!(
135                !tool.description_key.is_empty(),
136                "description_key should not be empty"
137            );
138            assert!(!tool.category.is_empty(), "category should not be empty");
139        }
140    }
141
142    #[test]
143    fn no_duplicate_names() {
144        let names: Vec<&str> = known_tools().iter().map(|t| t.name).collect();
145        let mut sorted = names.clone();
146        sorted.sort();
147        sorted.dedup();
148        assert_eq!(names.len(), sorted.len(), "duplicate tool names found");
149    }
150}