skilllite 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
skilllite/core/loops.py CHANGED
@@ -56,11 +56,12 @@ class AgenticLoop:
56
56
  custom_tool_handler: Optional[Callable] = None,
57
57
  enable_task_planning: bool = True,
58
58
  verbose: bool = True,
59
+ confirmation_callback: Optional[Callable[[str, str], bool]] = None,
59
60
  **kwargs
60
61
  ):
61
62
  """
62
63
  Initialize the agentic loop.
63
-
64
+
64
65
  Args:
65
66
  manager: SkillManager instance
66
67
  client: LLM client (OpenAI or Anthropic)
@@ -71,6 +72,9 @@ class AgenticLoop:
71
72
  custom_tool_handler: Optional custom tool handler function
72
73
  enable_task_planning: Whether to generate task list before execution
73
74
  verbose: Whether to print detailed logs
75
+ confirmation_callback: Callback for security confirmation (sandbox_level=3).
76
+ Signature: (security_report: str, scan_id: str) -> bool
77
+ If None and sandbox_level=3, will use interactive terminal confirmation.
74
78
  **kwargs: Additional arguments passed to the LLM
75
79
  """
76
80
  self.manager = manager
@@ -82,14 +86,157 @@ class AgenticLoop:
82
86
  self.custom_tool_handler = custom_tool_handler
83
87
  self.enable_task_planning = enable_task_planning
84
88
  self.verbose = verbose
89
+ self.confirmation_callback = confirmation_callback
85
90
  self.extra_kwargs = kwargs
86
91
  self.task_list: List[Dict] = []
87
-
92
+
93
+ # Initialize security scanner for sandbox_level=3
94
+ self._security_scanner = None
95
+ self._pending_confirmation = False # Track if confirmation is pending
96
+
88
97
  def _log(self, message: str) -> None:
89
98
  """Print log message if verbose mode is enabled."""
90
99
  if self.verbose:
91
100
  print(message)
92
-
101
+
102
+ def _get_security_scanner(self):
103
+ """Get or lazily initialize the security scanner."""
104
+ if self._security_scanner is None:
105
+ from .security import SecurityScanner
106
+ self._security_scanner = SecurityScanner()
107
+ return self._security_scanner
108
+
109
+ def _should_perform_security_scan(self) -> bool:
110
+ """Check if security scanning should be performed."""
111
+ import os
112
+ sandbox_level = os.environ.get("SKILLBOX_SANDBOX_LEVEL", "3")
113
+ return sandbox_level == "3" and self.confirmation_callback is not None
114
+
115
+ def _interactive_confirmation(self, report: str, scan_id: str) -> bool:
116
+ """Default interactive terminal confirmation."""
117
+ self._log(f"\n{report}")
118
+ self._log("\n" + "=" * 60)
119
+ while True:
120
+ response = input("⚠️ Allow execution? (y/n): ").strip().lower()
121
+ if response in ['y', 'yes']:
122
+ return True
123
+ elif response in ['n', 'no']:
124
+ return False
125
+ self._log("Please enter 'y' or 'n'")
126
+
127
+ def _perform_security_confirmation_for_tools(
128
+ self,
129
+ tool_calls: List[Any]
130
+ ) -> bool:
131
+ """
132
+ Perform security scan and confirmation for tool calls.
133
+
134
+ Returns True if execution should proceed, False if denied.
135
+ Also handles skills that require elevated permissions.
136
+ """
137
+ import os
138
+ from .security import SecurityScanResult
139
+
140
+ sandbox_level = os.environ.get("SKILLBOX_SANDBOX_LEVEL", "3")
141
+ if sandbox_level != "3":
142
+ return True # No confirmation needed for levels 1-2
143
+
144
+ # Get skill tool names
145
+ skill_tool_names = set(self.manager.skill_names())
146
+ skill_tool_names.update(self.manager._registry.list_multi_script_tools())
147
+
148
+ # Scan each skill tool call
149
+ combined_issues = []
150
+ scanned_skills = set()
151
+ requires_elevated = False # Track if any skill requires elevated permissions
152
+
153
+ for tc in tool_calls:
154
+ tool_name = tc.function.name if hasattr(tc, 'function') else tc.get('name', '')
155
+
156
+ # Only scan skill tools, not custom tools
157
+ if tool_name not in skill_tool_names:
158
+ continue
159
+
160
+ # Get skill info
161
+ skill_name = tool_name.split('__')[0] if '__' in tool_name else tool_name
162
+ if skill_name in scanned_skills:
163
+ continue
164
+ scanned_skills.add(skill_name)
165
+
166
+ skill_info = self.manager.get_skill(skill_name)
167
+ if not skill_info:
168
+ continue
169
+
170
+ # Check if skill requires elevated permissions
171
+ if skill_info.metadata and getattr(skill_info.metadata, 'requires_elevated_permissions', False):
172
+ requires_elevated = True
173
+ self._log(f"🔓 Skill '{skill_name}' requires elevated permissions")
174
+
175
+ # Parse input data
176
+ try:
177
+ import json
178
+ input_data = json.loads(tc.function.arguments) if hasattr(tc, 'function') else {}
179
+ except (json.JSONDecodeError, AttributeError):
180
+ input_data = {}
181
+
182
+ # Perform security scan
183
+ scanner = self._get_security_scanner()
184
+ result = scanner.scan_skill(skill_info, input_data)
185
+ combined_issues.extend(result.issues)
186
+
187
+ # If skill requires elevated permissions, downgrade sandbox level
188
+ if requires_elevated and not combined_issues:
189
+ self._log("✅ Skill requires elevated permissions, downgrading to sandbox level 1...")
190
+ os.environ["SKILLBOX_SANDBOX_LEVEL"] = "1"
191
+ self._pending_confirmation = True
192
+ return True
193
+
194
+ if not combined_issues:
195
+ return True # No issues found
196
+
197
+ # Create combined scan result
198
+ high_count = sum(1 for i in combined_issues if i.get("severity") in ["Critical", "High"])
199
+ medium_count = sum(1 for i in combined_issues if i.get("severity") == "Medium")
200
+ low_count = sum(1 for i in combined_issues if i.get("severity") == "Low")
201
+
202
+ combined_result = SecurityScanResult(
203
+ is_safe=high_count == 0,
204
+ issues=combined_issues,
205
+ scan_id=f"batch-{len(scanned_skills)}",
206
+ high_severity_count=high_count,
207
+ medium_severity_count=medium_count,
208
+ low_severity_count=low_count,
209
+ )
210
+
211
+ if not combined_result.requires_confirmation:
212
+ return True # Only low/medium issues, proceed
213
+
214
+ # Ask for confirmation
215
+ report = combined_result.format_report()
216
+ self._log(f"\n🔒 Security scan detected potential issues:")
217
+
218
+ if self.confirmation_callback:
219
+ confirmed = self.confirmation_callback(report, combined_result.scan_id)
220
+ else:
221
+ confirmed = self._interactive_confirmation(report, combined_result.scan_id)
222
+
223
+ if confirmed:
224
+ # Temporarily downgrade sandbox level to allow execution
225
+ self._log("✅ User confirmed. Executing with sandbox level 1...")
226
+ os.environ["SKILLBOX_SANDBOX_LEVEL"] = "1"
227
+ self._pending_confirmation = True
228
+ return True
229
+ else:
230
+ self._log("❌ User denied execution.")
231
+ return False
232
+
233
+ def _restore_sandbox_level(self, original_level: str) -> None:
234
+ """Restore original sandbox level after execution."""
235
+ import os
236
+ if self._pending_confirmation:
237
+ os.environ["SKILLBOX_SANDBOX_LEVEL"] = original_level
238
+ self._pending_confirmation = False
239
+
93
240
  def _get_execution_system_prompt(self) -> str:
94
241
  """
95
242
  Generate the main execution system prompt for skill selection and file operations.
@@ -573,17 +720,25 @@ Based on the documentation, call the tools with correct parameters.
573
720
  continue
574
721
 
575
722
  messages.append(message)
576
-
723
+
724
+ # Execute tools using unified execution service
577
725
  self._log(f"\n⚙️ Executing tools...")
726
+
578
727
  if self.custom_tool_handler:
728
+ # Custom tool handler takes precedence
579
729
  tool_results = self.custom_tool_handler(
580
730
  response, self.manager, allow_network, timeout
581
731
  )
582
732
  else:
583
- tool_results = self.manager.handle_tool_calls(
584
- response, allow_network=allow_network, timeout=timeout
733
+ # Use unified execution service with confirmation callback
734
+ # This handles security scanning, confirmation, and sandbox level management
735
+ tool_results = self.manager.handle_tool_calls_with_unified_service(
736
+ response,
737
+ confirmation_callback=self.confirmation_callback or self._interactive_confirmation,
738
+ allow_network=allow_network,
739
+ timeout=timeout
585
740
  )
586
-
741
+
587
742
  self._log(f"\n📊 Tool execution results:")
588
743
  for idx, (result, tc) in enumerate(zip(tool_results, message.tool_calls), 1):
589
744
  output = result.content
@@ -591,7 +746,7 @@ Based on the documentation, call the tools with correct parameters.
591
746
  output = output[:500] + "... (truncated)"
592
747
  self._log(f" {idx}. {tc.function.name}")
593
748
  self._log(f" Result: {output}")
594
-
749
+
595
750
  for result in tool_results:
596
751
  messages.append(result.to_openai_format())
597
752
 
@@ -693,19 +848,26 @@ Based on the documentation, call the tools with correct parameters.
693
848
  self._log(f" Arguments: {json.dumps(block.input, ensure_ascii=False)}")
694
849
 
695
850
  messages.append({"role": "assistant", "content": response.content})
696
-
851
+
852
+ # Execute tools using unified execution service
697
853
  self._log(f"\n⚙️ Executing tools...")
698
- tool_results = self.manager.handle_tool_calls_claude_native(
699
- response, allow_network=allow_network, timeout=timeout
854
+
855
+ # Use unified execution service with confirmation callback
856
+ # This handles security scanning, confirmation, and sandbox level management
857
+ tool_results = self.manager.handle_tool_calls_claude_native_with_unified_service(
858
+ response,
859
+ confirmation_callback=self.confirmation_callback or self._interactive_confirmation,
860
+ allow_network=allow_network,
861
+ timeout=timeout
700
862
  )
701
-
863
+
702
864
  self._log(f"\n📊 Tool execution results:")
703
865
  for idx, result in enumerate(tool_results, 1):
704
866
  output = result.content
705
867
  if len(output) > 500:
706
868
  output = output[:500] + "... (truncated)"
707
869
  self._log(f" {idx}. Result: {output}")
708
-
870
+
709
871
  formatted_results = self.manager.format_tool_results_claude_native(tool_results)
710
872
  messages.append({"role": "user", "content": formatted_results})
711
873
 
skilllite/core/manager.py CHANGED
@@ -304,7 +304,64 @@ class SkillManager:
304
304
  ) -> List[ToolResult]:
305
305
  """Parse and execute all tool calls from Claude's native API response."""
306
306
  return self._handler.handle_tool_calls_claude_native(response, allow_network, timeout)
307
-
307
+
308
+ def handle_tool_calls_with_unified_service(
309
+ self,
310
+ response: Any,
311
+ confirmation_callback: Optional[Callable[[str, str], bool]] = None,
312
+ allow_network: Optional[bool] = None,
313
+ timeout: Optional[int] = None
314
+ ) -> List[ToolResult]:
315
+ """
316
+ Parse and execute all tool calls using UnifiedExecutionService.
317
+
318
+ This method uses the unified execution layer which:
319
+ 1. Reads sandbox level at runtime
320
+ 2. Handles security scanning and confirmation per-skill
321
+ 3. Properly downgrades sandbox level after confirmation
322
+
323
+ Args:
324
+ response: Response from OpenAI-compatible API
325
+ confirmation_callback: Callback for security confirmation
326
+ allow_network: Whether to allow network access
327
+ timeout: Execution timeout in seconds
328
+
329
+ Returns:
330
+ List of ToolResult objects
331
+ """
332
+ return self._handler.handle_tool_calls_with_unified_service(
333
+ response,
334
+ confirmation_callback=confirmation_callback,
335
+ allow_network=allow_network,
336
+ timeout=timeout
337
+ )
338
+
339
+ def handle_tool_calls_claude_native_with_unified_service(
340
+ self,
341
+ response: Any,
342
+ confirmation_callback: Optional[Callable[[str, str], bool]] = None,
343
+ allow_network: Optional[bool] = None,
344
+ timeout: Optional[int] = None
345
+ ) -> List[ToolResult]:
346
+ """
347
+ Parse and execute all Claude tool calls using UnifiedExecutionService.
348
+
349
+ Args:
350
+ response: Response from Claude's native API
351
+ confirmation_callback: Callback for security confirmation
352
+ allow_network: Whether to allow network access
353
+ timeout: Execution timeout in seconds
354
+
355
+ Returns:
356
+ List of ToolResult objects
357
+ """
358
+ return self._handler.handle_tool_calls_claude_native_with_unified_service(
359
+ response,
360
+ confirmation_callback=confirmation_callback,
361
+ allow_network=allow_network,
362
+ timeout=timeout
363
+ )
364
+
308
365
  def format_tool_results_claude_native(self, results: List[ToolResult]) -> List[Dict[str, Any]]:
309
366
  """Format tool results for Claude's native API."""
310
367
  return self._handler.format_tool_results_claude_native(results)
@@ -335,6 +392,7 @@ class SkillManager:
335
392
  custom_tool_handler: Optional[Callable] = None,
336
393
  enable_task_planning: bool = True,
337
394
  verbose: bool = True,
395
+ confirmation_callback: Optional[Callable[[str, str], bool]] = None,
338
396
  **kwargs
339
397
  ) -> AgenticLoop:
340
398
  """
@@ -351,15 +409,16 @@ class SkillManager:
351
409
  custom_tool_handler: Optional custom tool handler
352
410
  enable_task_planning: Whether to generate task list before execution
353
411
  verbose: Whether to print detailed logs
412
+ confirmation_callback: Callback for security confirmation (sandbox_level=3)
354
413
  **kwargs: Additional arguments passed to the LLM
355
-
414
+
356
415
  Returns:
357
416
  AgenticLoop instance
358
-
417
+
359
418
  Example:
360
419
  # OpenAI-compatible (default)
361
420
  loop = manager.create_agentic_loop(client, "gpt-4")
362
-
421
+
363
422
  # Claude native API
364
423
  loop = manager.create_agentic_loop(client, "claude-3-opus",
365
424
  api_format="claude_native")
@@ -375,6 +434,7 @@ class SkillManager:
375
434
  custom_tool_handler=custom_tool_handler,
376
435
  enable_task_planning=enable_task_planning,
377
436
  verbose=verbose,
437
+ confirmation_callback=confirmation_callback,
378
438
  **kwargs
379
439
  )
380
440
 
@@ -421,14 +481,15 @@ class SkillManager:
421
481
  custom_tool_executor: Optional[Callable] = None,
422
482
  enable_task_planning: bool = True,
423
483
  verbose: bool = True,
484
+ confirmation_callback: Optional[Callable[[str, str], bool]] = None,
424
485
  **kwargs
425
486
  ) -> AgenticLoop:
426
487
  """
427
488
  Create an enhanced agentic loop with custom tools support.
428
-
489
+
429
490
  This method creates an AgenticLoop that can handle both skill tools
430
491
  and custom tools (like file operations).
431
-
492
+
432
493
  Args:
433
494
  client: LLM client (OpenAI-compatible)
434
495
  model: Model name to use
@@ -438,27 +499,32 @@ class SkillManager:
438
499
  custom_tool_executor: Executor function for custom tools
439
500
  enable_task_planning: Whether to generate task list before execution
440
501
  verbose: Whether to print detailed logs
502
+ confirmation_callback: Callback for security confirmation (sandbox_level=3)
441
503
  **kwargs: Additional arguments passed to the LLM
442
-
504
+
443
505
  Returns:
444
506
  AgenticLoop instance with enhanced capabilities
445
507
  """
446
508
  # Create custom tool handler that combines skill tools and custom tools
447
509
  def combined_tool_handler(response, manager, allow_network, timeout):
448
510
  from .tools import ToolUseRequest, ToolResult
449
-
511
+
450
512
  requests = ToolUseRequest.parse_from_openai_response(response)
451
513
  results = []
452
-
514
+
453
515
  # Get skill tool names
454
516
  skill_tool_names = set(self.skill_names())
455
517
  skill_tool_names.update(self._registry.list_multi_script_tools())
456
-
518
+
457
519
  for request in requests:
458
520
  if request.name in skill_tool_names:
459
- # Execute as skill tool
460
- result = self._handler.execute_tool_call(
461
- request, allow_network=allow_network, timeout=timeout
521
+ # Execute as skill tool using UnifiedExecutionService
522
+ # This handles security scanning, confirmation, and proper sandbox level
523
+ result = self._handler.execute_tool_call_with_unified_service(
524
+ request,
525
+ confirmation_callback=confirmation_callback,
526
+ allow_network=allow_network,
527
+ timeout=timeout
462
528
  )
463
529
  results.append(result)
464
530
  elif custom_tool_executor:
@@ -473,9 +539,9 @@ class SkillManager:
473
539
  results.append(ToolResult.error(
474
540
  request.id, f"No executor found for tool: {request.name}"
475
541
  ))
476
-
542
+
477
543
  return results
478
-
544
+
479
545
  return AgenticLoop(
480
546
  manager=self,
481
547
  client=client,
@@ -486,6 +552,7 @@ class SkillManager:
486
552
  custom_tool_handler=combined_tool_handler if custom_tool_executor else None,
487
553
  enable_task_planning=enable_task_planning,
488
554
  verbose=verbose,
555
+ confirmation_callback=confirmation_callback,
489
556
  **kwargs
490
557
  )
491
558
 
@@ -35,31 +35,37 @@ class SkillMetadata:
35
35
  network: NetworkPolicy = field(default_factory=NetworkPolicy)
36
36
  input_schema: Optional[Dict[str, Any]] = None
37
37
  output_schema: Optional[Dict[str, Any]] = None
38
-
38
+ requires_elevated_permissions: bool = False # For skills that need to write outside their directory
39
+
39
40
  @classmethod
40
41
  def from_dict(cls, data: Dict[str, Any], skill_dir: Optional[Path] = None) -> "SkillMetadata":
41
42
  """Create SkillMetadata from parsed YAML front matter."""
42
43
  version = data.get("version")
43
44
  if not version and "metadata" in data:
44
45
  version = data["metadata"].get("version")
45
-
46
+
46
47
  compatibility = data.get("compatibility")
47
-
48
+
48
49
  # Parse network policy from compatibility field
49
50
  network = parse_compatibility_for_network(compatibility)
50
-
51
+
51
52
  # Auto-detect entry point
52
53
  entry_point = ""
53
54
  if skill_dir:
54
55
  detected = detect_entry_point(skill_dir)
55
56
  if detected:
56
57
  entry_point = detected
57
-
58
+
58
59
  # Detect language from compatibility or entry point
59
60
  language = parse_compatibility_for_language(compatibility)
60
61
  if not language and entry_point:
61
62
  language = detect_language_from_entry_point(entry_point)
62
-
63
+
64
+ # Parse requires_elevated_permissions flag
65
+ requires_elevated = data.get("requires_elevated_permissions", False)
66
+ if isinstance(requires_elevated, str):
67
+ requires_elevated = requires_elevated.lower() in ("true", "yes", "1")
68
+
63
69
  return cls(
64
70
  name=data.get("name", ""),
65
71
  entry_point=entry_point,
@@ -69,7 +75,8 @@ class SkillMetadata:
69
75
  compatibility=compatibility,
70
76
  network=network,
71
77
  input_schema=data.get("input_schema"),
72
- output_schema=data.get("output_schema")
78
+ output_schema=data.get("output_schema"),
79
+ requires_elevated_permissions=bool(requires_elevated)
73
80
  )
74
81
 
75
82