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