Skip to main content

oxios_kernel/tools/
registration.rs

1//! CSpace → Tool Registry mapping.
2//!
3//! This module bridges the capability system and the agent's tool registry.
4//! Given an agent's [`CSpace`], it walks the capabilities and registers
5//! exactly the set of tools the agent is authorised to use.
6//!
7//! # Registration tiers
8//!
9//! | Tier | Tools | Condition |
10//! |------|-------|-----------|
11//! | Always-on | `ReadTool`, `WriteTool`, `EditTool`, `GrepTool`, `FindTool`, `LsTool`, `WebSearchTool`, `GetSearchResultsTool` | Every agent gets these |
12//! | CSpace-driven | `ExecTool`, `BrowserTool`, kernel domain tools, MCP, A2A, etc. | Only if a matching capability with sufficient rights exists |
13//!
14//! # Example
15//!
16//! ```no_run
17//! use std::sync::Arc;
18//! use oxi_sdk::ToolRegistry;
19//! use oxios_kernel::capability::template::CapabilityTemplate;
20//!
21//! let registry = ToolRegistry::new();
22//! let cspace = CapabilityTemplate::standard().build();
23//! let cache = Arc::new(oxi_sdk::SearchCache::new());
24//! // register_tools_from_cspace(&registry, &kernel, &cspace, cache, agent_id);
25//! ```
26
27use std::sync::Arc;
28
29use oxi_sdk::{
30    EditTool, FindTool, GetSearchResultsTool, GrepTool, LsTool, ReadTool, SearchCache,
31    ToolRegistry, WebSearchTool, WriteTool,
32};
33
34use crate::KernelHandle;
35use crate::access_manager::{AccessGate, AgentContext};
36use crate::capability::{CSpace, ResourceRef, Rights};
37use crate::tools::builtin::*;
38use crate::tools::gated_tool::GatedTool;
39use crate::tools::{A2aDelegateTool, A2aQueryTool, A2aSendTool, ExecTool, KnowledgeTool};
40use crate::types::AgentId;
41
42/// Register the always-on tool set into a [`ToolRegistry`].
43///
44/// Every agent receives these tools regardless of its capability space.
45/// This consists of file-system tools (read, write, edit, grep, find, ls)
46/// and web search tools.
47///
48/// This helper is also useful for unit tests that need a basic tool set
49/// without constructing a full CSpace.
50pub fn register_always_on(registry: &ToolRegistry, search_cache: Arc<SearchCache>) {
51    registry.register(ReadTool::new());
52    registry.register(WriteTool::new());
53    registry.register(EditTool::new());
54    registry.register(GrepTool::new());
55    registry.register(FindTool::new());
56    registry.register(LsTool::new());
57    registry.register(WebSearchTool::new(search_cache.clone()));
58    registry.register(GetSearchResultsTool::new(search_cache));
59}
60
61/// Register the headless-browser browse tools when the engine is available.
62///
63/// The concrete registration runs only with the `native-browser` feature;
64/// without it this is a no-op stub so the file compiles unchanged.
65#[cfg(feature = "native-browser")]
66fn register_browser_tools(kernel: &KernelHandle, registry: &ToolRegistry) {
67    if let Some(browser) = &kernel.browser
68        && let Some(engine) = browser.try_engine()
69    {
70        registry.register(oxi_sdk::BrowseTool::new(engine.clone()));
71        registry.register(oxi_sdk::BrowseExtractTool::new(engine.clone()));
72        registry.register(oxi_sdk::BrowseSessionTool::new(engine.clone()));
73        registry.register(oxi_sdk::BrowseScriptTool::new(engine));
74    }
75}
76
77/// No-op stub when the `native-browser` feature is disabled.
78#[cfg(not(feature = "native-browser"))]
79fn register_browser_tools(_kernel: &KernelHandle, _registry: &ToolRegistry) {}
80
81/// Register always-on tools with access gate and (RFC-035) approval wrapping.
82///
83/// Same as [`register_always_on`] but wraps each tool in [`GatedTool`] so that
84/// all file operations pass through the access gate and approval gate.
85///
86/// `approval_gate`, `event_bus`, and `pending_approvals` may all be `None`
87/// for headless / test paths; the gated tool still honors the access gate
88/// but skips the approval step.
89#[allow(clippy::too_many_arguments)]
90pub fn register_always_on_gated(
91    registry: &ToolRegistry,
92    search_cache: Arc<SearchCache>,
93    gate: Arc<AccessGate>,
94    context: AgentContext,
95    approval_gate: Option<Arc<crate::approval::ApprovalGate>>,
96    event_bus: Option<crate::event_bus::EventBus>,
97    pending_approvals: Option<Arc<crate::tools::PendingToolApprovals>>,
98    pending_path_access: Option<Arc<crate::tools::PendingPathAccess>>,
99) {
100    registry.register(GatedTool::with_approval(
101        ReadTool::new(),
102        gate.clone(),
103        context.clone(),
104        approval_gate.clone(),
105        event_bus.clone(),
106        pending_approvals.clone(),
107        pending_path_access.clone(),
108    ));
109    registry.register(GatedTool::with_approval(
110        WriteTool::new(),
111        gate.clone(),
112        context.clone(),
113        approval_gate.clone(),
114        event_bus.clone(),
115        pending_approvals.clone(),
116        pending_path_access.clone(),
117    ));
118    registry.register(GatedTool::with_approval(
119        EditTool::new(),
120        gate.clone(),
121        context.clone(),
122        approval_gate.clone(),
123        event_bus.clone(),
124        pending_approvals.clone(),
125        pending_path_access.clone(),
126    ));
127    registry.register(GatedTool::with_approval(
128        GrepTool::new(),
129        gate.clone(),
130        context.clone(),
131        approval_gate.clone(),
132        event_bus.clone(),
133        pending_approvals.clone(),
134        pending_path_access.clone(),
135    ));
136    registry.register(GatedTool::with_approval(
137        FindTool::new(),
138        gate.clone(),
139        context.clone(),
140        approval_gate.clone(),
141        event_bus.clone(),
142        pending_approvals.clone(),
143        pending_path_access.clone(),
144    ));
145    registry.register(GatedTool::with_approval(
146        LsTool::new(),
147        gate.clone(),
148        context.clone(),
149        approval_gate.clone(),
150        event_bus.clone(),
151        pending_approvals.clone(),
152        pending_path_access.clone(),
153    ));
154    registry.register(GatedTool::with_approval(
155        WebSearchTool::new(search_cache.clone()),
156        gate.clone(),
157        context.clone(),
158        approval_gate.clone(),
159        event_bus.clone(),
160        pending_approvals.clone(),
161        pending_path_access.clone(),
162    ));
163    registry.register(GatedTool::with_approval(
164        GetSearchResultsTool::new(search_cache),
165        gate,
166        context,
167        approval_gate,
168        event_bus,
169        pending_approvals,
170        pending_path_access.clone(),
171    ));
172}
173
174/// Register tools into `registry` based on the agent's [`CSpace`].
175///
176/// First registers the always-on tier (file ops + web search), then walks
177/// every capability in the CSpace and conditionally registers the
178/// corresponding kernel tools.
179///
180/// # Arguments
181///
182/// * `registry` — The agent's tool registry to populate.
183/// * `kernel` — Handle to the kernel for constructing tool instances.
184/// * `cspace` — The agent's capability space (determines which tools are available).
185/// * `search_cache` — Shared search cache for web search tools.
186/// * `agent_id` — The agent's ID (used by A2A tools for routing).
187///
188/// # CSpace → Tool mapping
189///
190/// | ResourceRef | Required rights | Registered tools |
191/// |-------------|----------------|-----------------|
192/// | `Exec { .. }` | `EXECUTE` | `ExecTool` |
193/// | `KernelDomain { "memory" }` | — | *(registered unconditionally in `register_all_kernel_tools`)* |
194/// | `KernelDomain { "project" }` | any | `ProjectTool` |
195/// | `KernelDomain { "agent" }` | any | `KernelAgentTool` |
196/// | `KernelDomain { "a2a" }` | any | `A2aDelegateTool`, `A2aSendTool`, `A2aQueryTool` |
197/// | `KernelDomain { "persona" }` | any | `PersonaTool` |
198/// | `KernelDomain { "program" }` | any | *(deprecated — skills via CSpace)* |
199/// | `KernelDomain { "cron" }` | any | `CronTool` |
200/// | `KernelDomain { "security" }` | any | `SecurityTool` |
201/// | `KernelDomain { "budget" }` | any | `BudgetTool` |
202/// | `KernelDomain { "resource" }` | any | `ResourceTool` |
203/// | `KernelDomain { "mcp" }` | any | `McpToolWrapper` |
204/// | `Program { .. }` | — | *(not registered; surfaced via ToolRetriever)* |
205pub fn register_tools_from_cspace(
206    registry: &ToolRegistry,
207    kernel: &KernelHandle,
208    cspace: &CSpace,
209    search_cache: Arc<SearchCache>,
210    agent_id: AgentId,
211) {
212    // ── Tier 1: Always-on tools ─────────────────────────────────────
213    register_always_on(registry, search_cache);
214
215    // ── Tier 2: CSpace-driven tools ─────────────────────────────────
216    for cap in cspace.iter() {
217        match &cap.resource {
218            // Command execution
219            ResourceRef::Exec { .. } if cap.rights.contains(Rights::EXECUTE) => {
220                registry.register(ExecTool::from_kernel(kernel));
221            }
222
223            // Headless browser — SDK browse tools (pure-Rust oxibrowser-core).
224            ResourceRef::Browser if cap.rights.contains(Rights::EXECUTE) => {
225                register_browser_tools(kernel, registry);
226            }
227
228            // Kernel domain tools
229            ResourceRef::KernelDomain { domain } => match domain.as_str() {
230                "memory" => { /* Registered unconditionally in register_all_kernel_tools */ }
231                "agent" => registry.register(KernelAgentTool::from_kernel(kernel)),
232                "a2a" => {
233                    registry.register(A2aDelegateTool::from_kernel(kernel, agent_id));
234                    registry.register(A2aSendTool::from_kernel(kernel, agent_id));
235                    registry.register(A2aQueryTool::from_kernel(kernel));
236                }
237                "persona" => registry.register(PersonaTool::from_kernel(kernel)),
238                "program" => { /* Skills are surfaced through CSpace + semantic retrieval, not individual tools */
239                }
240                "cron" => registry.register(CronTool::from_kernel(kernel)),
241                "security" => registry.register(SecurityTool::from_kernel(kernel)),
242                "budget" => registry.register(BudgetTool::from_kernel(kernel)),
243                "resource" => registry.register(ResourceTool::from_kernel(kernel)),
244                "knowledge" => registry.register(KnowledgeTool::from_kernel(kernel)),
245                "mcp" => { /* MCP tools are enumerated dynamically per agent */ }
246                _ => {} // Unknown domain — silently skip
247            },
248
249            // Programs are not registered as separate tools.
250            // ToolRetriever shows them in the capability index;
251            // agents use exec to run program commands.
252            ResourceRef::Skill { .. } => {}
253
254            // Space, Agent, Mcp resource refs are handled through
255            // their respective KernelDomain registrations above
256            // or through dedicated tool paths.
257            _ => {}
258        }
259    }
260}
261
262/// Register tools into `registry` with access gate + approval gate enforcement.
263///
264/// Same as [`register_tools_from_cspace`] but:
265/// - Always-on tools are wrapped in [`GatedTool`] for permission + approval checks
266/// - ExecTool is created with `AgentContext` and wrapped in [`GatedTool`] so
267///   RFC-035 Step 2.5 also covers shell + structured exec calls (replacing
268///   the bespoke exec-only shell approval block)
269///
270/// Use this in production. The ungated version exists for backward compatibility.
271///
272/// # Arguments
273///
274/// * `registry` — The agent's tool registry to populate.
275/// * `kernel` — Handle to the kernel for constructing tool instances.
276/// * `cspace` — The agent's capability space (determines which tools are available).
277/// * `search_cache` — Shared search cache for web search tools.
278/// * `agent_id` — The agent's ID (used by A2A tools for routing).
279/// * `gate` — The unified access gate for permission checks.
280/// * `context` — The agent's security context.
281/// * `approval_gate` — RFC-035 approval gate; consults declared policy,
282///   config overrides, and global resolvers per tool call.
283/// * `event_bus` — Publishes `KernelEvent::ApprovalRequested` when
284///   `RequireApproval` is returned.
285/// * `pending_approvals` — Shared registry of pending user decisions.
286#[allow(clippy::too_many_arguments)]
287pub fn register_tools_from_cspace_gated(
288    registry: &ToolRegistry,
289    kernel: &KernelHandle,
290    cspace: &CSpace,
291    search_cache: Arc<SearchCache>,
292    agent_id: AgentId,
293    gate: Arc<AccessGate>,
294    context: AgentContext,
295    approval_gate: Option<Arc<crate::approval::ApprovalGate>>,
296    event_bus: Option<crate::event_bus::EventBus>,
297    pending_approvals: Option<Arc<crate::tools::PendingToolApprovals>>,
298    pending_path_access: Option<Arc<crate::tools::PendingPathAccess>>,
299) {
300    // ── Tier 1: Always-on tools (gated) ──────────────────────────────
301    register_always_on_gated(
302        registry,
303        search_cache,
304        gate.clone(),
305        context.clone(),
306        approval_gate.clone(),
307        event_bus.clone(),
308        pending_approvals.clone(),
309        pending_path_access.clone(),
310    );
311
312    // ── Tier 2: CSpace-driven tools ─────────────────────────────────
313    for cap in cspace.iter() {
314        match &cap.resource {
315            // Command execution — wrap in GatedTool so Step 2.5 (RFC-035)
316            // also fires for exec. The inner ExecTool retains its own
317            // context, binary allowlist + access manager checks; the outer
318            // GatedTool runs the unified access gate + approval pipeline.
319            ResourceRef::Exec { .. } if cap.rights.contains(Rights::EXECUTE) => {
320                registry.register(GatedTool::with_approval(
321                    ExecTool::from_kernel_with_context(kernel, context.clone()),
322                    gate.clone(),
323                    context.clone(),
324                    approval_gate.clone(),
325                    event_bus.clone(),
326                    pending_approvals.clone(),
327                    pending_path_access.clone(),
328                ));
329            }
330
331            // Headless browser — SDK browse tools.
332            ResourceRef::Browser if cap.rights.contains(Rights::EXECUTE) => {
333                register_browser_tools(kernel, registry);
334            }
335
336            // Kernel domain tools (same as ungated — these already use KernelHandle internally)
337            ResourceRef::KernelDomain { domain } => match domain.as_str() {
338                "memory" => { /* Registered unconditionally in register_all_kernel_tools */ }
339                "space" => registry.register(ProjectTool::from_kernel(kernel)),
340                "agent" => registry.register(KernelAgentTool::from_kernel(kernel)),
341                "a2a" => {
342                    registry.register(A2aDelegateTool::from_kernel(kernel, agent_id));
343                    registry.register(A2aSendTool::from_kernel(kernel, agent_id));
344                    registry.register(A2aQueryTool::from_kernel(kernel));
345                }
346                "persona" => registry.register(PersonaTool::from_kernel(kernel)),
347                "program" => {}
348                "cron" => registry.register(CronTool::from_kernel(kernel)),
349                "security" => registry.register(SecurityTool::from_kernel(kernel)),
350                "budget" => registry.register(BudgetTool::from_kernel(kernel)),
351                "resource" => registry.register(ResourceTool::from_kernel(kernel)),
352                "knowledge" => registry.register(KnowledgeTool::from_kernel(kernel)),
353                "mcp" => {}
354                _ => {}
355            },
356
357            ResourceRef::Skill { .. } => {}
358            _ => {}
359        }
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn register_always_on_registers_eight_tools() {
369        let registry = ToolRegistry::new();
370        let cache = Arc::new(SearchCache::new());
371        register_always_on(&registry, cache);
372
373        // The always-on set is: read, write, edit, grep, find, ls, web_search, get_search_results
374        // ToolRegistry doesn't expose a count, but we can verify individual tool names.
375        let tool_names = registry.names();
376        assert!(
377            tool_names.contains(&"read".to_string()),
378            "read tool should be registered"
379        );
380        assert!(
381            tool_names.contains(&"write".to_string()),
382            "write tool should be registered"
383        );
384        assert!(
385            tool_names.contains(&"edit".to_string()),
386            "edit tool should be registered"
387        );
388        assert!(
389            tool_names.contains(&"grep".to_string()),
390            "grep tool should be registered"
391        );
392        assert!(
393            tool_names.contains(&"find".to_string()),
394            "find tool should be registered"
395        );
396        assert!(
397            tool_names.contains(&"ls".to_string()),
398            "ls tool should be registered"
399        );
400        assert!(
401            tool_names.contains(&"web_search".to_string()),
402            "web_search tool should be registered"
403        );
404        assert!(
405            tool_names.contains(&"get_search_results".to_string()),
406            "get_search_results tool should be registered"
407        );
408    }
409}