skilllite 0.1.0__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/__init__.py +1 -1
- skilllite/cli/__init__.py +19 -0
- skilllite/cli/__main__.py +10 -0
- skilllite/cli/binary.py +93 -0
- skilllite/cli/integrations/__init__.py +8 -0
- skilllite/cli/integrations/opencode.py +316 -0
- skilllite/cli/main.py +142 -0
- skilllite/cli/mcp.py +29 -0
- skilllite/core/__init__.py +2 -0
- skilllite/core/adapters/__init__.py +74 -0
- skilllite/core/adapters/langchain.py +362 -0
- skilllite/core/adapters/llamaindex.py +264 -0
- skilllite/core/handler.py +179 -4
- skilllite/core/loops.py +180 -15
- skilllite/core/manager.py +82 -15
- skilllite/core/metadata.py +14 -7
- skilllite/core/security.py +420 -0
- skilllite/mcp/server.py +537 -49
- skilllite/quick.py +14 -4
- skilllite/sandbox/context.py +155 -0
- skilllite/sandbox/execution_service.py +254 -0
- skilllite/sandbox/skillbox/executor.py +124 -19
- skilllite/sandbox/unified_executor.py +359 -0
- {skilllite-0.1.0.dist-info → skilllite-0.1.2.dist-info}/METADATA +151 -1
- skilllite-0.1.2.dist-info/RECORD +45 -0
- skilllite/cli.py +0 -217
- skilllite-0.1.0.dist-info/RECORD +0 -32
- {skilllite-0.1.0.dist-info → skilllite-0.1.2.dist-info}/WHEEL +0 -0
- {skilllite-0.1.0.dist-info → skilllite-0.1.2.dist-info}/entry_points.txt +0 -0
- {skilllite-0.1.0.dist-info → skilllite-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {skilllite-0.1.0.dist-info → skilllite-0.1.2.dist-info}/top_level.txt +0 -0
skilllite/mcp/server.py
CHANGED
|
@@ -4,15 +4,22 @@ MCP Server implementation for SkillLite.
|
|
|
4
4
|
This module provides a secure code execution sandbox server implementing
|
|
5
5
|
the Model Context Protocol (MCP).
|
|
6
6
|
|
|
7
|
+
Features:
|
|
8
|
+
1. **Skills Management**: List, inspect, and execute pre-defined skills
|
|
9
|
+
from the skills directory.
|
|
10
|
+
|
|
11
|
+
2. **Code Execution**: Execute arbitrary code in a secure sandbox with
|
|
12
|
+
security scanning.
|
|
13
|
+
|
|
7
14
|
Security Model:
|
|
8
15
|
The MCP server implements a two-phase execution model for security:
|
|
9
|
-
|
|
16
|
+
|
|
10
17
|
1. **scan_code**: First, scan the code for security issues. This returns
|
|
11
18
|
a detailed report of any potential risks found in the code.
|
|
12
|
-
|
|
19
|
+
|
|
13
20
|
2. **execute_code**: Then, execute the code. If security issues were found,
|
|
14
21
|
the caller must explicitly set `confirmed=true` to proceed.
|
|
15
|
-
|
|
22
|
+
|
|
16
23
|
This allows LLM clients to present security warnings to users and get
|
|
17
24
|
explicit confirmation before executing potentially dangerous code.
|
|
18
25
|
|
|
@@ -20,6 +27,7 @@ Environment Variables:
|
|
|
20
27
|
SKILLBOX_SANDBOX_LEVEL: Default sandbox level (1/2/3, default: 3)
|
|
21
28
|
SKILLBOX_PATH: Path to skillbox binary
|
|
22
29
|
MCP_SANDBOX_TIMEOUT: Execution timeout in seconds (default: 30)
|
|
30
|
+
SKILLLITE_SKILLS_DIR: Directory containing skills (default: ./.skills)
|
|
23
31
|
"""
|
|
24
32
|
|
|
25
33
|
import asyncio
|
|
@@ -90,7 +98,27 @@ except ImportError:
|
|
|
90
98
|
|
|
91
99
|
class SecurityScanResult:
|
|
92
100
|
"""Result of a security scan."""
|
|
93
|
-
|
|
101
|
+
|
|
102
|
+
# Issue types that are HARD BLOCKED in L3 sandbox (cannot execute even with confirmation)
|
|
103
|
+
# These operations are blocked at the sandbox runtime level, not just static analysis
|
|
104
|
+
HARD_BLOCKED_ISSUE_TYPES_L3 = {
|
|
105
|
+
"Process Execution", # os.system, subprocess, etc.
|
|
106
|
+
"ProcessExecution", # Alternative format
|
|
107
|
+
"process_execution", # Snake case format
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Rule IDs that are specifically hard blocked in L3 sandbox
|
|
111
|
+
HARD_BLOCKED_RULE_IDS_L3 = {
|
|
112
|
+
"py-subprocess", # subprocess.call/run/Popen
|
|
113
|
+
"py-os-system", # os.system/popen/spawn
|
|
114
|
+
"js-child-process", # child_process.exec/spawn
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Dangerous module imports that lead to hard blocks when combined with execution
|
|
118
|
+
HARD_BLOCKED_MODULES_L3 = {
|
|
119
|
+
"py-os-import", # import os/subprocess/shutil
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
def __init__(
|
|
95
123
|
self,
|
|
96
124
|
is_safe: bool,
|
|
@@ -100,6 +128,7 @@ class SecurityScanResult:
|
|
|
100
128
|
high_severity_count: int = 0,
|
|
101
129
|
medium_severity_count: int = 0,
|
|
102
130
|
low_severity_count: int = 0,
|
|
131
|
+
sandbox_level: int = 3,
|
|
103
132
|
):
|
|
104
133
|
self.is_safe = is_safe
|
|
105
134
|
self.issues = issues
|
|
@@ -108,8 +137,31 @@ class SecurityScanResult:
|
|
|
108
137
|
self.high_severity_count = high_severity_count
|
|
109
138
|
self.medium_severity_count = medium_severity_count
|
|
110
139
|
self.low_severity_count = low_severity_count
|
|
140
|
+
self.sandbox_level = sandbox_level
|
|
111
141
|
self.timestamp = time.time()
|
|
112
|
-
|
|
142
|
+
|
|
143
|
+
# Calculate hard blocked issues
|
|
144
|
+
self.hard_blocked_issues = self._find_hard_blocked_issues()
|
|
145
|
+
self.has_hard_blocked = len(self.hard_blocked_issues) > 0
|
|
146
|
+
|
|
147
|
+
def _find_hard_blocked_issues(self) -> List[Dict[str, Any]]:
|
|
148
|
+
"""Find issues that are hard blocked in the current sandbox level."""
|
|
149
|
+
if self.sandbox_level < 3:
|
|
150
|
+
# Only L3 has hard blocks
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
hard_blocked = []
|
|
154
|
+
for issue in self.issues:
|
|
155
|
+
issue_type = issue.get("issue_type", "")
|
|
156
|
+
rule_id = issue.get("rule_id", "")
|
|
157
|
+
|
|
158
|
+
# Check if this issue type or rule is hard blocked
|
|
159
|
+
if (issue_type in self.HARD_BLOCKED_ISSUE_TYPES_L3 or
|
|
160
|
+
rule_id in self.HARD_BLOCKED_RULE_IDS_L3):
|
|
161
|
+
hard_blocked.append(issue)
|
|
162
|
+
|
|
163
|
+
return hard_blocked
|
|
164
|
+
|
|
113
165
|
def to_dict(self) -> Dict[str, Any]:
|
|
114
166
|
return {
|
|
115
167
|
"is_safe": self.is_safe,
|
|
@@ -119,42 +171,64 @@ class SecurityScanResult:
|
|
|
119
171
|
"high_severity_count": self.high_severity_count,
|
|
120
172
|
"medium_severity_count": self.medium_severity_count,
|
|
121
173
|
"low_severity_count": self.low_severity_count,
|
|
122
|
-
"requires_confirmation": self.high_severity_count > 0,
|
|
174
|
+
"requires_confirmation": self.high_severity_count > 0 and not self.has_hard_blocked,
|
|
175
|
+
"has_hard_blocked": self.has_hard_blocked,
|
|
176
|
+
"hard_blocked_count": len(self.hard_blocked_issues),
|
|
177
|
+
"sandbox_level": self.sandbox_level,
|
|
123
178
|
}
|
|
124
|
-
|
|
179
|
+
|
|
125
180
|
def format_report(self) -> str:
|
|
126
181
|
"""Format a human-readable security report."""
|
|
127
182
|
if not self.issues:
|
|
128
183
|
return "✅ Security scan passed. No issues found."
|
|
129
|
-
|
|
184
|
+
|
|
130
185
|
lines = [
|
|
131
186
|
f"📋 Security Scan Report (ID: {self.scan_id[:8]})",
|
|
187
|
+
f" Sandbox Level: L{self.sandbox_level}",
|
|
132
188
|
f" Found {len(self.issues)} item(s) for review:",
|
|
133
189
|
"",
|
|
134
190
|
]
|
|
135
|
-
|
|
191
|
+
|
|
136
192
|
severity_icons = {
|
|
137
193
|
"Critical": "🔴",
|
|
138
194
|
"High": "🟠",
|
|
139
195
|
"Medium": "🟡",
|
|
140
196
|
"Low": "🟢",
|
|
141
197
|
}
|
|
142
|
-
|
|
198
|
+
|
|
143
199
|
for idx, issue in enumerate(self.issues, 1):
|
|
144
200
|
severity = issue.get("severity", "Medium")
|
|
145
201
|
icon = severity_icons.get(severity, "⚪")
|
|
146
|
-
|
|
202
|
+
|
|
203
|
+
# Mark hard blocked issues
|
|
204
|
+
is_hard_blocked = issue in self.hard_blocked_issues
|
|
205
|
+
block_marker = " 🚫 [HARD BLOCKED]" if is_hard_blocked else ""
|
|
206
|
+
|
|
207
|
+
lines.append(f" {icon} #{idx} [{severity}] {issue.get('issue_type', 'Unknown')}{block_marker}")
|
|
147
208
|
lines.append(f" ├─ Rule: {issue.get('rule_id', 'N/A')}")
|
|
148
209
|
lines.append(f" ├─ Line {issue.get('line_number', '?')}: {issue.get('description', '')}")
|
|
149
210
|
lines.append(f" └─ Code: {issue.get('code_snippet', '')[:60]}...")
|
|
150
211
|
lines.append("")
|
|
151
|
-
|
|
152
|
-
|
|
212
|
+
|
|
213
|
+
# Different messages based on whether there are hard blocked issues
|
|
214
|
+
if self.has_hard_blocked:
|
|
215
|
+
lines.append("🚫 HARD BLOCKED: This code contains operations that CANNOT be executed")
|
|
216
|
+
lines.append(f" in the current L{self.sandbox_level} sandbox environment.")
|
|
217
|
+
lines.append("")
|
|
218
|
+
lines.append(" The following operations are permanently blocked at runtime:")
|
|
219
|
+
for issue in self.hard_blocked_issues:
|
|
220
|
+
lines.append(f" • {issue.get('issue_type', 'Unknown')}: {issue.get('description', '')}")
|
|
221
|
+
lines.append("")
|
|
222
|
+
lines.append(" ⚠️ Even with confirmation, this code will fail to execute.")
|
|
223
|
+
lines.append(" Options:")
|
|
224
|
+
lines.append(" 1. Modify the code to remove blocked operations")
|
|
225
|
+
lines.append(" 2. Use a lower sandbox level (L1 or L2) if permitted")
|
|
226
|
+
elif self.high_severity_count > 0:
|
|
153
227
|
lines.append("⚠️ High severity issues found. Confirmation required to execute.")
|
|
154
228
|
lines.append(f" To proceed, call execute_code with confirmed=true and scan_id=\"{self.scan_id}\"")
|
|
155
229
|
else:
|
|
156
230
|
lines.append("ℹ️ Only low/medium severity issues found. Safe to execute.")
|
|
157
|
-
|
|
231
|
+
|
|
158
232
|
return "\n".join(lines)
|
|
159
233
|
|
|
160
234
|
|
|
@@ -235,26 +309,37 @@ This skill executes code from MCP.
|
|
|
235
309
|
|
|
236
310
|
return skill_dir, code_file
|
|
237
311
|
|
|
238
|
-
def scan_code(self, language: str, code: str) -> SecurityScanResult:
|
|
239
|
-
"""Scan code for security issues without executing it.
|
|
312
|
+
def scan_code(self, language: str, code: str, sandbox_level: Optional[int] = None) -> SecurityScanResult:
|
|
313
|
+
"""Scan code for security issues without executing it.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
language: Programming language (python, javascript, bash)
|
|
317
|
+
code: Code to scan
|
|
318
|
+
sandbox_level: Sandbox level to check against (default: from env or 3)
|
|
319
|
+
"""
|
|
320
|
+
# Use default sandbox level if not specified
|
|
321
|
+
if sandbox_level is None:
|
|
322
|
+
sandbox_level = self.default_sandbox_level
|
|
323
|
+
|
|
240
324
|
if not self.runtime_available:
|
|
241
325
|
return SecurityScanResult(
|
|
242
326
|
is_safe=False,
|
|
243
|
-
issues=[{"severity": "Critical", "issue_type": "RuntimeError",
|
|
327
|
+
issues=[{"severity": "Critical", "issue_type": "RuntimeError",
|
|
244
328
|
"description": f"skillbox not found at {self.skillbox_path}",
|
|
245
329
|
"rule_id": "system", "line_number": 0, "code_snippet": ""}],
|
|
246
330
|
scan_id="error",
|
|
247
331
|
code_hash="",
|
|
248
332
|
high_severity_count=1,
|
|
333
|
+
sandbox_level=sandbox_level,
|
|
249
334
|
)
|
|
250
|
-
|
|
335
|
+
|
|
251
336
|
self._cleanup_expired_scans()
|
|
252
337
|
code_hash = self._generate_code_hash(language, code)
|
|
253
338
|
scan_id = self._generate_scan_id(code_hash)
|
|
254
|
-
|
|
339
|
+
|
|
255
340
|
try:
|
|
256
341
|
skill_dir, code_file = self._create_temp_skill(language, code)
|
|
257
|
-
|
|
342
|
+
|
|
258
343
|
try:
|
|
259
344
|
result = subprocess.run(
|
|
260
345
|
[self.skillbox_path, "security-scan", code_file],
|
|
@@ -262,12 +347,12 @@ This skill executes code from MCP.
|
|
|
262
347
|
text=True,
|
|
263
348
|
timeout=30
|
|
264
349
|
)
|
|
265
|
-
|
|
350
|
+
|
|
266
351
|
issues = self._parse_scan_output(result.stdout + result.stderr)
|
|
267
352
|
high_count = sum(1 for i in issues if i.get("severity") in ["Critical", "High"])
|
|
268
353
|
medium_count = sum(1 for i in issues if i.get("severity") == "Medium")
|
|
269
354
|
low_count = sum(1 for i in issues if i.get("severity") == "Low")
|
|
270
|
-
|
|
355
|
+
|
|
271
356
|
scan_result = SecurityScanResult(
|
|
272
357
|
is_safe=high_count == 0,
|
|
273
358
|
issues=issues,
|
|
@@ -276,14 +361,15 @@ This skill executes code from MCP.
|
|
|
276
361
|
high_severity_count=high_count,
|
|
277
362
|
medium_severity_count=medium_count,
|
|
278
363
|
low_severity_count=low_count,
|
|
364
|
+
sandbox_level=sandbox_level,
|
|
279
365
|
)
|
|
280
|
-
|
|
366
|
+
|
|
281
367
|
self._scan_cache[scan_id] = scan_result
|
|
282
368
|
return scan_result
|
|
283
|
-
|
|
369
|
+
|
|
284
370
|
finally:
|
|
285
371
|
shutil.rmtree(skill_dir, ignore_errors=True)
|
|
286
|
-
|
|
372
|
+
|
|
287
373
|
except subprocess.TimeoutExpired:
|
|
288
374
|
return SecurityScanResult(
|
|
289
375
|
is_safe=False,
|
|
@@ -293,6 +379,7 @@ This skill executes code from MCP.
|
|
|
293
379
|
scan_id=scan_id,
|
|
294
380
|
code_hash=code_hash,
|
|
295
381
|
high_severity_count=1,
|
|
382
|
+
sandbox_level=sandbox_level,
|
|
296
383
|
)
|
|
297
384
|
except Exception as e:
|
|
298
385
|
return SecurityScanResult(
|
|
@@ -303,6 +390,7 @@ This skill executes code from MCP.
|
|
|
303
390
|
scan_id=scan_id,
|
|
304
391
|
code_hash=code_hash,
|
|
305
392
|
high_severity_count=1,
|
|
393
|
+
sandbox_level=sandbox_level,
|
|
306
394
|
)
|
|
307
395
|
|
|
308
396
|
def _parse_scan_output(self, output: str) -> List[Dict[str, Any]]:
|
|
@@ -404,9 +492,31 @@ This skill executes code from MCP.
|
|
|
404
492
|
}
|
|
405
493
|
|
|
406
494
|
code_hash = self._generate_code_hash(language, code)
|
|
407
|
-
|
|
495
|
+
|
|
408
496
|
if sandbox_level >= 3 and not confirmed:
|
|
409
|
-
scan_result = self.scan_code(language, code)
|
|
497
|
+
scan_result = self.scan_code(language, code, sandbox_level=sandbox_level)
|
|
498
|
+
|
|
499
|
+
# Check for hard blocked issues first
|
|
500
|
+
if scan_result.has_hard_blocked:
|
|
501
|
+
return {
|
|
502
|
+
"success": False,
|
|
503
|
+
"stdout": "",
|
|
504
|
+
"stderr": (
|
|
505
|
+
f"🚫 Execution Blocked\n\n"
|
|
506
|
+
f"{scan_result.format_report()}\n\n"
|
|
507
|
+
f"❌ This code contains operations that are PERMANENTLY BLOCKED\n"
|
|
508
|
+
f" in the L{sandbox_level} sandbox environment.\n\n"
|
|
509
|
+
f" Even with confirmation, this code CANNOT be executed.\n\n"
|
|
510
|
+
f"Options:\n"
|
|
511
|
+
f" 1. Modify the code to remove blocked operations\n"
|
|
512
|
+
f" 2. Use sandbox_level=1 or sandbox_level=2 (if permitted)\n"
|
|
513
|
+
),
|
|
514
|
+
"exit_code": 4,
|
|
515
|
+
"hard_blocked": True,
|
|
516
|
+
"security_issues": scan_result.to_dict(),
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
# Soft risk: can be confirmed
|
|
410
520
|
if scan_result.high_severity_count > 0:
|
|
411
521
|
return {
|
|
412
522
|
"success": False,
|
|
@@ -414,7 +524,9 @@ This skill executes code from MCP.
|
|
|
414
524
|
"stderr": (
|
|
415
525
|
f"🔐 Security Review Required\n\n"
|
|
416
526
|
f"{scan_result.format_report()}\n\n"
|
|
417
|
-
f"
|
|
527
|
+
f"⚠️ IMPORTANT: You MUST ask the user for confirmation before proceeding.\n"
|
|
528
|
+
f"Show this security report to the user and wait for their explicit approval.\n\n"
|
|
529
|
+
f"If the user approves, call execute_code again with:\n"
|
|
418
530
|
f" - confirmed: true\n"
|
|
419
531
|
f" - scan_id: \"{scan_result.scan_id}\"\n"
|
|
420
532
|
),
|
|
@@ -423,7 +535,7 @@ This skill executes code from MCP.
|
|
|
423
535
|
"scan_id": scan_result.scan_id,
|
|
424
536
|
"security_issues": scan_result.to_dict(),
|
|
425
537
|
}
|
|
426
|
-
|
|
538
|
+
|
|
427
539
|
if confirmed and scan_id:
|
|
428
540
|
cached_result = self.verify_scan(scan_id, code_hash)
|
|
429
541
|
if not cached_result:
|
|
@@ -436,6 +548,28 @@ This skill executes code from MCP.
|
|
|
436
548
|
),
|
|
437
549
|
"exit_code": 3,
|
|
438
550
|
}
|
|
551
|
+
|
|
552
|
+
# Even with confirmation, check for hard blocked issues
|
|
553
|
+
if cached_result.has_hard_blocked:
|
|
554
|
+
return {
|
|
555
|
+
"success": False,
|
|
556
|
+
"stdout": "",
|
|
557
|
+
"stderr": (
|
|
558
|
+
f"🚫 Execution Blocked (Even After Confirmation)\n\n"
|
|
559
|
+
f"The code contains operations that are PERMANENTLY BLOCKED\n"
|
|
560
|
+
f"in the L{sandbox_level} sandbox environment:\n\n"
|
|
561
|
+
+ "\n".join(f" • {issue.get('issue_type', 'Unknown')}: {issue.get('description', '')}"
|
|
562
|
+
for issue in cached_result.hard_blocked_issues) +
|
|
563
|
+
f"\n\n"
|
|
564
|
+
f"❌ Confirmation cannot override sandbox runtime restrictions.\n\n"
|
|
565
|
+
f"Options:\n"
|
|
566
|
+
f" 1. Modify the code to remove blocked operations\n"
|
|
567
|
+
f" 2. Use sandbox_level=1 or sandbox_level=2 (if permitted)\n"
|
|
568
|
+
),
|
|
569
|
+
"exit_code": 4,
|
|
570
|
+
"hard_blocked": True,
|
|
571
|
+
"security_issues": cached_result.to_dict(),
|
|
572
|
+
}
|
|
439
573
|
|
|
440
574
|
try:
|
|
441
575
|
skill_dir, _ = self._create_temp_skill(language, code)
|
|
@@ -494,21 +628,31 @@ This skill executes code from MCP.
|
|
|
494
628
|
class MCPServer:
|
|
495
629
|
"""MCP server for SkillLite sandbox execution.
|
|
496
630
|
|
|
497
|
-
This server provides
|
|
498
|
-
|
|
499
|
-
|
|
631
|
+
This server provides tools for skills management and secure code execution:
|
|
632
|
+
|
|
633
|
+
Skills Tools:
|
|
634
|
+
1. **list_skills**: List all available skills in the skills directory.
|
|
635
|
+
2. **get_skill_info**: Get detailed information about a specific skill.
|
|
636
|
+
3. **run_skill**: Execute a skill with given input parameters.
|
|
637
|
+
|
|
638
|
+
Code Execution Tools:
|
|
639
|
+
4. **scan_code**: Scan code for security issues before execution.
|
|
500
640
|
Returns a detailed report and a scan_id for confirmation.
|
|
501
|
-
|
|
502
|
-
2. **execute_code**: Execute code in a sandbox. If high-severity
|
|
641
|
+
5. **execute_code**: Execute code in a sandbox. If high-severity
|
|
503
642
|
security issues are found, requires explicit confirmation.
|
|
504
|
-
|
|
505
|
-
Example workflow:
|
|
643
|
+
|
|
644
|
+
Example workflow for skills:
|
|
645
|
+
1. Call list_skills to see available skills
|
|
646
|
+
2. Call get_skill_info to understand a skill's parameters
|
|
647
|
+
3. Call run_skill with the required input
|
|
648
|
+
|
|
649
|
+
Example workflow for code execution:
|
|
506
650
|
1. Call scan_code to check for security issues
|
|
507
651
|
2. Review the security report with the user
|
|
508
652
|
3. If user approves, call execute_code with confirmed=true and scan_id
|
|
509
653
|
"""
|
|
510
|
-
|
|
511
|
-
def __init__(self):
|
|
654
|
+
|
|
655
|
+
def __init__(self, skills_dir: Optional[str] = None):
|
|
512
656
|
if not MCP_AVAILABLE:
|
|
513
657
|
raise ImportError(
|
|
514
658
|
"MCP library not available. Please install it with: "
|
|
@@ -516,14 +660,98 @@ class MCPServer:
|
|
|
516
660
|
)
|
|
517
661
|
self.server = Server("skilllite-mcp-server")
|
|
518
662
|
self.executor = SandboxExecutor()
|
|
663
|
+
|
|
664
|
+
# Initialize SkillManager for skills support
|
|
665
|
+
self.skills_dir = skills_dir or os.environ.get("SKILLLITE_SKILLS_DIR", "./.skills")
|
|
666
|
+
self.skill_manager = None
|
|
667
|
+
self._init_skill_manager()
|
|
668
|
+
|
|
519
669
|
self._setup_handlers()
|
|
670
|
+
|
|
671
|
+
def _init_skill_manager(self):
|
|
672
|
+
"""Initialize the SkillManager if skills directory exists."""
|
|
673
|
+
try:
|
|
674
|
+
from ..core.manager import SkillManager
|
|
675
|
+
skills_path = Path(self.skills_dir)
|
|
676
|
+
if skills_path.exists():
|
|
677
|
+
self.skill_manager = SkillManager(skills_dir=str(skills_path))
|
|
678
|
+
except Exception as e:
|
|
679
|
+
# Skills not available, continue without them
|
|
680
|
+
pass
|
|
520
681
|
|
|
521
682
|
def _setup_handlers(self):
|
|
522
683
|
"""Setup MCP server handlers."""
|
|
523
|
-
|
|
684
|
+
|
|
524
685
|
@self.server.list_tools()
|
|
525
686
|
async def list_tools() -> List[Tool]:
|
|
526
|
-
|
|
687
|
+
tools = [
|
|
688
|
+
# Skills management tools
|
|
689
|
+
Tool(
|
|
690
|
+
name="list_skills",
|
|
691
|
+
description=(
|
|
692
|
+
"List all available skills in the skills directory. "
|
|
693
|
+
"Returns skill names, descriptions, and languages."
|
|
694
|
+
),
|
|
695
|
+
inputSchema={
|
|
696
|
+
"type": "object",
|
|
697
|
+
"properties": {},
|
|
698
|
+
"required": []
|
|
699
|
+
}
|
|
700
|
+
),
|
|
701
|
+
Tool(
|
|
702
|
+
name="get_skill_info",
|
|
703
|
+
description=(
|
|
704
|
+
"Get detailed information about a specific skill, "
|
|
705
|
+
"including its input schema, description, and usage."
|
|
706
|
+
),
|
|
707
|
+
inputSchema={
|
|
708
|
+
"type": "object",
|
|
709
|
+
"properties": {
|
|
710
|
+
"skill_name": {
|
|
711
|
+
"type": "string",
|
|
712
|
+
"description": "Name of the skill to get info for"
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
"required": ["skill_name"]
|
|
716
|
+
}
|
|
717
|
+
),
|
|
718
|
+
Tool(
|
|
719
|
+
name="run_skill",
|
|
720
|
+
description=(
|
|
721
|
+
"Execute a skill with the given input parameters. "
|
|
722
|
+
"Use list_skills to see available skills and "
|
|
723
|
+
"get_skill_info to understand required parameters. "
|
|
724
|
+
"IMPORTANT: If the skill has high-severity security issues, "
|
|
725
|
+
"you MUST show the security report to the user and ASK for their explicit confirmation "
|
|
726
|
+
"before setting confirmed=true. Do NOT auto-confirm without user approval."
|
|
727
|
+
),
|
|
728
|
+
inputSchema={
|
|
729
|
+
"type": "object",
|
|
730
|
+
"properties": {
|
|
731
|
+
"skill_name": {
|
|
732
|
+
"type": "string",
|
|
733
|
+
"description": "Name of the skill to execute"
|
|
734
|
+
},
|
|
735
|
+
"input": {
|
|
736
|
+
"type": "object",
|
|
737
|
+
"description": "Input parameters for the skill"
|
|
738
|
+
},
|
|
739
|
+
"confirmed": {
|
|
740
|
+
"type": "boolean",
|
|
741
|
+
"description": (
|
|
742
|
+
"Set to true ONLY after the user has explicitly approved execution. "
|
|
743
|
+
"You must ask the user for confirmation first."
|
|
744
|
+
)
|
|
745
|
+
},
|
|
746
|
+
"scan_id": {
|
|
747
|
+
"type": "string",
|
|
748
|
+
"description": "Scan ID from security review (required when confirmed=true)"
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
"required": ["skill_name"]
|
|
752
|
+
}
|
|
753
|
+
),
|
|
754
|
+
# Code execution tools
|
|
527
755
|
Tool(
|
|
528
756
|
name="scan_code",
|
|
529
757
|
description=(
|
|
@@ -551,8 +779,9 @@ class MCPServer:
|
|
|
551
779
|
name="execute_code",
|
|
552
780
|
description=(
|
|
553
781
|
"Execute code in a secure sandbox environment. "
|
|
554
|
-
"If security issues are found, you
|
|
555
|
-
"and
|
|
782
|
+
"IMPORTANT: If security issues are found, you MUST show the security report "
|
|
783
|
+
"to the user and ASK for their explicit confirmation before setting confirmed=true. "
|
|
784
|
+
"Do NOT auto-confirm without user approval."
|
|
556
785
|
),
|
|
557
786
|
inputSchema={
|
|
558
787
|
"type": "object",
|
|
@@ -569,8 +798,8 @@ class MCPServer:
|
|
|
569
798
|
"confirmed": {
|
|
570
799
|
"type": "boolean",
|
|
571
800
|
"description": (
|
|
572
|
-
"Set to true
|
|
573
|
-
"
|
|
801
|
+
"Set to true ONLY after the user has explicitly approved execution. "
|
|
802
|
+
"You must ask the user for confirmation first."
|
|
574
803
|
),
|
|
575
804
|
"default": False
|
|
576
805
|
},
|
|
@@ -595,13 +824,22 @@ class MCPServer:
|
|
|
595
824
|
}
|
|
596
825
|
)
|
|
597
826
|
]
|
|
827
|
+
return tools
|
|
598
828
|
|
|
599
829
|
@self.server.call_tool()
|
|
600
830
|
async def call_tool(
|
|
601
831
|
name: str,
|
|
602
832
|
arguments: Dict[str, Any]
|
|
603
833
|
) -> "CallToolResult":
|
|
604
|
-
|
|
834
|
+
# Skills management tools
|
|
835
|
+
if name == "list_skills":
|
|
836
|
+
return await self._handle_list_skills(arguments)
|
|
837
|
+
elif name == "get_skill_info":
|
|
838
|
+
return await self._handle_get_skill_info(arguments)
|
|
839
|
+
elif name == "run_skill":
|
|
840
|
+
return await self._handle_run_skill(arguments)
|
|
841
|
+
# Code execution tools
|
|
842
|
+
elif name == "scan_code":
|
|
605
843
|
return await self._handle_scan_code(arguments)
|
|
606
844
|
elif name == "execute_code":
|
|
607
845
|
return await self._handle_execute_code(arguments)
|
|
@@ -675,6 +913,18 @@ class MCPServer:
|
|
|
675
913
|
sandbox_level=sandbox_level,
|
|
676
914
|
)
|
|
677
915
|
|
|
916
|
+
# Handle hard blocked case - this is a definitive block, not a confirmation request
|
|
917
|
+
if result.get("hard_blocked"):
|
|
918
|
+
return CallToolResult(
|
|
919
|
+
isError=True,
|
|
920
|
+
content=[
|
|
921
|
+
TextContent(
|
|
922
|
+
type="text",
|
|
923
|
+
text=result["stderr"]
|
|
924
|
+
)
|
|
925
|
+
]
|
|
926
|
+
)
|
|
927
|
+
|
|
678
928
|
if result.get("requires_confirmation"):
|
|
679
929
|
return CallToolResult(
|
|
680
930
|
content=[
|
|
@@ -684,15 +934,15 @@ class MCPServer:
|
|
|
684
934
|
)
|
|
685
935
|
]
|
|
686
936
|
)
|
|
687
|
-
|
|
937
|
+
|
|
688
938
|
output_lines = []
|
|
689
939
|
if result["stdout"]:
|
|
690
940
|
output_lines.append(f"Output:\n{result['stdout']}")
|
|
691
941
|
if result["stderr"]:
|
|
692
942
|
output_lines.append(f"Errors:\n{result['stderr']}")
|
|
693
|
-
|
|
943
|
+
|
|
694
944
|
output_text = "\n".join(output_lines) if output_lines else "Execution completed successfully (no output)"
|
|
695
|
-
|
|
945
|
+
|
|
696
946
|
if result["success"]:
|
|
697
947
|
return CallToolResult(
|
|
698
948
|
content=[
|
|
@@ -712,7 +962,245 @@ class MCPServer:
|
|
|
712
962
|
)
|
|
713
963
|
]
|
|
714
964
|
)
|
|
715
|
-
|
|
965
|
+
|
|
966
|
+
async def _handle_list_skills(self, arguments: Dict[str, Any]) -> "CallToolResult":
|
|
967
|
+
"""Handle list_skills tool call."""
|
|
968
|
+
if not self.skill_manager:
|
|
969
|
+
return CallToolResult(
|
|
970
|
+
content=[
|
|
971
|
+
TextContent(
|
|
972
|
+
type="text",
|
|
973
|
+
text=f"No skills available. Skills directory not found: {self.skills_dir}"
|
|
974
|
+
)
|
|
975
|
+
]
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
skills = self.skill_manager.list_skills()
|
|
979
|
+
if not skills:
|
|
980
|
+
return CallToolResult(
|
|
981
|
+
content=[
|
|
982
|
+
TextContent(
|
|
983
|
+
type="text",
|
|
984
|
+
text=f"No skills found in directory: {self.skills_dir}"
|
|
985
|
+
)
|
|
986
|
+
]
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
# Format skills list
|
|
990
|
+
lines = ["Available Skills:", ""]
|
|
991
|
+
for skill in skills:
|
|
992
|
+
lines.append(f"• **{skill.name}**")
|
|
993
|
+
if skill.description:
|
|
994
|
+
lines.append(f" {skill.description}")
|
|
995
|
+
if skill.language:
|
|
996
|
+
lines.append(f" Language: {skill.language}")
|
|
997
|
+
lines.append("")
|
|
998
|
+
|
|
999
|
+
return CallToolResult(
|
|
1000
|
+
content=[
|
|
1001
|
+
TextContent(
|
|
1002
|
+
type="text",
|
|
1003
|
+
text="\n".join(lines)
|
|
1004
|
+
)
|
|
1005
|
+
]
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
async def _handle_get_skill_info(self, arguments: Dict[str, Any]) -> "CallToolResult":
|
|
1009
|
+
"""Handle get_skill_info tool call."""
|
|
1010
|
+
skill_name = arguments.get("skill_name")
|
|
1011
|
+
|
|
1012
|
+
if not skill_name:
|
|
1013
|
+
return CallToolResult(
|
|
1014
|
+
isError=True,
|
|
1015
|
+
content=[
|
|
1016
|
+
TextContent(
|
|
1017
|
+
type="text",
|
|
1018
|
+
text="Missing required argument: skill_name"
|
|
1019
|
+
)
|
|
1020
|
+
]
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
if not self.skill_manager:
|
|
1024
|
+
return CallToolResult(
|
|
1025
|
+
isError=True,
|
|
1026
|
+
content=[
|
|
1027
|
+
TextContent(
|
|
1028
|
+
type="text",
|
|
1029
|
+
text=f"No skills available. Skills directory not found: {self.skills_dir}"
|
|
1030
|
+
)
|
|
1031
|
+
]
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
skill = self.skill_manager.get_skill(skill_name)
|
|
1035
|
+
if not skill:
|
|
1036
|
+
available = ", ".join(self.skill_manager.skill_names()) or "none"
|
|
1037
|
+
return CallToolResult(
|
|
1038
|
+
isError=True,
|
|
1039
|
+
content=[
|
|
1040
|
+
TextContent(
|
|
1041
|
+
type="text",
|
|
1042
|
+
text=f"Skill not found: {skill_name}\nAvailable skills: {available}"
|
|
1043
|
+
)
|
|
1044
|
+
]
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
# Format skill info
|
|
1048
|
+
lines = [f"# {skill.name}", ""]
|
|
1049
|
+
if skill.description:
|
|
1050
|
+
lines.append(skill.description)
|
|
1051
|
+
lines.append("")
|
|
1052
|
+
|
|
1053
|
+
lines.append("## Details")
|
|
1054
|
+
lines.append(f"- **Language**: {skill.language or 'unknown'}")
|
|
1055
|
+
lines.append(f"- **Path**: {skill.path}")
|
|
1056
|
+
if skill.entry_point:
|
|
1057
|
+
lines.append(f"- **Entry Point**: {skill.entry_point}")
|
|
1058
|
+
|
|
1059
|
+
# Add input schema if available
|
|
1060
|
+
if skill.input_schema:
|
|
1061
|
+
lines.append("")
|
|
1062
|
+
lines.append("## Input Schema")
|
|
1063
|
+
lines.append("```json")
|
|
1064
|
+
lines.append(json.dumps(skill.input_schema, indent=2))
|
|
1065
|
+
lines.append("```")
|
|
1066
|
+
|
|
1067
|
+
return CallToolResult(
|
|
1068
|
+
content=[
|
|
1069
|
+
TextContent(
|
|
1070
|
+
type="text",
|
|
1071
|
+
text="\n".join(lines)
|
|
1072
|
+
)
|
|
1073
|
+
]
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
async def _handle_run_skill(self, arguments: Dict[str, Any]) -> "CallToolResult":
|
|
1077
|
+
"""
|
|
1078
|
+
Handle run_skill tool call using UnifiedExecutionService.
|
|
1079
|
+
|
|
1080
|
+
This method uses the unified execution layer which:
|
|
1081
|
+
1. Reads sandbox level at runtime
|
|
1082
|
+
2. Handles security scanning
|
|
1083
|
+
3. Properly downgrades sandbox level after confirmation
|
|
1084
|
+
"""
|
|
1085
|
+
skill_name = arguments.get("skill_name")
|
|
1086
|
+
input_data = arguments.get("input", {})
|
|
1087
|
+
confirmed = arguments.get("confirmed", False)
|
|
1088
|
+
scan_id = arguments.get("scan_id")
|
|
1089
|
+
|
|
1090
|
+
if not skill_name:
|
|
1091
|
+
return CallToolResult(
|
|
1092
|
+
isError=True,
|
|
1093
|
+
content=[
|
|
1094
|
+
TextContent(
|
|
1095
|
+
type="text",
|
|
1096
|
+
text="Missing required argument: skill_name"
|
|
1097
|
+
)
|
|
1098
|
+
]
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
if not self.skill_manager:
|
|
1102
|
+
return CallToolResult(
|
|
1103
|
+
isError=True,
|
|
1104
|
+
content=[
|
|
1105
|
+
TextContent(
|
|
1106
|
+
type="text",
|
|
1107
|
+
text=f"No skills available. Skills directory not found: {self.skills_dir}"
|
|
1108
|
+
)
|
|
1109
|
+
]
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
if not self.skill_manager.has_skill(skill_name):
|
|
1113
|
+
available = ", ".join(self.skill_manager.skill_names()) or "none"
|
|
1114
|
+
return CallToolResult(
|
|
1115
|
+
isError=True,
|
|
1116
|
+
content=[
|
|
1117
|
+
TextContent(
|
|
1118
|
+
type="text",
|
|
1119
|
+
text=f"Skill not found: {skill_name}\nAvailable skills: {available}"
|
|
1120
|
+
)
|
|
1121
|
+
]
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
# Get skill info
|
|
1125
|
+
skill = self.skill_manager.get_skill(skill_name)
|
|
1126
|
+
if not skill:
|
|
1127
|
+
return CallToolResult(
|
|
1128
|
+
isError=True,
|
|
1129
|
+
content=[
|
|
1130
|
+
TextContent(
|
|
1131
|
+
type="text",
|
|
1132
|
+
text=f"Could not load skill: {skill_name}"
|
|
1133
|
+
)
|
|
1134
|
+
]
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
# Use UnifiedExecutionService
|
|
1138
|
+
from ..sandbox.execution_service import UnifiedExecutionService
|
|
1139
|
+
from ..sandbox.context import ExecutionContext
|
|
1140
|
+
|
|
1141
|
+
service = UnifiedExecutionService.get_instance()
|
|
1142
|
+
|
|
1143
|
+
# MCP uses async confirmation pattern (return report -> client calls back with confirmed=True)
|
|
1144
|
+
# Create a "callback" that captures the scan result for MCP's async flow
|
|
1145
|
+
scan_result_holder = {"result": None}
|
|
1146
|
+
|
|
1147
|
+
def mcp_confirmation_callback(report: str, scan_id_from_scan: str) -> bool:
|
|
1148
|
+
# If client already confirmed, return True
|
|
1149
|
+
if confirmed:
|
|
1150
|
+
return True
|
|
1151
|
+
# Otherwise, store the result and return False to abort execution
|
|
1152
|
+
# MCP will then return the report to the client
|
|
1153
|
+
scan_result_holder["result"] = {"report": report, "scan_id": scan_id_from_scan}
|
|
1154
|
+
return False
|
|
1155
|
+
|
|
1156
|
+
# Execute using unified service
|
|
1157
|
+
result = service.execute_skill(
|
|
1158
|
+
skill_info=skill,
|
|
1159
|
+
input_data=input_data,
|
|
1160
|
+
confirmation_callback=mcp_confirmation_callback if not confirmed else lambda r, s: True,
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
# Check if we need to return a security report (MCP async confirmation pattern)
|
|
1164
|
+
if scan_result_holder["result"] is not None:
|
|
1165
|
+
report_data = scan_result_holder["result"]
|
|
1166
|
+
return CallToolResult(
|
|
1167
|
+
content=[
|
|
1168
|
+
TextContent(
|
|
1169
|
+
type="text",
|
|
1170
|
+
text=(
|
|
1171
|
+
f"🔐 Security Review Required for skill '{skill_name}'\n\n"
|
|
1172
|
+
f"{report_data['report']}\n\n"
|
|
1173
|
+
f"⚠️ IMPORTANT: You MUST ask the user for confirmation before proceeding.\n"
|
|
1174
|
+
f"Show this security report to the user and wait for their explicit approval.\n\n"
|
|
1175
|
+
f"If the user approves, call run_skill again with:\n"
|
|
1176
|
+
f" - confirmed: true\n"
|
|
1177
|
+
f" - scan_id: \"{report_data['scan_id']}\"\n"
|
|
1178
|
+
)
|
|
1179
|
+
)
|
|
1180
|
+
]
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
# Return execution result
|
|
1184
|
+
if result.success:
|
|
1185
|
+
return CallToolResult(
|
|
1186
|
+
content=[
|
|
1187
|
+
TextContent(
|
|
1188
|
+
type="text",
|
|
1189
|
+
text=f"Skill '{skill_name}' executed successfully.\n\nOutput:\n{result.output}"
|
|
1190
|
+
)
|
|
1191
|
+
]
|
|
1192
|
+
)
|
|
1193
|
+
else:
|
|
1194
|
+
return CallToolResult(
|
|
1195
|
+
isError=True,
|
|
1196
|
+
content=[
|
|
1197
|
+
TextContent(
|
|
1198
|
+
type="text",
|
|
1199
|
+
text=f"Skill '{skill_name}' execution failed.\n\nError:\n{result.error}"
|
|
1200
|
+
)
|
|
1201
|
+
]
|
|
1202
|
+
)
|
|
1203
|
+
|
|
716
1204
|
async def run(self):
|
|
717
1205
|
"""Run the MCP server."""
|
|
718
1206
|
async with stdio_server() as (read_stream, write_stream):
|