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