1use std::collections::{HashMap, HashSet};
7use std::path::Path;
8
9use super::builtin;
10use super::memory;
11use crate::llm::LlmClient;
12use crate::prompt;
13use crate::skills::{self, LoadedSkill};
14use crate::types::{EventSink, ToolDefinition, ToolResult};
15
16pub trait PlanningControlExecutor {
19 fn execute(
20 &mut self,
21 tool_name: &str,
22 arguments: &str,
23 event_sink: &mut dyn EventSink,
24 ) -> ToolResult;
25}
26use skilllite_core::config::EmbeddingConfig;
27
28#[allow(dead_code)] pub struct MemoryVectorContext<'a> {
31 pub client: &'a LlmClient,
32 pub embed_config: &'a EmbeddingConfig,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ToolCapability {
38 FilesystemWrite,
39 MemoryWrite,
40 ProcessExec,
41 Preview,
42 Delegation,
43 SkillExecution,
44}
45
46#[derive(Debug, Clone, Copy)]
48pub struct CapabilityPolicy {
49 allow_filesystem_write: bool,
50 allow_memory_write: bool,
51 allow_process_exec: bool,
52 allow_preview: bool,
53 allow_delegation: bool,
54 allow_skill_execution: bool,
55}
56
57impl Default for CapabilityPolicy {
58 fn default() -> Self {
59 Self::full_access()
60 }
61}
62
63impl CapabilityPolicy {
64 pub const fn full_access() -> Self {
66 Self {
67 allow_filesystem_write: true,
68 allow_memory_write: true,
69 allow_process_exec: true,
70 allow_preview: true,
71 allow_delegation: true,
72 allow_skill_execution: true,
73 }
74 }
75
76 pub const fn read_only() -> Self {
78 Self {
79 allow_filesystem_write: false,
80 allow_memory_write: false,
81 allow_process_exec: false,
82 allow_preview: false,
83 allow_delegation: false,
84 allow_skill_execution: false,
85 }
86 }
87
88 #[must_use]
89 pub fn with_filesystem_write(mut self, allow: bool) -> Self {
90 self.allow_filesystem_write = allow;
91 self
92 }
93
94 #[must_use]
95 pub fn with_memory_write(mut self, allow: bool) -> Self {
96 self.allow_memory_write = allow;
97 self
98 }
99
100 #[must_use]
101 pub fn with_process_exec(mut self, allow: bool) -> Self {
102 self.allow_process_exec = allow;
103 self
104 }
105
106 #[must_use]
107 pub fn with_preview(mut self, allow: bool) -> Self {
108 self.allow_preview = allow;
109 self
110 }
111
112 #[must_use]
113 pub fn with_delegation(mut self, allow: bool) -> Self {
114 self.allow_delegation = allow;
115 self
116 }
117
118 #[must_use]
119 pub fn with_skill_execution(mut self, allow: bool) -> Self {
120 self.allow_skill_execution = allow;
121 self
122 }
123
124 pub fn allows(&self, capabilities: &[ToolCapability]) -> bool {
125 capabilities.iter().all(|capability| match capability {
126 ToolCapability::FilesystemWrite => self.allow_filesystem_write,
127 ToolCapability::MemoryWrite => self.allow_memory_write,
128 ToolCapability::ProcessExec => self.allow_process_exec,
129 ToolCapability::Preview => self.allow_preview,
130 ToolCapability::Delegation => self.allow_delegation,
131 ToolCapability::SkillExecution => self.allow_skill_execution,
132 })
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::{CapabilityPolicy, ExtensionRegistry};
139
140 #[test]
141 fn read_only_policy_filters_mutating_tools() {
142 let registry = ExtensionRegistry::read_only(true, false, &[]);
143
144 assert!(registry.owns_tool("read_file"));
145 assert!(registry.owns_tool("memory_search"));
146 assert!(registry.owns_tool("complete_task"));
147 assert!(!registry.owns_tool("write_file"));
148 assert!(!registry.owns_tool("memory_write"));
149 assert!(!registry.owns_tool("run_command"));
150 assert!(!registry.owns_tool("preview_server"));
151 }
152
153 #[test]
154 fn full_registry_keeps_mutating_tools() {
155 let registry = ExtensionRegistry::new(true, false, &[]);
156
157 assert!(registry.owns_tool("write_file"));
158 assert!(registry.owns_tool("memory_write"));
159 assert!(registry.owns_tool("run_command"));
160 assert!(registry.owns_tool("preview_server"));
161 }
162
163 #[test]
164 fn custom_policy_can_allow_preview_without_other_writes() {
165 let registry = ExtensionRegistry::builder(true, false, &[])
166 .with_policy(CapabilityPolicy::read_only().with_preview(true))
167 .register(super::builtin::get_builtin_tools())
168 .register_memory_if(true)
169 .build();
170
171 assert!(registry.owns_tool("preview_server"));
172 assert!(!registry.owns_tool("write_file"));
173 assert!(!registry.owns_tool("memory_write"));
174 assert!(!registry.owns_tool("run_command"));
175 }
176
177 #[test]
178 fn planning_only_tools_excluded_when_task_planning_disabled() {
179 let registry = ExtensionRegistry::builder(true, false, &[])
180 .with_task_planning(false)
181 .register(super::builtin::get_builtin_tools())
182 .register_memory_if(true)
183 .build();
184
185 assert!(!registry.owns_tool("complete_task"));
186 assert!(!registry.owns_tool("update_task_plan"));
187 assert!(registry.owns_tool("read_file"));
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum ToolScope {
194 AllModes,
196 PlanningOnly,
198}
199
200#[derive(Debug, Clone)]
202pub enum ToolHandler {
203 BuiltinSync,
204 BuiltinAsync,
205 Memory,
206 Skill {
207 skill_name: String,
208 },
209 PlanningControl,
211}
212
213#[derive(Debug, Clone)]
215pub struct RegisteredTool {
216 pub definition: ToolDefinition,
217 pub capabilities: Vec<ToolCapability>,
218 pub handler: ToolHandler,
219 pub scope: ToolScope,
220}
221
222impl RegisteredTool {
223 pub fn new(
224 definition: ToolDefinition,
225 capabilities: Vec<ToolCapability>,
226 handler: ToolHandler,
227 ) -> Self {
228 Self {
229 definition,
230 capabilities,
231 handler,
232 scope: ToolScope::AllModes,
233 }
234 }
235
236 #[must_use]
237 pub fn with_scope(mut self, scope: ToolScope) -> Self {
238 self.scope = scope;
239 self
240 }
241
242 pub fn name(&self) -> &str {
243 &self.definition.function.name
244 }
245}
246
247#[derive(Debug, Clone, Default)]
253pub struct ToolAvailabilityView {
254 tool_names: HashSet<String>,
255 skill_names: HashSet<String>,
256}
257
258impl ToolAvailabilityView {
259 fn register(&mut self, tool: &RegisteredTool) {
260 self.tool_names.insert(tool.name().to_string());
261 if let ToolHandler::Skill { skill_name } = &tool.handler {
262 self.skill_names.insert(skill_name.clone());
263 self.skill_names.insert(skill_name.replace('-', "_"));
264 }
265 }
266
267 pub fn has_tool(&self, name: &str) -> bool {
268 self.tool_names.contains(name)
269 }
270
271 pub fn has_any_tool(&self, names: &[&str]) -> bool {
272 names.iter().any(|name| self.has_tool(name))
273 }
274
275 pub fn has_skill_hint(&self, hint: &str) -> bool {
276 self.skill_names.contains(hint) || self.skill_names.contains(&hint.replace('-', "_"))
277 }
278
279 pub fn has_any_skills(&self) -> bool {
280 !self.skill_names.is_empty()
281 }
282
283 pub fn filter_callable_skills<'a>(&self, skills: &'a [LoadedSkill]) -> Vec<&'a LoadedSkill> {
284 skills
285 .iter()
286 .filter(|skill| {
287 self.has_skill_hint(&skill.name)
288 || skill
289 .tool_definitions
290 .iter()
291 .any(|td| self.has_tool(&td.function.name))
292 })
293 .collect()
294 }
295}
296
297#[derive(Debug)]
308pub struct ExtensionRegistry<'a> {
309 tool_definitions: Vec<ToolDefinition>,
311 tools_by_name: HashMap<String, RegisteredTool>,
313 availability: ToolAvailabilityView,
315 policy: CapabilityPolicy,
317 pub enable_memory: bool,
319 pub enable_memory_vector: bool,
321 pub skills: &'a [LoadedSkill],
323}
324
325#[derive(Debug)]
327pub struct ExtensionRegistryBuilder<'a> {
328 registered_tools: Vec<RegisteredTool>,
329 policy: CapabilityPolicy,
330 enable_memory: bool,
331 enable_memory_vector: bool,
332 enable_task_planning: bool,
333 skills: &'a [LoadedSkill],
334}
335
336impl<'a> ExtensionRegistryBuilder<'a> {
337 pub fn new(enable_memory: bool, enable_memory_vector: bool, skills: &'a [LoadedSkill]) -> Self {
339 Self {
340 registered_tools: Vec::new(),
341 policy: CapabilityPolicy::default(),
342 enable_memory,
343 enable_memory_vector,
344 enable_task_planning: true, skills,
346 }
347 }
348
349 #[must_use]
351 pub fn with_task_planning(mut self, enable: bool) -> Self {
352 self.enable_task_planning = enable;
353 self
354 }
355
356 #[must_use]
358 pub fn with_policy(mut self, policy: CapabilityPolicy) -> Self {
359 self.policy = policy;
360 self
361 }
362
363 #[must_use]
365 pub fn register(mut self, tools: impl IntoIterator<Item = RegisteredTool>) -> Self {
366 self.registered_tools.extend(tools);
367 self
368 }
369
370 #[must_use]
372 pub fn register_memory_if(mut self, enable: bool) -> Self {
373 if enable {
374 self.registered_tools.extend(memory::get_memory_tools());
375 }
376 self
377 }
378
379 pub fn build(self) -> ExtensionRegistry<'a> {
382 let mut registered_tools = self.registered_tools;
383 for skill in self.skills {
384 for td in &skill.tool_definitions {
385 registered_tools.push(RegisteredTool::new(
386 td.clone(),
387 vec![ToolCapability::SkillExecution],
388 ToolHandler::Skill {
389 skill_name: skill.name.clone(),
390 },
391 ));
392 }
393 }
394
395 let mut tool_definitions = Vec::new();
396 let mut tools_by_name = HashMap::new();
397 let mut availability = ToolAvailabilityView::default();
398 for registered in registered_tools {
399 if registered.scope == ToolScope::PlanningOnly && !self.enable_task_planning {
400 tracing::debug!(
401 "Skip PlanningOnly tool (task planning disabled): {}",
402 registered.name()
403 );
404 continue;
405 }
406 if !self.policy.allows(®istered.capabilities) {
407 tracing::debug!("Skip tool due to capability policy: {}", registered.name());
408 continue;
409 }
410 let tool_name = registered.name().to_string();
411 if tools_by_name.contains_key(&tool_name) {
412 tracing::debug!("Skip duplicate tool name: {}", tool_name);
413 continue;
414 }
415 tool_definitions.push(registered.definition.clone());
416 availability.register(®istered);
417 tools_by_name.insert(tool_name, registered);
418 }
419
420 ExtensionRegistry {
421 tool_definitions,
422 tools_by_name,
423 availability,
424 policy: self.policy,
425 enable_memory: self.enable_memory,
426 enable_memory_vector: self.enable_memory_vector,
427 skills: self.skills,
428 }
429 }
430}
431
432impl<'a> ExtensionRegistry<'a> {
433 pub fn new(enable_memory: bool, enable_memory_vector: bool, skills: &'a [LoadedSkill]) -> Self {
435 Self::builder(enable_memory, enable_memory_vector, skills)
436 .with_policy(CapabilityPolicy::full_access())
437 .register(builtin::get_builtin_tools())
438 .register_memory_if(enable_memory)
439 .build()
440 }
441
442 pub fn with_task_planning(
445 enable_memory: bool,
446 enable_memory_vector: bool,
447 enable_task_planning: bool,
448 skills: &'a [LoadedSkill],
449 ) -> Self {
450 Self::builder(enable_memory, enable_memory_vector, skills)
451 .with_task_planning(enable_task_planning)
452 .with_policy(CapabilityPolicy::full_access())
453 .register(builtin::get_builtin_tools())
454 .register_memory_if(enable_memory)
455 .build()
456 }
457
458 pub fn read_only(
460 enable_memory: bool,
461 enable_memory_vector: bool,
462 skills: &'a [LoadedSkill],
463 ) -> Self {
464 Self::builder(enable_memory, enable_memory_vector, skills)
465 .with_policy(CapabilityPolicy::read_only())
466 .register(builtin::get_builtin_tools())
467 .register_memory_if(enable_memory)
468 .build()
469 }
470
471 pub fn read_only_with_task_planning(
473 enable_memory: bool,
474 enable_memory_vector: bool,
475 enable_task_planning: bool,
476 skills: &'a [LoadedSkill],
477 ) -> Self {
478 Self::builder(enable_memory, enable_memory_vector, skills)
479 .with_task_planning(enable_task_planning)
480 .with_policy(CapabilityPolicy::read_only())
481 .register(builtin::get_builtin_tools())
482 .register_memory_if(enable_memory)
483 .build()
484 }
485
486 pub fn builder(
488 enable_memory: bool,
489 enable_memory_vector: bool,
490 skills: &'a [LoadedSkill],
491 ) -> ExtensionRegistryBuilder<'a> {
492 ExtensionRegistryBuilder::new(enable_memory, enable_memory_vector, skills)
493 }
494
495 pub fn all_tool_definitions(&self) -> Vec<ToolDefinition> {
497 self.tool_definitions.clone()
498 }
499
500 pub fn availability(&self) -> &ToolAvailabilityView {
502 &self.availability
503 }
504
505 pub fn owns_tool(&self, name: &str) -> bool {
507 self.tools_by_name.contains_key(name)
508 }
509
510 pub async fn execute(
514 &self,
515 tool_name: &str,
516 arguments: &str,
517 workspace: &Path,
518 event_sink: &mut dyn EventSink,
519 embed_ctx: Option<&MemoryVectorContext<'_>>,
520 planning_ctx: Option<&mut dyn PlanningControlExecutor>,
521 ) -> ToolResult {
522 let Some(registered) = self.tools_by_name.get(tool_name) else {
523 return ToolResult {
524 tool_call_id: String::new(),
525 tool_name: tool_name.to_string(),
526 content: format!(
527 "Tool '{}' is unavailable in the current execution mode",
528 tool_name
529 ),
530 is_error: true,
531 counts_as_failure: true,
532 };
533 };
534
535 if !self.policy.allows(®istered.capabilities) {
536 return ToolResult {
537 tool_call_id: String::new(),
538 tool_name: tool_name.to_string(),
539 content: format!(
540 "Tool '{}' is unavailable in the current execution mode",
541 tool_name
542 ),
543 is_error: true,
544 counts_as_failure: true,
545 };
546 }
547
548 match ®istered.handler {
549 ToolHandler::PlanningControl => {
550 if let Some(ctx) = planning_ctx {
551 ctx.execute(tool_name, arguments, event_sink)
552 } else {
553 ToolResult {
554 tool_call_id: String::new(),
555 tool_name: tool_name.to_string(),
556 content: format!(
557 "Tool '{}' requires task-planning mode and must be executed by the agent loop",
558 tool_name
559 ),
560 is_error: true,
561 counts_as_failure: true,
562 }
563 }
564 }
565 ToolHandler::BuiltinSync => {
566 builtin::execute_builtin_tool(tool_name, arguments, workspace, Some(event_sink))
567 }
568 ToolHandler::BuiltinAsync => {
569 builtin::execute_async_builtin_tool(tool_name, arguments, workspace, event_sink)
570 .await
571 }
572 ToolHandler::Memory => {
573 memory::execute_memory_tool(
574 tool_name,
575 arguments,
576 workspace,
577 "default",
578 self.enable_memory_vector,
579 embed_ctx,
580 )
581 .await
582 }
583 ToolHandler::Skill { skill_name } => {
584 if let Some(skill) = skills::find_skill_by_name(self.skills, skill_name) {
585 skills::execute_skill(skill, tool_name, arguments, workspace, event_sink, None)
586 } else if let Some(skill) = skills::find_skill_by_tool_name(self.skills, tool_name)
587 {
588 skills::execute_skill(skill, tool_name, arguments, workspace, event_sink, None)
589 } else if let Some(skill) = skills::find_skill_by_name(self.skills, tool_name) {
590 let docs = prompt::get_skill_full_docs(skill).unwrap_or_else(|| {
592 format!(
593 "Skill '{}' is reference-only (no executable entry point). Use its guidance to generate content yourself using write_output.",
594 skill.name
595 )
596 });
597 ToolResult {
598 tool_call_id: String::new(),
599 tool_name: tool_name.to_string(),
600 content: format!(
601 "Note: '{}' is a reference-only skill (no executable script). Its documentation is provided below — use these guidelines to generate the content yourself, then save with write_output and preview with preview_server.\n\n{}",
602 skill.name, docs
603 ),
604 is_error: false,
605 counts_as_failure: false,
606 }
607 } else {
608 ToolResult {
609 tool_call_id: String::new(),
610 tool_name: tool_name.to_string(),
611 content: format!("Unknown skill tool: {}", tool_name),
612 is_error: true,
613 counts_as_failure: true,
614 }
615 }
616 }
617 }
618 }
619}