langchain-skilllite 0.1.0__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.
@@ -0,0 +1,54 @@
1
+ """
2
+ LangChain integration for SkillLite.
3
+
4
+ This package provides LangChain-compatible tools for executing SkillLite skills
5
+ in a sandboxed environment. It acts as a thin adapter layer on top of the
6
+ skilllite core package.
7
+
8
+ Key Features:
9
+ - SkillLiteTool: LangChain BaseTool adapter for individual skills
10
+ - SkillLiteToolkit: Convenient toolkit for loading multiple skills
11
+ - Security scanning and confirmation callbacks for sandbox level 3
12
+ - Full async support for LangGraph agents
13
+
14
+ Installation:
15
+ pip install langchain-skilllite
16
+
17
+ Quick Start:
18
+ ```python
19
+ from langchain_skilllite import SkillLiteToolkit
20
+ from langchain_openai import ChatOpenAI
21
+ from langgraph.prebuilt import create_react_agent
22
+
23
+ # Load skills as LangChain tools
24
+ tools = SkillLiteToolkit.from_directory("./skills")
25
+
26
+ # Use with any LangChain agent
27
+ agent = create_react_agent(ChatOpenAI(), tools)
28
+ result = agent.invoke({"messages": [("user", "Run my skill")]})
29
+ ```
30
+
31
+ For more information, see:
32
+ - SkillLite: https://github.com/EXboys/skilllite
33
+ - LangChain: https://python.langchain.com/
34
+ """
35
+
36
+ from langchain_skilllite.tools import (
37
+ SkillLiteTool,
38
+ SkillLiteToolkit,
39
+ )
40
+ from langchain_skilllite.callbacks import (
41
+ SkillLiteCallbackHandler,
42
+ )
43
+ from langchain_skilllite._version import __version__
44
+
45
+ __all__ = [
46
+ # Core Tools
47
+ "SkillLiteTool",
48
+ "SkillLiteToolkit",
49
+ # Callbacks
50
+ "SkillLiteCallbackHandler",
51
+ # Version
52
+ "__version__",
53
+ ]
54
+
@@ -0,0 +1,4 @@
1
+ """Version information for langchain-skilllite."""
2
+
3
+ __version__ = "0.1.0"
4
+
@@ -0,0 +1,166 @@
1
+ """
2
+ LangChain callback handlers for SkillLite.
3
+
4
+ This module provides callback handlers for integrating SkillLite
5
+ with LangChain's callback system for logging, tracing, and monitoring.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
12
+ from uuid import UUID
13
+
14
+ from langchain_core.callbacks import BaseCallbackHandler
15
+ from langchain_core.outputs import LLMResult
16
+
17
+ if TYPE_CHECKING:
18
+ from langchain_core.agents import AgentAction, AgentFinish
19
+ from langchain_core.messages import BaseMessage
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class SkillLiteCallbackHandler(BaseCallbackHandler):
25
+ """
26
+ LangChain callback handler for SkillLite skill execution.
27
+
28
+ This handler logs skill execution events and can be used for
29
+ monitoring, debugging, and auditing SkillLite tool usage.
30
+
31
+ Usage:
32
+ from langchain_skilllite import SkillLiteCallbackHandler
33
+
34
+ handler = SkillLiteCallbackHandler(verbose=True)
35
+
36
+ # Use with LangChain agent
37
+ agent.invoke({"input": "..."}, config={"callbacks": [handler]})
38
+
39
+ Attributes:
40
+ verbose: Whether to print execution details
41
+ execution_log: List of execution events
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ verbose: bool = False,
47
+ log_level: int = logging.INFO,
48
+ ):
49
+ """
50
+ Initialize the callback handler.
51
+
52
+ Args:
53
+ verbose: If True, print execution details to stdout
54
+ log_level: Logging level for internal logging
55
+ """
56
+ super().__init__()
57
+ self.verbose = verbose
58
+ self.log_level = log_level
59
+ self.execution_log: List[Dict[str, Any]] = []
60
+ self._current_tool: Optional[str] = None
61
+
62
+ def on_tool_start(
63
+ self,
64
+ serialized: Dict[str, Any],
65
+ input_str: str,
66
+ *,
67
+ run_id: UUID,
68
+ parent_run_id: Optional[UUID] = None,
69
+ tags: Optional[List[str]] = None,
70
+ metadata: Optional[Dict[str, Any]] = None,
71
+ inputs: Optional[Dict[str, Any]] = None,
72
+ **kwargs: Any,
73
+ ) -> None:
74
+ """Called when a tool starts running."""
75
+ tool_name = serialized.get("name", "unknown")
76
+ self._current_tool = tool_name
77
+
78
+ event = {
79
+ "event": "tool_start",
80
+ "tool_name": tool_name,
81
+ "run_id": str(run_id),
82
+ "input": input_str[:200] if input_str else None,
83
+ }
84
+ self.execution_log.append(event)
85
+
86
+ if self.verbose:
87
+ print(f"🔧 [SkillLite] Starting tool: {tool_name}")
88
+ logger.log(self.log_level, f"Tool started: {tool_name}")
89
+
90
+ def on_tool_end(
91
+ self,
92
+ output: Any,
93
+ *,
94
+ run_id: UUID,
95
+ parent_run_id: Optional[UUID] = None,
96
+ **kwargs: Any,
97
+ ) -> None:
98
+ """Called when a tool finishes."""
99
+ # Handle different output types (str, ToolMessage, etc.)
100
+ if isinstance(output, str):
101
+ output_preview = output[:200] if output else None
102
+ elif hasattr(output, "content"):
103
+ # ToolMessage or similar object
104
+ content = output.content
105
+ output_preview = content[:200] if isinstance(content, str) and content else str(content)[:200]
106
+ else:
107
+ output_preview = str(output)[:200] if output else None
108
+
109
+ event = {
110
+ "event": "tool_end",
111
+ "tool_name": self._current_tool,
112
+ "run_id": str(run_id),
113
+ "output_preview": output_preview,
114
+ "success": True,
115
+ }
116
+ self.execution_log.append(event)
117
+
118
+ if self.verbose:
119
+ print(f"✅ [SkillLite] Tool completed: {self._current_tool}")
120
+ logger.log(self.log_level, f"Tool completed: {self._current_tool}")
121
+
122
+ self._current_tool = None
123
+
124
+ def on_tool_error(
125
+ self,
126
+ error: BaseException,
127
+ *,
128
+ run_id: UUID,
129
+ parent_run_id: Optional[UUID] = None,
130
+ **kwargs: Any,
131
+ ) -> None:
132
+ """Called when a tool errors."""
133
+ event = {
134
+ "event": "tool_error",
135
+ "tool_name": self._current_tool,
136
+ "run_id": str(run_id),
137
+ "error": str(error),
138
+ "success": False,
139
+ }
140
+ self.execution_log.append(event)
141
+
142
+ if self.verbose:
143
+ print(f"❌ [SkillLite] Tool error: {self._current_tool} - {error}")
144
+ logger.error(f"Tool error: {self._current_tool} - {error}")
145
+
146
+ self._current_tool = None
147
+
148
+ def get_execution_summary(self) -> Dict[str, Any]:
149
+ """Get a summary of all execution events."""
150
+ total = len(self.execution_log)
151
+ tool_starts = sum(1 for e in self.execution_log if e["event"] == "tool_start")
152
+ tool_ends = sum(1 for e in self.execution_log if e["event"] == "tool_end")
153
+ tool_errors = sum(1 for e in self.execution_log if e["event"] == "tool_error")
154
+
155
+ return {
156
+ "total_events": total,
157
+ "tool_executions": tool_starts,
158
+ "successful": tool_ends,
159
+ "errors": tool_errors,
160
+ "success_rate": tool_ends / tool_starts if tool_starts > 0 else 0,
161
+ }
162
+
163
+ def clear_log(self) -> None:
164
+ """Clear the execution log."""
165
+ self.execution_log.clear()
166
+
@@ -0,0 +1,526 @@
1
+ """
2
+ LangChain tools for SkillLite skill execution.
3
+
4
+ This module provides LangChain-compatible tool wrappers for SkillLite skills.
5
+ All execution logic is delegated to the skilllite core package.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import hashlib
12
+ import os
13
+ import time
14
+ import uuid
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Callable, Dict, List, Optional, Type, TYPE_CHECKING
17
+
18
+ from langchain_core.tools import BaseTool
19
+ from langchain_core.callbacks import (
20
+ CallbackManagerForToolRun,
21
+ AsyncCallbackManagerForToolRun,
22
+ )
23
+ from pydantic import BaseModel, Field, ConfigDict
24
+
25
+ # Import from skilllite core - this is the key dependency
26
+ from skilllite import SkillManager, SkillInfo
27
+
28
+ if TYPE_CHECKING:
29
+ from pathlib import Path
30
+
31
+ # Type aliases for confirmation callbacks
32
+ ConfirmationCallback = Callable[[str, str], bool]
33
+ AsyncConfirmationCallback = Callable[[str, str], "asyncio.Future[bool]"]
34
+
35
+
36
+ @dataclass
37
+ class SecurityScanResult:
38
+ """Result of a security scan for skill execution."""
39
+
40
+ is_safe: bool
41
+ issues: List[Dict[str, Any]] = field(default_factory=list)
42
+ scan_id: str = ""
43
+ code_hash: str = ""
44
+ high_severity_count: int = 0
45
+ medium_severity_count: int = 0
46
+ low_severity_count: int = 0
47
+ timestamp: float = field(default_factory=time.time)
48
+
49
+ @property
50
+ def requires_confirmation(self) -> bool:
51
+ """Check if user confirmation is required."""
52
+ return self.high_severity_count > 0
53
+
54
+ def format_report(self) -> str:
55
+ """Format a human-readable security report."""
56
+ if not self.issues:
57
+ return "✅ Security scan passed. No issues found."
58
+
59
+ lines = [
60
+ f"📋 Security Scan Report (ID: {self.scan_id[:8]})",
61
+ f" Found {len(self.issues)} item(s) for review:",
62
+ "",
63
+ ]
64
+
65
+ severity_icons = {
66
+ "Critical": "🔴",
67
+ "High": "🟠",
68
+ "Medium": "🟡",
69
+ "Low": "🟢",
70
+ }
71
+
72
+ for idx, issue in enumerate(self.issues, 1):
73
+ severity = issue.get("severity", "Medium")
74
+ icon = severity_icons.get(severity, "⚪")
75
+ lines.append(f" {icon} #{idx} [{severity}] {issue.get('issue_type', 'Unknown')}")
76
+ lines.append(f" ├─ Rule: {issue.get('rule_id', 'N/A')}")
77
+ lines.append(f" ├─ Line {issue.get('line_number', '?')}: {issue.get('description', '')}")
78
+ snippet = issue.get('code_snippet', '')
79
+ lines.append(f" └─ Code: {snippet[:60]}{'...' if len(snippet) > 60 else ''}")
80
+ lines.append("")
81
+
82
+ if self.high_severity_count > 0:
83
+ lines.append("⚠️ High severity issues found. Confirmation required to execute.")
84
+ else:
85
+ lines.append("ℹ️ Only low/medium severity issues found. Safe to execute.")
86
+
87
+ return "\n".join(lines)
88
+
89
+
90
+ class SkillLiteTool(BaseTool):
91
+ """
92
+ LangChain BaseTool adapter for a single SkillLite skill.
93
+
94
+ This wraps a SkillLite skill as a LangChain tool, enabling it to be
95
+ used with LangChain agents and LangGraph.
96
+
97
+ All execution is delegated to the skilllite core package.
98
+
99
+ Attributes:
100
+ name: Tool name (same as skill name)
101
+ description: Tool description from SKILL.md
102
+ manager: SkillManager instance (from skilllite)
103
+ skill_name: Name of the skill to execute
104
+ sandbox_level: Sandbox security level (1/2/3)
105
+ """
106
+
107
+ name: str = Field(description="Tool name")
108
+ description: str = Field(description="Tool description")
109
+ args_schema: Optional[Type[BaseModel]] = Field(
110
+ default=None, description="Pydantic schema for arguments"
111
+ )
112
+
113
+ # SkillLite specific fields - delegated to skilllite core
114
+ manager: Any = Field(exclude=True) # SkillManager instance
115
+ skill_name: str = Field(description="SkillLite skill name")
116
+ allow_network: bool = Field(default=False, description="Allow network access")
117
+ timeout: Optional[int] = Field(default=None, description="Execution timeout in seconds")
118
+
119
+ # Security confirmation fields
120
+ sandbox_level: int = Field(default=3, description="Sandbox security level (1/2/3)")
121
+ confirmation_callback: Optional[Any] = Field(
122
+ default=None,
123
+ exclude=True,
124
+ description="Sync callback: (report: str, scan_id: str) -> bool"
125
+ )
126
+ async_confirmation_callback: Optional[Any] = Field(
127
+ default=None,
128
+ exclude=True,
129
+ description="Async callback: (report: str, scan_id: str) -> Future[bool]"
130
+ )
131
+
132
+ # Internal cache
133
+ _scan_cache: Dict[str, SecurityScanResult] = {}
134
+ _confirmed_skills: Dict[str, float] = {} # skill_name -> confirmation timestamp
135
+ _SCAN_CACHE_TTL: int = 300 # 5 minutes
136
+ _CONFIRMATION_TTL: int = 3600 # 1 hour - once confirmed, don't ask again
137
+
138
+ model_config = ConfigDict(arbitrary_types_allowed=True)
139
+
140
+ def _generate_input_hash(self, input_data: Dict[str, Any]) -> str:
141
+ """Generate a hash of the input data for verification."""
142
+ import json
143
+ content = json.dumps(input_data, sort_keys=True, ensure_ascii=False)
144
+ return hashlib.sha256(content.encode()).hexdigest()[:16]
145
+
146
+ def _cleanup_expired_scans(self) -> None:
147
+ """Remove expired scan results from cache."""
148
+ current_time = time.time()
149
+ expired_keys = [
150
+ k for k, v in self._scan_cache.items()
151
+ if current_time - v.timestamp > self._SCAN_CACHE_TTL
152
+ ]
153
+ for key in expired_keys:
154
+ del self._scan_cache[key]
155
+
156
+ def _perform_security_scan(self, input_data: Dict[str, Any]) -> SecurityScanResult:
157
+ """
158
+ Perform a security scan on the skill execution.
159
+ Delegates to skilllite's skillbox binary for actual scanning.
160
+ """
161
+ self._cleanup_expired_scans()
162
+ input_hash = self._generate_input_hash(input_data)
163
+ scan_id = str(uuid.uuid4())
164
+
165
+ try:
166
+ # Get skill info from skilllite's registry
167
+ skill_info = self.manager._registry.get_skill(self.skill_name)
168
+ entry_point = skill_info.metadata.entry_point if skill_info and skill_info.metadata else None
169
+
170
+ if skill_info and entry_point:
171
+ entry_script = skill_info.path / entry_point
172
+ if entry_script.exists():
173
+ # Use skilllite's skillbox for security scanning
174
+ from skilllite.sandbox.skillbox import find_binary
175
+ import subprocess
176
+
177
+ skillbox_path = find_binary()
178
+ if skillbox_path:
179
+ result = subprocess.run(
180
+ [skillbox_path, "security-scan", str(entry_script)],
181
+ capture_output=True,
182
+ text=True,
183
+ timeout=30
184
+ )
185
+ issues = self._parse_scan_output(result.stdout + result.stderr)
186
+ high_count = sum(1 for i in issues if i.get("severity") in ["Critical", "High"])
187
+ medium_count = sum(1 for i in issues if i.get("severity") == "Medium")
188
+ low_count = sum(1 for i in issues if i.get("severity") == "Low")
189
+
190
+ scan_result = SecurityScanResult(
191
+ is_safe=high_count == 0,
192
+ issues=issues,
193
+ scan_id=scan_id,
194
+ code_hash=input_hash,
195
+ high_severity_count=high_count,
196
+ medium_severity_count=medium_count,
197
+ low_severity_count=low_count,
198
+ )
199
+ self._scan_cache[scan_id] = scan_result
200
+ return scan_result
201
+ except Exception:
202
+ pass
203
+
204
+ # Default: no issues found
205
+ scan_result = SecurityScanResult(
206
+ is_safe=True,
207
+ issues=[],
208
+ scan_id=scan_id,
209
+ code_hash=input_hash,
210
+ )
211
+ self._scan_cache[scan_id] = scan_result
212
+ return scan_result
213
+
214
+ def _parse_scan_output(self, output: str) -> List[Dict[str, Any]]:
215
+ """Parse skillbox scan output into structured issues."""
216
+ issues = []
217
+ current_issue: Optional[Dict[str, Any]] = None
218
+
219
+ for line in output.split('\n'):
220
+ line = line.strip()
221
+ if not line:
222
+ continue
223
+
224
+ if any(sev in line for sev in ['[Critical]', '[High]', '[Medium]', '[Low]']):
225
+ if current_issue:
226
+ issues.append(current_issue)
227
+
228
+ severity = "Medium"
229
+ for sev in ['Critical', 'High', 'Medium', 'Low']:
230
+ if f'[{sev}]' in line:
231
+ severity = sev
232
+ break
233
+
234
+ current_issue = {
235
+ "severity": severity,
236
+ "issue_type": "SecurityIssue",
237
+ "description": line,
238
+ "rule_id": "unknown",
239
+ "line_number": 0,
240
+ "code_snippet": ""
241
+ }
242
+ elif current_issue:
243
+ if 'Rule:' in line:
244
+ current_issue["rule_id"] = line.split('Rule:')[-1].strip()
245
+ elif 'Line' in line:
246
+ try:
247
+ line_num = int(line.split('Line')[-1].split(':')[0].strip())
248
+ current_issue["line_number"] = line_num
249
+ except ValueError:
250
+ pass
251
+ elif 'Code:' in line or '│' in line:
252
+ current_issue["code_snippet"] = line.split('Code:')[-1].strip() if 'Code:' in line else line
253
+
254
+ if current_issue:
255
+ issues.append(current_issue)
256
+
257
+ return issues
258
+
259
+ def _is_skill_confirmed(self) -> bool:
260
+ """Check if this skill has been confirmed recently."""
261
+ if self.skill_name in self._confirmed_skills:
262
+ confirmed_at = self._confirmed_skills[self.skill_name]
263
+ if time.time() - confirmed_at < self._CONFIRMATION_TTL:
264
+ return True
265
+ # Expired, remove from cache
266
+ del self._confirmed_skills[self.skill_name]
267
+ return False
268
+
269
+ def _mark_skill_confirmed(self) -> None:
270
+ """Mark this skill as confirmed."""
271
+ self._confirmed_skills[self.skill_name] = time.time()
272
+
273
+ def _run(
274
+ self,
275
+ run_manager: Optional[CallbackManagerForToolRun] = None,
276
+ **kwargs: Any
277
+ ) -> str:
278
+ """Execute the skill synchronously. Delegates to skilllite core."""
279
+ skip_skillbox_confirmation = False
280
+ old_sandbox_level = None
281
+
282
+ try:
283
+ # Security scan for sandbox level 3
284
+ if self.sandbox_level >= 3:
285
+ # Check if already confirmed in this session
286
+ if self._is_skill_confirmed():
287
+ skip_skillbox_confirmation = True
288
+ else:
289
+ scan_result = self._perform_security_scan(kwargs)
290
+
291
+ if scan_result.requires_confirmation:
292
+ if self.confirmation_callback:
293
+ report = scan_result.format_report()
294
+ confirmed = self.confirmation_callback(report, scan_result.scan_id)
295
+
296
+ if not confirmed:
297
+ return (
298
+ f"🔐 Execution cancelled by user.\n\n"
299
+ f"{report}\n\n"
300
+ f"User declined to proceed with execution."
301
+ )
302
+ # Mark as confirmed so we don't ask again
303
+ self._mark_skill_confirmed()
304
+ skip_skillbox_confirmation = True
305
+ else:
306
+ return (
307
+ f"🔐 Security Review Required\n\n"
308
+ f"{scan_result.format_report()}\n\n"
309
+ f"Provide a confirmation_callback when creating the tool."
310
+ )
311
+
312
+ # Adjust sandbox level if user confirmed
313
+ if skip_skillbox_confirmation:
314
+ old_sandbox_level = os.environ.get("SKILLBOX_SANDBOX_LEVEL")
315
+ os.environ["SKILLBOX_SANDBOX_LEVEL"] = "1"
316
+
317
+ # Delegate execution to skilllite core
318
+ result = self.manager.execute(
319
+ self.skill_name,
320
+ kwargs,
321
+ allow_network=self.allow_network,
322
+ timeout=self.timeout
323
+ )
324
+ if result.success:
325
+ return result.output or "Execution completed successfully"
326
+ else:
327
+ return f"Error: {result.error}"
328
+ except Exception as e:
329
+ return f"Execution failed: {str(e)}"
330
+ finally:
331
+ if skip_skillbox_confirmation:
332
+ if old_sandbox_level is not None:
333
+ os.environ["SKILLBOX_SANDBOX_LEVEL"] = old_sandbox_level
334
+ elif "SKILLBOX_SANDBOX_LEVEL" in os.environ:
335
+ del os.environ["SKILLBOX_SANDBOX_LEVEL"]
336
+
337
+ async def _arun(
338
+ self,
339
+ run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
340
+ **kwargs: Any
341
+ ) -> str:
342
+ """Execute the skill asynchronously. Delegates to skilllite core."""
343
+ skip_skillbox_confirmation = False
344
+ old_sandbox_level = None
345
+
346
+ try:
347
+ if self.sandbox_level >= 3:
348
+ # Check if already confirmed in this session
349
+ if self._is_skill_confirmed():
350
+ skip_skillbox_confirmation = True
351
+ else:
352
+ scan_result = await asyncio.to_thread(self._perform_security_scan, kwargs)
353
+
354
+ if scan_result.requires_confirmation:
355
+ if self.async_confirmation_callback:
356
+ report = scan_result.format_report()
357
+ confirmed = await self.async_confirmation_callback(report, scan_result.scan_id)
358
+ if not confirmed:
359
+ return f"🔐 Execution cancelled by user.\n\n{report}"
360
+ self._mark_skill_confirmed()
361
+ skip_skillbox_confirmation = True
362
+ elif self.confirmation_callback:
363
+ report = scan_result.format_report()
364
+ confirmed = await asyncio.to_thread(
365
+ self.confirmation_callback, report, scan_result.scan_id
366
+ )
367
+ if not confirmed:
368
+ return f"🔐 Execution cancelled by user.\n\n{report}"
369
+ self._mark_skill_confirmed()
370
+ skip_skillbox_confirmation = True
371
+ else:
372
+ return (
373
+ f"🔐 Security Review Required\n\n"
374
+ f"{scan_result.format_report()}"
375
+ )
376
+
377
+ if skip_skillbox_confirmation:
378
+ old_sandbox_level = os.environ.get("SKILLBOX_SANDBOX_LEVEL")
379
+ os.environ["SKILLBOX_SANDBOX_LEVEL"] = "1"
380
+
381
+ # Delegate to skilllite core
382
+ result = await asyncio.to_thread(
383
+ self.manager.execute,
384
+ self.skill_name,
385
+ kwargs,
386
+ self.allow_network,
387
+ self.timeout
388
+ )
389
+ if result.success:
390
+ return result.output or "Execution completed successfully"
391
+ else:
392
+ return f"Error: {result.error}"
393
+ except Exception as e:
394
+ return f"Execution failed: {str(e)}"
395
+ finally:
396
+ if skip_skillbox_confirmation:
397
+ if old_sandbox_level is not None:
398
+ os.environ["SKILLBOX_SANDBOX_LEVEL"] = old_sandbox_level
399
+ elif "SKILLBOX_SANDBOX_LEVEL" in os.environ:
400
+ del os.environ["SKILLBOX_SANDBOX_LEVEL"]
401
+
402
+
403
+ class SkillLiteToolkit:
404
+ """
405
+ LangChain Toolkit for SkillLite.
406
+
407
+ Provides a convenient way to create LangChain tools from all skills
408
+ registered in a SkillManager.
409
+
410
+ Usage:
411
+ manager = SkillManager(skills_dir="./skills")
412
+ tools = SkillLiteToolkit.from_manager(manager)
413
+
414
+ # Or with options
415
+ tools = SkillLiteToolkit.from_manager(
416
+ manager,
417
+ skill_names=["calculator", "web_search"],
418
+ allow_network=True,
419
+ timeout=60
420
+ )
421
+
422
+ # With security confirmation callback (for sandbox level 3)
423
+ def confirm_execution(report: str, scan_id: str) -> bool:
424
+ print(report)
425
+ return input("Continue? [y/N]: ").lower() == 'y'
426
+
427
+ tools = SkillLiteToolkit.from_manager(
428
+ manager,
429
+ sandbox_level=3,
430
+ confirmation_callback=confirm_execution
431
+ )
432
+ """
433
+
434
+ @staticmethod
435
+ def from_manager(
436
+ manager: "SkillManager",
437
+ skill_names: Optional[List[str]] = None,
438
+ allow_network: bool = False,
439
+ timeout: Optional[int] = None,
440
+ sandbox_level: int = 3,
441
+ confirmation_callback: Optional[ConfirmationCallback] = None,
442
+ async_confirmation_callback: Optional[AsyncConfirmationCallback] = None,
443
+ ) -> List[SkillLiteTool]:
444
+ """
445
+ Create LangChain tools from a SkillManager.
446
+
447
+ Args:
448
+ manager: SkillManager instance with registered skills
449
+ skill_names: Optional list of skill names to include (default: all)
450
+ allow_network: Whether to allow network access for all tools
451
+ timeout: Execution timeout in seconds for all tools
452
+ sandbox_level: Sandbox security level (1/2/3, default: 3)
453
+ - Level 1: No sandbox - direct execution
454
+ - Level 2: Sandbox isolation only
455
+ - Level 3: Sandbox + security scanning (requires confirmation)
456
+ confirmation_callback: Sync callback for security confirmation.
457
+ Signature: (security_report: str, scan_id: str) -> bool
458
+ async_confirmation_callback: Async callback for security confirmation.
459
+ Signature: (security_report: str, scan_id: str) -> Future[bool]
460
+
461
+ Returns:
462
+ List of SkillLiteTool instances
463
+ """
464
+ tools = []
465
+
466
+ # Get executable skills from skilllite core
467
+ skills = manager.list_executable_skills()
468
+
469
+ for skill in skills:
470
+ # Filter by name if specified
471
+ if skill_names and skill.name not in skill_names:
472
+ continue
473
+
474
+ # Create tool with security confirmation support
475
+ tool = SkillLiteTool(
476
+ name=skill.name,
477
+ description=skill.description or f"Execute the {skill.name} skill",
478
+ manager=manager,
479
+ skill_name=skill.name,
480
+ allow_network=allow_network,
481
+ timeout=timeout,
482
+ sandbox_level=sandbox_level,
483
+ confirmation_callback=confirmation_callback,
484
+ async_confirmation_callback=async_confirmation_callback,
485
+ )
486
+ tools.append(tool)
487
+
488
+ return tools
489
+
490
+ @staticmethod
491
+ def from_directory(
492
+ skills_dir: str,
493
+ skill_names: Optional[List[str]] = None,
494
+ allow_network: bool = False,
495
+ timeout: Optional[int] = None,
496
+ sandbox_level: int = 3,
497
+ confirmation_callback: Optional[ConfirmationCallback] = None,
498
+ async_confirmation_callback: Optional[AsyncConfirmationCallback] = None,
499
+ ) -> List[SkillLiteTool]:
500
+ """
501
+ Create LangChain tools from a skills directory.
502
+
503
+ Convenience method that creates a SkillManager and loads all skills.
504
+
505
+ Args:
506
+ skills_dir: Path to directory containing skill folders
507
+ skill_names: Optional list of skill names to include
508
+ allow_network: Whether to allow network access
509
+ timeout: Execution timeout in seconds
510
+ sandbox_level: Sandbox security level (1/2/3)
511
+ confirmation_callback: Sync callback for security confirmation
512
+ async_confirmation_callback: Async callback for security confirmation
513
+
514
+ Returns:
515
+ List of SkillLiteTool instances
516
+ """
517
+ manager = SkillManager(skills_dir=skills_dir)
518
+ return SkillLiteToolkit.from_manager(
519
+ manager=manager,
520
+ skill_names=skill_names,
521
+ allow_network=allow_network,
522
+ timeout=timeout,
523
+ sandbox_level=sandbox_level,
524
+ confirmation_callback=confirmation_callback,
525
+ async_confirmation_callback=async_confirmation_callback,
526
+ )
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-skilllite
3
+ Version: 0.1.0
4
+ Summary: LangChain integration for SkillLite - Lightweight sandboxed Python skill execution engine
5
+ Author-email: SkillLite Team <skilllite@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/EXboys/langchain-skilllite
8
+ Project-URL: Documentation, https://github.com/EXboys/langchain-skilllite#readme
9
+ Project-URL: Repository, https://github.com/EXboys/langchain-skilllite
10
+ Project-URL: Issues, https://github.com/EXboys/langchain-skilllite/issues
11
+ Project-URL: LangChain, https://python.langchain.com/
12
+ Project-URL: SkillLite, https://github.com/EXboys/skilllite
13
+ Keywords: langchain,skilllite,agent,tools,sandbox,llm,skills,python-execution
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: langchain-core>=0.3.0
31
+ Requires-Dist: skilllite>=0.1.1
32
+ Provides-Extra: langgraph
33
+ Requires-Dist: langgraph>=0.2.0; extra == "langgraph"
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest>=7.0; extra == "test"
36
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
37
+ Requires-Dist: langchain-tests>=0.3.0; extra == "test"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=7.0; extra == "dev"
40
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
41
+ Requires-Dist: langchain-tests>=0.3.0; extra == "dev"
42
+ Requires-Dist: black>=23.0; extra == "dev"
43
+ Requires-Dist: mypy>=1.0; extra == "dev"
44
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
45
+ Dynamic: license-file
46
+
47
+ # langchain-skilllite
48
+
49
+ [![PyPI version](https://badge.fury.io/py/langchain-skilllite.svg)](https://badge.fury.io/py/langchain-skilllite)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
+
52
+ LangChain integration for [SkillLite](https://github.com/EXboys/skilllite) - a lightweight sandboxed Python skill execution engine.
53
+
54
+ ## Features
55
+
56
+ - 🔒 **Sandboxed Execution** - All skills run in a Rust-based sandbox (skillbox)
57
+ - 📝 **Declarative Skills** - Define skills via SKILL.md, no Python wrappers needed
58
+ - 🔍 **Security Scanning** - Pre-execution code analysis for dangerous operations
59
+ - ✅ **Confirmation Callbacks** - User approval for high-severity security issues
60
+ - ⚡ **Async Support** - Full async support for LangGraph agents
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install langchain-skilllite
66
+ ```
67
+
68
+ This will also install the required dependencies:
69
+ - `langchain-core>=0.3.0`
70
+ - `skilllite>=0.1.1`
71
+
72
+ ## Quick Start
73
+
74
+ ```python
75
+ from langchain_skilllite import SkillLiteToolkit
76
+ from langchain_openai import ChatOpenAI
77
+ from langgraph.prebuilt import create_react_agent
78
+
79
+ # Load all skills from a directory as LangChain tools
80
+ tools = SkillLiteToolkit.from_directory("./skills")
81
+
82
+ # Create a LangGraph agent
83
+ agent = create_react_agent(ChatOpenAI(model="gpt-4"), tools)
84
+
85
+ # Run the agent
86
+ result = agent.invoke({
87
+ "messages": [("user", "Calculate 15 + 27 using the calculator skill")]
88
+ })
89
+ ```
90
+
91
+ ## Usage
92
+
93
+ ### Basic Usage with SkillManager
94
+
95
+ ```python
96
+ from skilllite import SkillManager
97
+ from langchain_skilllite import SkillLiteToolkit
98
+
99
+ # Create a SkillManager
100
+ manager = SkillManager(skills_dir="./skills")
101
+
102
+ # Convert all skills to LangChain tools
103
+ tools = SkillLiteToolkit.from_manager(manager)
104
+
105
+ # Or select specific skills
106
+ tools = SkillLiteToolkit.from_manager(
107
+ manager,
108
+ skill_names=["calculator", "web_search"],
109
+ allow_network=True,
110
+ timeout=60
111
+ )
112
+ ```
113
+
114
+ ### Security Levels
115
+
116
+ SkillLite supports three sandbox security levels:
117
+
118
+ | Level | Description |
119
+ |-------|-------------|
120
+ | 1 | No sandbox - direct execution (fastest, least secure) |
121
+ | 2 | Sandbox isolation only |
122
+ | 3 | Sandbox + security scanning (default, most secure) |
123
+
124
+ ```python
125
+ # Level 3 with confirmation callback for high-severity issues
126
+ def confirm_execution(report: str, scan_id: str) -> bool:
127
+ print(report)
128
+ return input("Proceed? [y/N]: ").lower() == 'y'
129
+
130
+ tools = SkillLiteToolkit.from_directory(
131
+ "./skills",
132
+ sandbox_level=3,
133
+ confirmation_callback=confirm_execution
134
+ )
135
+ ```
136
+
137
+ ### Async Confirmation (for LangGraph)
138
+
139
+ ```python
140
+ import asyncio
141
+
142
+ async def async_confirm(report: str, scan_id: str) -> bool:
143
+ print(report)
144
+ # In a real app, this might be a UI prompt
145
+ return True
146
+
147
+ tools = SkillLiteToolkit.from_directory(
148
+ "./skills",
149
+ sandbox_level=3,
150
+ async_confirmation_callback=async_confirm
151
+ )
152
+ ```
153
+
154
+ ### Callback Handler for Monitoring
155
+
156
+ ```python
157
+ from langchain_skilllite import SkillLiteCallbackHandler
158
+
159
+ handler = SkillLiteCallbackHandler(verbose=True)
160
+
161
+ # Use with agent
162
+ result = agent.invoke(
163
+ {"messages": [("user", "Run my skill")]},
164
+ config={"callbacks": [handler]}
165
+ )
166
+
167
+ # Get execution summary
168
+ print(handler.get_execution_summary())
169
+ ```
170
+
171
+ ## API Reference
172
+
173
+ ### SkillLiteTool
174
+
175
+ LangChain `BaseTool` adapter for a single SkillLite skill.
176
+
177
+ ### SkillLiteToolkit
178
+
179
+ Factory class for creating multiple `SkillLiteTool` instances.
180
+
181
+ - `from_manager(manager, ...)` - Create tools from a SkillManager
182
+ - `from_directory(skills_dir, ...)` - Create tools from a skills directory
183
+
184
+ ### SkillLiteCallbackHandler
185
+
186
+ LangChain callback handler for monitoring skill execution.
187
+
188
+ ## Requirements
189
+
190
+ - Python >= 3.9
191
+ - langchain-core >= 0.3.0
192
+ - skilllite >= 0.1.1
193
+
194
+ ## License
195
+
196
+ MIT License - see [LICENSE](LICENSE) for details.
197
+
198
+ ## Links
199
+
200
+ - [SkillLite Repository](https://github.com/EXboys/skilllite)
201
+ - [LangChain Documentation](https://python.langchain.com/)
202
+ - [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
203
+
@@ -0,0 +1,9 @@
1
+ langchain_skilllite/__init__.py,sha256=pkzWP7nP7A_hRsfl0TbgA65WWq-UpMMBCyy4VLslD8Q,1461
2
+ langchain_skilllite/_version.py,sha256=UluS3ysGlayfko8ludKoiC9cT8bp9A2iMfbL6sP5VqQ,75
3
+ langchain_skilllite/callbacks.py,sha256=L-_Qguh_R1jd7Qtx_QN0kaFE5wX7t-9USOoUNaUug6k,5254
4
+ langchain_skilllite/tools.py,sha256=2OBxA2GwNAwnQzYAUmV63FkmI6qB7-r36_LagtVu27E,20921
5
+ langchain_skilllite-0.1.0.dist-info/licenses/LICENSE,sha256=HcK5iz9Y3FKj6oiQH6Q0tx1fYDXhKqkrCUUM8XRngRk,1072
6
+ langchain_skilllite-0.1.0.dist-info/METADATA,sha256=icBeT2u8kwcdN-naTw5zlvaBdsxzD7cdoWzvUjMke84,6077
7
+ langchain_skilllite-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
+ langchain_skilllite-0.1.0.dist-info/top_level.txt,sha256=XmA85AHn71wM124Kssl3hQRQElz3JJuDUjtuLJlL7nE,20
9
+ langchain_skilllite-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SkillLite Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1 @@
1
+ langchain_skilllite