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/__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 +175 -13
- skilllite/core/manager.py +82 -15
- skilllite/core/metadata.py +14 -7
- skilllite/core/security.py +420 -0
- skilllite/mcp/server.py +266 -58
- 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.1.dist-info → skilllite-0.1.2.dist-info}/METADATA +98 -1
- {skilllite-0.1.1.dist-info → skilllite-0.1.2.dist-info}/RECORD +21 -14
- {skilllite-0.1.1.dist-info → skilllite-0.1.2.dist-info}/WHEEL +0 -0
- {skilllite-0.1.1.dist-info → skilllite-0.1.2.dist-info}/entry_points.txt +0 -0
- {skilllite-0.1.1.dist-info → skilllite-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {skilllite-0.1.1.dist-info → skilllite-0.1.2.dist-info}/top_level.txt +0 -0
skilllite/mcp/server.py
CHANGED
|
@@ -98,7 +98,27 @@ except ImportError:
|
|
|
98
98
|
|
|
99
99
|
class SecurityScanResult:
|
|
100
100
|
"""Result of a security scan."""
|
|
101
|
-
|
|
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
|
+
|
|
102
122
|
def __init__(
|
|
103
123
|
self,
|
|
104
124
|
is_safe: bool,
|
|
@@ -108,6 +128,7 @@ class SecurityScanResult:
|
|
|
108
128
|
high_severity_count: int = 0,
|
|
109
129
|
medium_severity_count: int = 0,
|
|
110
130
|
low_severity_count: int = 0,
|
|
131
|
+
sandbox_level: int = 3,
|
|
111
132
|
):
|
|
112
133
|
self.is_safe = is_safe
|
|
113
134
|
self.issues = issues
|
|
@@ -116,8 +137,31 @@ class SecurityScanResult:
|
|
|
116
137
|
self.high_severity_count = high_severity_count
|
|
117
138
|
self.medium_severity_count = medium_severity_count
|
|
118
139
|
self.low_severity_count = low_severity_count
|
|
140
|
+
self.sandbox_level = sandbox_level
|
|
119
141
|
self.timestamp = time.time()
|
|
120
|
-
|
|
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
|
+
|
|
121
165
|
def to_dict(self) -> Dict[str, Any]:
|
|
122
166
|
return {
|
|
123
167
|
"is_safe": self.is_safe,
|
|
@@ -127,42 +171,64 @@ class SecurityScanResult:
|
|
|
127
171
|
"high_severity_count": self.high_severity_count,
|
|
128
172
|
"medium_severity_count": self.medium_severity_count,
|
|
129
173
|
"low_severity_count": self.low_severity_count,
|
|
130
|
-
"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,
|
|
131
178
|
}
|
|
132
|
-
|
|
179
|
+
|
|
133
180
|
def format_report(self) -> str:
|
|
134
181
|
"""Format a human-readable security report."""
|
|
135
182
|
if not self.issues:
|
|
136
183
|
return "✅ Security scan passed. No issues found."
|
|
137
|
-
|
|
184
|
+
|
|
138
185
|
lines = [
|
|
139
186
|
f"📋 Security Scan Report (ID: {self.scan_id[:8]})",
|
|
187
|
+
f" Sandbox Level: L{self.sandbox_level}",
|
|
140
188
|
f" Found {len(self.issues)} item(s) for review:",
|
|
141
189
|
"",
|
|
142
190
|
]
|
|
143
|
-
|
|
191
|
+
|
|
144
192
|
severity_icons = {
|
|
145
193
|
"Critical": "🔴",
|
|
146
194
|
"High": "🟠",
|
|
147
195
|
"Medium": "🟡",
|
|
148
196
|
"Low": "🟢",
|
|
149
197
|
}
|
|
150
|
-
|
|
198
|
+
|
|
151
199
|
for idx, issue in enumerate(self.issues, 1):
|
|
152
200
|
severity = issue.get("severity", "Medium")
|
|
153
201
|
icon = severity_icons.get(severity, "⚪")
|
|
154
|
-
|
|
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}")
|
|
155
208
|
lines.append(f" ├─ Rule: {issue.get('rule_id', 'N/A')}")
|
|
156
209
|
lines.append(f" ├─ Line {issue.get('line_number', '?')}: {issue.get('description', '')}")
|
|
157
210
|
lines.append(f" └─ Code: {issue.get('code_snippet', '')[:60]}...")
|
|
158
211
|
lines.append("")
|
|
159
|
-
|
|
160
|
-
|
|
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:
|
|
161
227
|
lines.append("⚠️ High severity issues found. Confirmation required to execute.")
|
|
162
228
|
lines.append(f" To proceed, call execute_code with confirmed=true and scan_id=\"{self.scan_id}\"")
|
|
163
229
|
else:
|
|
164
230
|
lines.append("ℹ️ Only low/medium severity issues found. Safe to execute.")
|
|
165
|
-
|
|
231
|
+
|
|
166
232
|
return "\n".join(lines)
|
|
167
233
|
|
|
168
234
|
|
|
@@ -243,26 +309,37 @@ This skill executes code from MCP.
|
|
|
243
309
|
|
|
244
310
|
return skill_dir, code_file
|
|
245
311
|
|
|
246
|
-
def scan_code(self, language: str, code: str) -> SecurityScanResult:
|
|
247
|
-
"""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
|
+
|
|
248
324
|
if not self.runtime_available:
|
|
249
325
|
return SecurityScanResult(
|
|
250
326
|
is_safe=False,
|
|
251
|
-
issues=[{"severity": "Critical", "issue_type": "RuntimeError",
|
|
327
|
+
issues=[{"severity": "Critical", "issue_type": "RuntimeError",
|
|
252
328
|
"description": f"skillbox not found at {self.skillbox_path}",
|
|
253
329
|
"rule_id": "system", "line_number": 0, "code_snippet": ""}],
|
|
254
330
|
scan_id="error",
|
|
255
331
|
code_hash="",
|
|
256
332
|
high_severity_count=1,
|
|
333
|
+
sandbox_level=sandbox_level,
|
|
257
334
|
)
|
|
258
|
-
|
|
335
|
+
|
|
259
336
|
self._cleanup_expired_scans()
|
|
260
337
|
code_hash = self._generate_code_hash(language, code)
|
|
261
338
|
scan_id = self._generate_scan_id(code_hash)
|
|
262
|
-
|
|
339
|
+
|
|
263
340
|
try:
|
|
264
341
|
skill_dir, code_file = self._create_temp_skill(language, code)
|
|
265
|
-
|
|
342
|
+
|
|
266
343
|
try:
|
|
267
344
|
result = subprocess.run(
|
|
268
345
|
[self.skillbox_path, "security-scan", code_file],
|
|
@@ -270,12 +347,12 @@ This skill executes code from MCP.
|
|
|
270
347
|
text=True,
|
|
271
348
|
timeout=30
|
|
272
349
|
)
|
|
273
|
-
|
|
350
|
+
|
|
274
351
|
issues = self._parse_scan_output(result.stdout + result.stderr)
|
|
275
352
|
high_count = sum(1 for i in issues if i.get("severity") in ["Critical", "High"])
|
|
276
353
|
medium_count = sum(1 for i in issues if i.get("severity") == "Medium")
|
|
277
354
|
low_count = sum(1 for i in issues if i.get("severity") == "Low")
|
|
278
|
-
|
|
355
|
+
|
|
279
356
|
scan_result = SecurityScanResult(
|
|
280
357
|
is_safe=high_count == 0,
|
|
281
358
|
issues=issues,
|
|
@@ -284,14 +361,15 @@ This skill executes code from MCP.
|
|
|
284
361
|
high_severity_count=high_count,
|
|
285
362
|
medium_severity_count=medium_count,
|
|
286
363
|
low_severity_count=low_count,
|
|
364
|
+
sandbox_level=sandbox_level,
|
|
287
365
|
)
|
|
288
|
-
|
|
366
|
+
|
|
289
367
|
self._scan_cache[scan_id] = scan_result
|
|
290
368
|
return scan_result
|
|
291
|
-
|
|
369
|
+
|
|
292
370
|
finally:
|
|
293
371
|
shutil.rmtree(skill_dir, ignore_errors=True)
|
|
294
|
-
|
|
372
|
+
|
|
295
373
|
except subprocess.TimeoutExpired:
|
|
296
374
|
return SecurityScanResult(
|
|
297
375
|
is_safe=False,
|
|
@@ -301,6 +379,7 @@ This skill executes code from MCP.
|
|
|
301
379
|
scan_id=scan_id,
|
|
302
380
|
code_hash=code_hash,
|
|
303
381
|
high_severity_count=1,
|
|
382
|
+
sandbox_level=sandbox_level,
|
|
304
383
|
)
|
|
305
384
|
except Exception as e:
|
|
306
385
|
return SecurityScanResult(
|
|
@@ -311,6 +390,7 @@ This skill executes code from MCP.
|
|
|
311
390
|
scan_id=scan_id,
|
|
312
391
|
code_hash=code_hash,
|
|
313
392
|
high_severity_count=1,
|
|
393
|
+
sandbox_level=sandbox_level,
|
|
314
394
|
)
|
|
315
395
|
|
|
316
396
|
def _parse_scan_output(self, output: str) -> List[Dict[str, Any]]:
|
|
@@ -412,9 +492,31 @@ This skill executes code from MCP.
|
|
|
412
492
|
}
|
|
413
493
|
|
|
414
494
|
code_hash = self._generate_code_hash(language, code)
|
|
415
|
-
|
|
495
|
+
|
|
416
496
|
if sandbox_level >= 3 and not confirmed:
|
|
417
|
-
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
|
|
418
520
|
if scan_result.high_severity_count > 0:
|
|
419
521
|
return {
|
|
420
522
|
"success": False,
|
|
@@ -422,7 +524,9 @@ This skill executes code from MCP.
|
|
|
422
524
|
"stderr": (
|
|
423
525
|
f"🔐 Security Review Required\n\n"
|
|
424
526
|
f"{scan_result.format_report()}\n\n"
|
|
425
|
-
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"
|
|
426
530
|
f" - confirmed: true\n"
|
|
427
531
|
f" - scan_id: \"{scan_result.scan_id}\"\n"
|
|
428
532
|
),
|
|
@@ -431,7 +535,7 @@ This skill executes code from MCP.
|
|
|
431
535
|
"scan_id": scan_result.scan_id,
|
|
432
536
|
"security_issues": scan_result.to_dict(),
|
|
433
537
|
}
|
|
434
|
-
|
|
538
|
+
|
|
435
539
|
if confirmed and scan_id:
|
|
436
540
|
cached_result = self.verify_scan(scan_id, code_hash)
|
|
437
541
|
if not cached_result:
|
|
@@ -444,6 +548,28 @@ This skill executes code from MCP.
|
|
|
444
548
|
),
|
|
445
549
|
"exit_code": 3,
|
|
446
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
|
+
}
|
|
447
573
|
|
|
448
574
|
try:
|
|
449
575
|
skill_dir, _ = self._create_temp_skill(language, code)
|
|
@@ -594,7 +720,10 @@ class MCPServer:
|
|
|
594
720
|
description=(
|
|
595
721
|
"Execute a skill with the given input parameters. "
|
|
596
722
|
"Use list_skills to see available skills and "
|
|
597
|
-
"get_skill_info to understand required parameters."
|
|
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."
|
|
598
727
|
),
|
|
599
728
|
inputSchema={
|
|
600
729
|
"type": "object",
|
|
@@ -606,6 +735,17 @@ class MCPServer:
|
|
|
606
735
|
"input": {
|
|
607
736
|
"type": "object",
|
|
608
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)"
|
|
609
749
|
}
|
|
610
750
|
},
|
|
611
751
|
"required": ["skill_name"]
|
|
@@ -639,8 +779,9 @@ class MCPServer:
|
|
|
639
779
|
name="execute_code",
|
|
640
780
|
description=(
|
|
641
781
|
"Execute code in a secure sandbox environment. "
|
|
642
|
-
"If security issues are found, you
|
|
643
|
-
"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."
|
|
644
785
|
),
|
|
645
786
|
inputSchema={
|
|
646
787
|
"type": "object",
|
|
@@ -657,8 +798,8 @@ class MCPServer:
|
|
|
657
798
|
"confirmed": {
|
|
658
799
|
"type": "boolean",
|
|
659
800
|
"description": (
|
|
660
|
-
"Set to true
|
|
661
|
-
"
|
|
801
|
+
"Set to true ONLY after the user has explicitly approved execution. "
|
|
802
|
+
"You must ask the user for confirmation first."
|
|
662
803
|
),
|
|
663
804
|
"default": False
|
|
664
805
|
},
|
|
@@ -772,6 +913,18 @@ class MCPServer:
|
|
|
772
913
|
sandbox_level=sandbox_level,
|
|
773
914
|
)
|
|
774
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
|
+
|
|
775
928
|
if result.get("requires_confirmation"):
|
|
776
929
|
return CallToolResult(
|
|
777
930
|
content=[
|
|
@@ -781,15 +934,15 @@ class MCPServer:
|
|
|
781
934
|
)
|
|
782
935
|
]
|
|
783
936
|
)
|
|
784
|
-
|
|
937
|
+
|
|
785
938
|
output_lines = []
|
|
786
939
|
if result["stdout"]:
|
|
787
940
|
output_lines.append(f"Output:\n{result['stdout']}")
|
|
788
941
|
if result["stderr"]:
|
|
789
942
|
output_lines.append(f"Errors:\n{result['stderr']}")
|
|
790
|
-
|
|
943
|
+
|
|
791
944
|
output_text = "\n".join(output_lines) if output_lines else "Execution completed successfully (no output)"
|
|
792
|
-
|
|
945
|
+
|
|
793
946
|
if result["success"]:
|
|
794
947
|
return CallToolResult(
|
|
795
948
|
content=[
|
|
@@ -921,9 +1074,18 @@ class MCPServer:
|
|
|
921
1074
|
)
|
|
922
1075
|
|
|
923
1076
|
async def _handle_run_skill(self, arguments: Dict[str, Any]) -> "CallToolResult":
|
|
924
|
-
"""
|
|
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
|
+
"""
|
|
925
1085
|
skill_name = arguments.get("skill_name")
|
|
926
1086
|
input_data = arguments.get("input", {})
|
|
1087
|
+
confirmed = arguments.get("confirmed", False)
|
|
1088
|
+
scan_id = arguments.get("scan_id")
|
|
927
1089
|
|
|
928
1090
|
if not skill_name:
|
|
929
1091
|
return CallToolResult(
|
|
@@ -959,36 +1121,82 @@ class MCPServer:
|
|
|
959
1121
|
]
|
|
960
1122
|
)
|
|
961
1123
|
|
|
962
|
-
#
|
|
963
|
-
|
|
964
|
-
|
|
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
|
+
)
|
|
965
1136
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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"
|
|
982
1178
|
)
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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:
|
|
986
1194
|
return CallToolResult(
|
|
987
1195
|
isError=True,
|
|
988
1196
|
content=[
|
|
989
1197
|
TextContent(
|
|
990
1198
|
type="text",
|
|
991
|
-
text=f"
|
|
1199
|
+
text=f"Skill '{skill_name}' execution failed.\n\nError:\n{result.error}"
|
|
992
1200
|
)
|
|
993
1201
|
]
|
|
994
1202
|
)
|
skilllite/quick.py
CHANGED
|
@@ -92,7 +92,8 @@ class SkillRunner:
|
|
|
92
92
|
allow_network: Optional[bool] = None,
|
|
93
93
|
enable_sandbox: Optional[bool] = None,
|
|
94
94
|
execution_timeout: Optional[int] = None,
|
|
95
|
-
max_memory_mb: Optional[int] = None
|
|
95
|
+
max_memory_mb: Optional[int] = None,
|
|
96
|
+
confirmation_callback: Optional[Callable[[str, str], bool]] = None
|
|
96
97
|
):
|
|
97
98
|
"""
|
|
98
99
|
Initialize SkillRunner.
|
|
@@ -122,6 +123,9 @@ class SkillRunner:
|
|
|
122
123
|
enable_sandbox: Whether to enable sandbox protection (defaults from .env or True)
|
|
123
124
|
execution_timeout: Skill execution timeout in seconds (defaults from .env or 120)
|
|
124
125
|
max_memory_mb: Maximum memory limit in MB (defaults from .env or 512)
|
|
126
|
+
confirmation_callback: Callback for security confirmation when sandbox_level=3.
|
|
127
|
+
Signature: (security_report: str, scan_id: str) -> bool
|
|
128
|
+
If None and sandbox_level=3, will use interactive terminal confirmation.
|
|
125
129
|
"""
|
|
126
130
|
# Load .env
|
|
127
131
|
load_env(env_file)
|
|
@@ -161,7 +165,11 @@ class SkillRunner:
|
|
|
161
165
|
|
|
162
166
|
self.custom_tool_executor = custom_tool_executor
|
|
163
167
|
self.use_enhanced_loop = use_enhanced_loop
|
|
164
|
-
|
|
168
|
+
|
|
169
|
+
# Security confirmation callback
|
|
170
|
+
# If None and sandbox_level=3, will use interactive terminal confirmation
|
|
171
|
+
self.confirmation_callback = confirmation_callback
|
|
172
|
+
|
|
165
173
|
# Lazy initialization
|
|
166
174
|
self._client = None
|
|
167
175
|
self._manager = None
|
|
@@ -282,7 +290,8 @@ Example of CORRECT approach:
|
|
|
282
290
|
model=self.model,
|
|
283
291
|
max_iterations=self.max_iterations,
|
|
284
292
|
custom_tools=self.custom_tools if self.custom_tools else None,
|
|
285
|
-
custom_tool_executor=tool_executor
|
|
293
|
+
custom_tool_executor=tool_executor,
|
|
294
|
+
confirmation_callback=self.confirmation_callback
|
|
286
295
|
)
|
|
287
296
|
else:
|
|
288
297
|
# Use basic AgenticLoop (backward compatible)
|
|
@@ -290,7 +299,8 @@ Example of CORRECT approach:
|
|
|
290
299
|
client=self.client,
|
|
291
300
|
model=self.model,
|
|
292
301
|
system_prompt=self.system_context,
|
|
293
|
-
max_iterations=self.max_iterations
|
|
302
|
+
max_iterations=self.max_iterations,
|
|
303
|
+
confirmation_callback=self.confirmation_callback
|
|
294
304
|
)
|
|
295
305
|
|
|
296
306
|
response = loop.run(user_message)
|