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.
- skilllite/__init__.py +159 -0
- skilllite/analyzer.py +391 -0
- skilllite/builtin_tools.py +240 -0
- skilllite/cli.py +217 -0
- skilllite/core/__init__.py +65 -0
- skilllite/core/executor.py +182 -0
- skilllite/core/handler.py +332 -0
- skilllite/core/loops.py +770 -0
- skilllite/core/manager.py +507 -0
- skilllite/core/metadata.py +338 -0
- skilllite/core/prompt_builder.py +321 -0
- skilllite/core/registry.py +185 -0
- skilllite/core/skill_info.py +181 -0
- skilllite/core/tool_builder.py +338 -0
- skilllite/core/tools.py +253 -0
- skilllite/mcp/__init__.py +45 -0
- skilllite/mcp/server.py +734 -0
- skilllite/quick.py +420 -0
- skilllite/sandbox/__init__.py +36 -0
- skilllite/sandbox/base.py +93 -0
- skilllite/sandbox/config.py +229 -0
- skilllite/sandbox/skillbox/__init__.py +44 -0
- skilllite/sandbox/skillbox/binary.py +421 -0
- skilllite/sandbox/skillbox/executor.py +608 -0
- skilllite/sandbox/utils.py +77 -0
- skilllite/validation.py +137 -0
- skilllite-0.1.0.dist-info/METADATA +293 -0
- skilllite-0.1.0.dist-info/RECORD +32 -0
- skilllite-0.1.0.dist-info/WHEEL +5 -0
- skilllite-0.1.0.dist-info/entry_points.txt +3 -0
- skilllite-0.1.0.dist-info/licenses/LICENSE +21 -0
- skilllite-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skillbox executor - interfaces with the Rust skillbox binary.
|
|
3
|
+
|
|
4
|
+
This module provides the SkillboxExecutor class that implements the
|
|
5
|
+
SandboxExecutor interface using the Rust-based skillbox sandbox.
|
|
6
|
+
|
|
7
|
+
This is the canonical implementation of skill execution with sandbox support.
|
|
8
|
+
The core/executor.py module delegates to this class.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from ..base import SandboxExecutor, ExecutionResult
|
|
21
|
+
from ..config import (
|
|
22
|
+
SandboxConfig,
|
|
23
|
+
DEFAULT_EXECUTION_TIMEOUT,
|
|
24
|
+
DEFAULT_MAX_MEMORY_MB,
|
|
25
|
+
DEFAULT_SANDBOX_LEVEL,
|
|
26
|
+
DEFAULT_ALLOW_NETWORK,
|
|
27
|
+
DEFAULT_ENABLE_SANDBOX,
|
|
28
|
+
)
|
|
29
|
+
from ..utils import convert_json_to_cli_args
|
|
30
|
+
from .binary import find_binary, ensure_installed
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SkillboxExecutor(SandboxExecutor):
|
|
34
|
+
"""
|
|
35
|
+
Executes skills using the skillbox binary.
|
|
36
|
+
|
|
37
|
+
This class provides a Python interface to the Rust-based sandbox executor.
|
|
38
|
+
Supports both traditional skill execution (via entry_point in SKILL.md) and
|
|
39
|
+
direct script execution (via exec command).
|
|
40
|
+
|
|
41
|
+
Features:
|
|
42
|
+
- Sandbox security levels (1/2/3)
|
|
43
|
+
- Configurable resource limits (memory, timeout)
|
|
44
|
+
- Level 3 user interaction support for authorization prompts
|
|
45
|
+
- Memory monitoring with psutil (optional)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
binary_path: Optional[str] = None,
|
|
51
|
+
cache_dir: Optional[str] = None,
|
|
52
|
+
allow_network: bool = False,
|
|
53
|
+
enable_sandbox: bool = True,
|
|
54
|
+
execution_timeout: Optional[int] = None,
|
|
55
|
+
max_memory_mb: Optional[int] = None,
|
|
56
|
+
sandbox_level: Optional[str] = None,
|
|
57
|
+
auto_install: bool = False
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initialize the executor.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
binary_path: Path to the skillbox binary. If None, auto-detect.
|
|
64
|
+
cache_dir: Directory for caching virtual environments.
|
|
65
|
+
allow_network: Whether to allow network access by default.
|
|
66
|
+
enable_sandbox: Whether to enable sandbox protection (default: True).
|
|
67
|
+
execution_timeout: Skill execution timeout in seconds (default: 120).
|
|
68
|
+
max_memory_mb: Maximum memory limit in MB (default: 512).
|
|
69
|
+
sandbox_level: Sandbox security level (1/2/3, default from env or 3).
|
|
70
|
+
auto_install: Automatically download and install binary if not found.
|
|
71
|
+
"""
|
|
72
|
+
# Load configuration with environment variable fallbacks
|
|
73
|
+
self._config = SandboxConfig(
|
|
74
|
+
binary_path=binary_path,
|
|
75
|
+
cache_dir=cache_dir,
|
|
76
|
+
allow_network=allow_network if allow_network else DEFAULT_ALLOW_NETWORK,
|
|
77
|
+
enable_sandbox=enable_sandbox if enable_sandbox else DEFAULT_ENABLE_SANDBOX,
|
|
78
|
+
execution_timeout=execution_timeout if execution_timeout is not None else DEFAULT_EXECUTION_TIMEOUT,
|
|
79
|
+
max_memory_mb=max_memory_mb if max_memory_mb is not None else DEFAULT_MAX_MEMORY_MB,
|
|
80
|
+
sandbox_level=sandbox_level if sandbox_level is not None else DEFAULT_SANDBOX_LEVEL,
|
|
81
|
+
auto_install=auto_install,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Set instance attributes from config
|
|
85
|
+
self.binary_path = self._config.binary_path or self._find_binary(auto_install)
|
|
86
|
+
self.cache_dir = self._config.cache_dir
|
|
87
|
+
self.allow_network = self._config.allow_network
|
|
88
|
+
self.enable_sandbox = self._config.enable_sandbox
|
|
89
|
+
self.execution_timeout = self._config.execution_timeout
|
|
90
|
+
self.max_memory_mb = self._config.max_memory_mb
|
|
91
|
+
self.sandbox_level = self._config.sandbox_level
|
|
92
|
+
|
|
93
|
+
def _find_binary(self, auto_install: bool = False) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Find the skillbox binary.
|
|
96
|
+
|
|
97
|
+
Uses the binary module to search for the binary in standard locations.
|
|
98
|
+
If auto_install is True, will download and install if not found.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
auto_install: Automatically install if not found.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Path to the binary.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
FileNotFoundError: If binary not found and auto_install is False.
|
|
108
|
+
PermissionError: If permission denied when accessing binary.
|
|
109
|
+
RuntimeError: If failed to find or install binary.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
return ensure_installed(auto_install=auto_install, show_progress=True)
|
|
113
|
+
except FileNotFoundError as e:
|
|
114
|
+
raise FileNotFoundError(
|
|
115
|
+
f"Skillbox binary not found. Please run 'cargo build --release' in skillbox/ "
|
|
116
|
+
f"directory, or set SKILLBOX_BINARY_PATH environment variable to the binary path. "
|
|
117
|
+
f"Original error: {e}"
|
|
118
|
+
) from e
|
|
119
|
+
except PermissionError as e:
|
|
120
|
+
raise PermissionError(
|
|
121
|
+
f"Permission denied when accessing skillbox binary. "
|
|
122
|
+
f"Please check file permissions. Original error: {e}"
|
|
123
|
+
) from e
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
f"Failed to find or install skillbox binary: {e}. "
|
|
127
|
+
f"Please ensure Rust is installed and run 'cargo build --release' in skillbox/ directory."
|
|
128
|
+
) from e
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def is_available(self) -> bool:
|
|
132
|
+
"""Check if skillbox is available and ready to use."""
|
|
133
|
+
if not self.binary_path:
|
|
134
|
+
return False
|
|
135
|
+
return os.path.exists(self.binary_path) and os.access(self.binary_path, os.X_OK)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def name(self) -> str:
|
|
139
|
+
"""Return the name of this sandbox implementation."""
|
|
140
|
+
return "skillbox"
|
|
141
|
+
|
|
142
|
+
def _convert_json_to_cli_args(self, input_data: Dict[str, Any]) -> List[str]:
|
|
143
|
+
"""
|
|
144
|
+
Convert JSON input data to command line arguments list.
|
|
145
|
+
|
|
146
|
+
Delegates to the shared utility function in sandbox.utils.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
input_data: JSON input data from LLM
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of command line arguments
|
|
153
|
+
"""
|
|
154
|
+
return convert_json_to_cli_args(input_data)
|
|
155
|
+
|
|
156
|
+
def _build_skill_env(self, skill_dir: Path, timeout: Optional[int] = None) -> Dict[str, str]:
|
|
157
|
+
"""
|
|
158
|
+
Build environment variables for skill execution.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
skill_dir: Path to the skill directory
|
|
162
|
+
timeout: Optional timeout override
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Environment dictionary
|
|
166
|
+
"""
|
|
167
|
+
return {
|
|
168
|
+
**os.environ,
|
|
169
|
+
"PYTHONUNBUFFERED": "1",
|
|
170
|
+
"SKILL_DIR": str(skill_dir),
|
|
171
|
+
"SKILL_ASSETS_DIR": str(skill_dir / "assets"),
|
|
172
|
+
"SKILL_REFERENCES_DIR": str(skill_dir / "references"),
|
|
173
|
+
"SKILL_SCRIPTS_DIR": str(skill_dir / "scripts"),
|
|
174
|
+
"SKILLBOX_SANDBOX_LEVEL": self.sandbox_level,
|
|
175
|
+
"SKILLBOX_MAX_MEMORY_MB": str(self.max_memory_mb),
|
|
176
|
+
"SKILLBOX_TIMEOUT_SECS": str(timeout if timeout is not None else self.execution_timeout),
|
|
177
|
+
"SKILLBOX_AUTO_APPROVE": os.environ.get("SKILLBOX_AUTO_APPROVE", ""),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def _parse_output(self, stdout: str, stderr: str, returncode: int) -> ExecutionResult:
|
|
181
|
+
"""
|
|
182
|
+
Parse subprocess output into ExecutionResult.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
stdout: Standard output
|
|
186
|
+
stderr: Standard error
|
|
187
|
+
returncode: Process return code
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
ExecutionResult
|
|
191
|
+
"""
|
|
192
|
+
if returncode == 0:
|
|
193
|
+
try:
|
|
194
|
+
output = json.loads(stdout.strip())
|
|
195
|
+
return ExecutionResult(
|
|
196
|
+
success=True,
|
|
197
|
+
output=output,
|
|
198
|
+
exit_code=returncode,
|
|
199
|
+
stdout=stdout,
|
|
200
|
+
stderr=stderr
|
|
201
|
+
)
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
# Script output is not JSON, return as raw text
|
|
204
|
+
return ExecutionResult(
|
|
205
|
+
success=True,
|
|
206
|
+
output={"raw_output": stdout.strip()},
|
|
207
|
+
exit_code=returncode,
|
|
208
|
+
stdout=stdout,
|
|
209
|
+
stderr=stderr
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
return ExecutionResult(
|
|
213
|
+
success=False,
|
|
214
|
+
error=stderr or f"Exit code: {returncode}",
|
|
215
|
+
exit_code=returncode,
|
|
216
|
+
stdout=stdout,
|
|
217
|
+
stderr=stderr
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def exec_script(
|
|
221
|
+
self,
|
|
222
|
+
skill_dir: Path,
|
|
223
|
+
script_path: str,
|
|
224
|
+
input_data: Dict[str, Any],
|
|
225
|
+
args: Optional[list] = None,
|
|
226
|
+
allow_network: Optional[bool] = None,
|
|
227
|
+
timeout: Optional[int] = None,
|
|
228
|
+
enable_sandbox: Optional[bool] = None
|
|
229
|
+
) -> ExecutionResult:
|
|
230
|
+
"""
|
|
231
|
+
Execute a specific script directly.
|
|
232
|
+
|
|
233
|
+
This method allows executing any script in the skill directory without
|
|
234
|
+
requiring an entry_point in SKILL.md. Useful for skills with multiple
|
|
235
|
+
scripts or prompt-only skills with helper scripts.
|
|
236
|
+
|
|
237
|
+
Note: Due to skillbox's --args parameter not correctly parsing arguments,
|
|
238
|
+
we execute Python scripts directly using subprocess for CLI-style scripts.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
skill_dir: Path to the skill directory
|
|
242
|
+
script_path: Relative path to the script (e.g., "scripts/init_skill.py")
|
|
243
|
+
input_data: Input data for the script. For CLI scripts using argparse,
|
|
244
|
+
this will be automatically converted to command line arguments.
|
|
245
|
+
args: Optional command line arguments list (overrides auto-conversion)
|
|
246
|
+
allow_network: Override default network setting
|
|
247
|
+
timeout: Execution timeout in seconds
|
|
248
|
+
enable_sandbox: Override default sandbox setting
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
ExecutionResult with the output or error
|
|
252
|
+
"""
|
|
253
|
+
if allow_network is None:
|
|
254
|
+
allow_network = self.allow_network
|
|
255
|
+
|
|
256
|
+
if enable_sandbox is None:
|
|
257
|
+
enable_sandbox = self.enable_sandbox
|
|
258
|
+
|
|
259
|
+
# Convert JSON input to CLI args if no explicit args provided
|
|
260
|
+
if args is None and input_data:
|
|
261
|
+
args = self._convert_json_to_cli_args(input_data)
|
|
262
|
+
|
|
263
|
+
full_script_path = skill_dir / script_path
|
|
264
|
+
|
|
265
|
+
# Check sandbox level - Level 3 requires skillbox for security scanning
|
|
266
|
+
# Level 1 and 2 can use direct execution for better performance
|
|
267
|
+
if script_path.endswith('.py') and self.sandbox_level != "3":
|
|
268
|
+
return self._exec_python_script_direct(
|
|
269
|
+
skill_dir, full_script_path, input_data, args, timeout
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# For other languages or Level 3, use skillbox exec
|
|
273
|
+
cmd = [
|
|
274
|
+
self.binary_path,
|
|
275
|
+
"exec",
|
|
276
|
+
str(skill_dir),
|
|
277
|
+
script_path,
|
|
278
|
+
json.dumps(input_data),
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
if args:
|
|
282
|
+
args_str = " ".join(args) if isinstance(args, list) else args
|
|
283
|
+
cmd.extend(["--args", args_str])
|
|
284
|
+
|
|
285
|
+
if allow_network:
|
|
286
|
+
cmd.append("--allow-network")
|
|
287
|
+
|
|
288
|
+
if enable_sandbox:
|
|
289
|
+
cmd.append("--enable-sandbox")
|
|
290
|
+
|
|
291
|
+
if self.cache_dir:
|
|
292
|
+
cmd.extend(["--cache-dir", self.cache_dir])
|
|
293
|
+
|
|
294
|
+
# Add resource limits
|
|
295
|
+
effective_timeout = timeout if timeout is not None else self.execution_timeout
|
|
296
|
+
cmd.extend([
|
|
297
|
+
"--timeout", str(effective_timeout),
|
|
298
|
+
"--max-memory", str(self.max_memory_mb)
|
|
299
|
+
])
|
|
300
|
+
|
|
301
|
+
skill_env = self._build_skill_env(skill_dir, timeout)
|
|
302
|
+
|
|
303
|
+
# Execute with Level 3 user interaction support
|
|
304
|
+
try:
|
|
305
|
+
if self.sandbox_level == "3":
|
|
306
|
+
# Level 3: Allow user interaction for authorization prompts
|
|
307
|
+
result = subprocess.run(
|
|
308
|
+
cmd,
|
|
309
|
+
stdin=None,
|
|
310
|
+
stdout=subprocess.PIPE,
|
|
311
|
+
stderr=None, # Let stderr flow to terminal for authorization prompts
|
|
312
|
+
text=True,
|
|
313
|
+
timeout=effective_timeout,
|
|
314
|
+
env=skill_env
|
|
315
|
+
)
|
|
316
|
+
return self._parse_output(result.stdout, "", result.returncode)
|
|
317
|
+
else:
|
|
318
|
+
result = subprocess.run(
|
|
319
|
+
cmd,
|
|
320
|
+
capture_output=True,
|
|
321
|
+
text=True,
|
|
322
|
+
timeout=effective_timeout,
|
|
323
|
+
env=skill_env
|
|
324
|
+
)
|
|
325
|
+
return self._parse_output(result.stdout, result.stderr, result.returncode)
|
|
326
|
+
|
|
327
|
+
except subprocess.TimeoutExpired:
|
|
328
|
+
return ExecutionResult(
|
|
329
|
+
success=False,
|
|
330
|
+
error=f"Execution timed out after {effective_timeout} seconds",
|
|
331
|
+
exit_code=-1
|
|
332
|
+
)
|
|
333
|
+
except FileNotFoundError:
|
|
334
|
+
return ExecutionResult(
|
|
335
|
+
success=False,
|
|
336
|
+
error=f"skillbox binary not found at: {self.binary_path}",
|
|
337
|
+
exit_code=-1
|
|
338
|
+
)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
return ExecutionResult(
|
|
341
|
+
success=False,
|
|
342
|
+
error=f"Execution failed: {str(e)}",
|
|
343
|
+
exit_code=-1
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def _exec_python_script_direct(
|
|
347
|
+
self,
|
|
348
|
+
skill_dir: Path,
|
|
349
|
+
script_path: Path,
|
|
350
|
+
input_data: Dict[str, Any],
|
|
351
|
+
args: Optional[list],
|
|
352
|
+
timeout: Optional[int]
|
|
353
|
+
) -> ExecutionResult:
|
|
354
|
+
"""
|
|
355
|
+
Execute Python script directly using subprocess with memory monitoring.
|
|
356
|
+
|
|
357
|
+
This provides better argument handling for CLI-style Python scripts
|
|
358
|
+
that use argparse or sys.argv, and adds memory limit protection.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
skill_dir: Path to the skill directory
|
|
362
|
+
script_path: Full path to the script
|
|
363
|
+
input_data: Input data (for reference)
|
|
364
|
+
args: Command line arguments
|
|
365
|
+
timeout: Execution timeout in seconds
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
ExecutionResult with the output or error
|
|
369
|
+
"""
|
|
370
|
+
effective_timeout = timeout if timeout is not None else self.execution_timeout
|
|
371
|
+
|
|
372
|
+
# Try to import psutil for memory monitoring
|
|
373
|
+
try:
|
|
374
|
+
import psutil
|
|
375
|
+
has_psutil = True
|
|
376
|
+
except ImportError:
|
|
377
|
+
has_psutil = False
|
|
378
|
+
|
|
379
|
+
python_executable = sys.executable
|
|
380
|
+
cmd = [python_executable, str(script_path)]
|
|
381
|
+
|
|
382
|
+
if args:
|
|
383
|
+
cmd.extend(args)
|
|
384
|
+
|
|
385
|
+
skill_env = self._build_skill_env(skill_dir, timeout)
|
|
386
|
+
|
|
387
|
+
# Memory monitoring variables
|
|
388
|
+
memory_limit_bytes = self.max_memory_mb * 1024 * 1024
|
|
389
|
+
memory_exceeded = threading.Event()
|
|
390
|
+
memory_monitor_error: List[str] = []
|
|
391
|
+
|
|
392
|
+
def monitor_memory(proc: subprocess.Popen) -> None:
|
|
393
|
+
"""Monitor process memory usage in a separate thread."""
|
|
394
|
+
if not has_psutil:
|
|
395
|
+
return
|
|
396
|
+
try:
|
|
397
|
+
import psutil as ps
|
|
398
|
+
ps_process = ps.Process(proc.pid)
|
|
399
|
+
while not memory_exceeded.is_set() and proc.poll() is None:
|
|
400
|
+
try:
|
|
401
|
+
mem_info = ps_process.memory_info()
|
|
402
|
+
if mem_info.rss > memory_limit_bytes:
|
|
403
|
+
memory_monitor_error.append(
|
|
404
|
+
f"Process killed: memory usage ({mem_info.rss / (1024*1024):.2f} MB) "
|
|
405
|
+
f"exceeded limit ({self.max_memory_mb} MB)"
|
|
406
|
+
)
|
|
407
|
+
memory_exceeded.set()
|
|
408
|
+
proc.terminate()
|
|
409
|
+
break
|
|
410
|
+
except (ps.NoSuchProcess, ps.AccessDenied):
|
|
411
|
+
break
|
|
412
|
+
time.sleep(0.1)
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
# Pass input data via stdin for scripts that read from stdin
|
|
418
|
+
input_json = json.dumps(input_data, ensure_ascii=False)
|
|
419
|
+
|
|
420
|
+
proc = subprocess.Popen(
|
|
421
|
+
cmd,
|
|
422
|
+
stdin=subprocess.PIPE,
|
|
423
|
+
stdout=subprocess.PIPE,
|
|
424
|
+
stderr=subprocess.PIPE,
|
|
425
|
+
text=True,
|
|
426
|
+
env=skill_env
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Start memory monitoring thread
|
|
430
|
+
if has_psutil:
|
|
431
|
+
monitor_thread = threading.Thread(target=monitor_memory, args=(proc,), daemon=True)
|
|
432
|
+
monitor_thread.start()
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
stdout, stderr = proc.communicate(input=input_json, timeout=effective_timeout)
|
|
436
|
+
except subprocess.TimeoutExpired:
|
|
437
|
+
proc.kill()
|
|
438
|
+
proc.communicate()
|
|
439
|
+
memory_exceeded.set()
|
|
440
|
+
return ExecutionResult(
|
|
441
|
+
success=False,
|
|
442
|
+
error=f"Execution timed out after {effective_timeout} seconds",
|
|
443
|
+
exit_code=-1
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Stop memory monitoring
|
|
447
|
+
memory_exceeded.set()
|
|
448
|
+
|
|
449
|
+
# Check if process was killed due to memory limit
|
|
450
|
+
if memory_monitor_error:
|
|
451
|
+
return ExecutionResult(
|
|
452
|
+
success=False,
|
|
453
|
+
error=memory_monitor_error[0],
|
|
454
|
+
exit_code=-1
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return self._parse_output(stdout, stderr, proc.returncode)
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
memory_exceeded.set()
|
|
461
|
+
return ExecutionResult(
|
|
462
|
+
success=False,
|
|
463
|
+
error=f"Execution failed: {str(e)}",
|
|
464
|
+
exit_code=-1
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def execute(
|
|
468
|
+
self,
|
|
469
|
+
skill_dir: Path,
|
|
470
|
+
input_data: Dict[str, Any],
|
|
471
|
+
allow_network: Optional[bool] = None,
|
|
472
|
+
timeout: Optional[int] = None,
|
|
473
|
+
entry_point: Optional[str] = None,
|
|
474
|
+
enable_sandbox: Optional[bool] = None
|
|
475
|
+
) -> ExecutionResult:
|
|
476
|
+
"""
|
|
477
|
+
Execute a skill with the given input.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
skill_dir: Path to the skill directory
|
|
481
|
+
input_data: Input data for the skill
|
|
482
|
+
allow_network: Override default network setting
|
|
483
|
+
timeout: Execution timeout in seconds
|
|
484
|
+
entry_point: Optional specific script to execute (e.g., "scripts/init_skill.py").
|
|
485
|
+
If provided, uses exec_script instead of run command.
|
|
486
|
+
enable_sandbox: Override default sandbox setting
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
ExecutionResult with the output or error
|
|
490
|
+
"""
|
|
491
|
+
# If a specific entry_point is provided, use exec_script
|
|
492
|
+
if entry_point:
|
|
493
|
+
return self.exec_script(
|
|
494
|
+
skill_dir=skill_dir,
|
|
495
|
+
script_path=entry_point,
|
|
496
|
+
input_data=input_data,
|
|
497
|
+
allow_network=allow_network,
|
|
498
|
+
timeout=timeout,
|
|
499
|
+
enable_sandbox=enable_sandbox
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if allow_network is None:
|
|
503
|
+
allow_network = self.allow_network
|
|
504
|
+
|
|
505
|
+
if enable_sandbox is None:
|
|
506
|
+
enable_sandbox = self.enable_sandbox
|
|
507
|
+
|
|
508
|
+
effective_timeout = timeout if timeout is not None else self.execution_timeout
|
|
509
|
+
|
|
510
|
+
# Check sandbox level - Level 1 and 2 should use direct execution for Python skills
|
|
511
|
+
if self.sandbox_level != "3":
|
|
512
|
+
python_entry_points = ["scripts/main.py", "main.py"]
|
|
513
|
+
for entry in python_entry_points:
|
|
514
|
+
script_path = skill_dir / entry
|
|
515
|
+
if script_path.exists():
|
|
516
|
+
return self.exec_script(
|
|
517
|
+
skill_dir=skill_dir,
|
|
518
|
+
script_path=entry,
|
|
519
|
+
input_data=input_data,
|
|
520
|
+
allow_network=allow_network,
|
|
521
|
+
timeout=timeout,
|
|
522
|
+
enable_sandbox=enable_sandbox
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Build command for skillbox run
|
|
526
|
+
cmd = [
|
|
527
|
+
self.binary_path,
|
|
528
|
+
"run",
|
|
529
|
+
str(skill_dir),
|
|
530
|
+
json.dumps(input_data)
|
|
531
|
+
]
|
|
532
|
+
|
|
533
|
+
if allow_network:
|
|
534
|
+
cmd.append("--allow-network")
|
|
535
|
+
|
|
536
|
+
if self.cache_dir:
|
|
537
|
+
cmd.extend(["--cache-dir", self.cache_dir])
|
|
538
|
+
|
|
539
|
+
skill_env = self._build_skill_env(skill_dir, timeout)
|
|
540
|
+
|
|
541
|
+
# Execute with Level 3 user interaction support
|
|
542
|
+
try:
|
|
543
|
+
if self.sandbox_level == "3":
|
|
544
|
+
result = subprocess.run(
|
|
545
|
+
cmd,
|
|
546
|
+
stdin=None,
|
|
547
|
+
stdout=subprocess.PIPE,
|
|
548
|
+
stderr=None,
|
|
549
|
+
text=True,
|
|
550
|
+
timeout=effective_timeout,
|
|
551
|
+
env=skill_env
|
|
552
|
+
)
|
|
553
|
+
else:
|
|
554
|
+
result = subprocess.run(
|
|
555
|
+
cmd,
|
|
556
|
+
capture_output=True,
|
|
557
|
+
text=True,
|
|
558
|
+
timeout=effective_timeout,
|
|
559
|
+
env=skill_env
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
stderr = result.stderr if hasattr(result, 'stderr') and result.stderr else ""
|
|
563
|
+
|
|
564
|
+
if result.returncode == 0:
|
|
565
|
+
try:
|
|
566
|
+
output = json.loads(result.stdout.strip())
|
|
567
|
+
return ExecutionResult(
|
|
568
|
+
success=True,
|
|
569
|
+
output=output,
|
|
570
|
+
exit_code=result.returncode,
|
|
571
|
+
stdout=result.stdout,
|
|
572
|
+
stderr=stderr
|
|
573
|
+
)
|
|
574
|
+
except json.JSONDecodeError as e:
|
|
575
|
+
return ExecutionResult(
|
|
576
|
+
success=False,
|
|
577
|
+
error=f"Invalid JSON output: {e}",
|
|
578
|
+
exit_code=result.returncode,
|
|
579
|
+
stdout=result.stdout,
|
|
580
|
+
stderr=stderr
|
|
581
|
+
)
|
|
582
|
+
else:
|
|
583
|
+
return ExecutionResult(
|
|
584
|
+
success=False,
|
|
585
|
+
error=stderr or f"Exit code: {result.returncode}",
|
|
586
|
+
exit_code=result.returncode,
|
|
587
|
+
stdout=result.stdout,
|
|
588
|
+
stderr=stderr
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
except subprocess.TimeoutExpired:
|
|
592
|
+
return ExecutionResult(
|
|
593
|
+
success=False,
|
|
594
|
+
error=f"Execution timed out after {effective_timeout} seconds",
|
|
595
|
+
exit_code=-1
|
|
596
|
+
)
|
|
597
|
+
except FileNotFoundError:
|
|
598
|
+
return ExecutionResult(
|
|
599
|
+
success=False,
|
|
600
|
+
error=f"skillbox binary not found at: {self.binary_path}",
|
|
601
|
+
exit_code=-1
|
|
602
|
+
)
|
|
603
|
+
except Exception as e:
|
|
604
|
+
return ExecutionResult(
|
|
605
|
+
success=False,
|
|
606
|
+
error=str(e),
|
|
607
|
+
exit_code=-1
|
|
608
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utilities for sandbox implementations.
|
|
3
|
+
|
|
4
|
+
This module provides shared functionality used by different executor
|
|
5
|
+
implementations, reducing code duplication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Default positional argument keys that should be treated as positional args
|
|
12
|
+
DEFAULT_POSITIONAL_KEYS = {"skill_name", "skill-name", "name", "input", "file", "filename"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def convert_json_to_cli_args(
|
|
16
|
+
input_data: Dict[str, Any],
|
|
17
|
+
positional_keys: set = None
|
|
18
|
+
) -> List[str]:
|
|
19
|
+
"""
|
|
20
|
+
Convert JSON input data to command line arguments list.
|
|
21
|
+
|
|
22
|
+
This handles the conversion of JSON parameters to CLI format:
|
|
23
|
+
- Positional args: keys like "skill_name" or "skill-name" become positional values
|
|
24
|
+
- Named args: keys like "path" become "--path value"
|
|
25
|
+
- Boolean flags: true becomes "--flag", false is omitted
|
|
26
|
+
- Arrays: become comma-separated values
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
input_data: JSON input data from LLM
|
|
30
|
+
positional_keys: Set of keys to treat as positional arguments.
|
|
31
|
+
Defaults to DEFAULT_POSITIONAL_KEYS.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of command line arguments
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> convert_json_to_cli_args({"name": "test", "verbose": True, "count": 5})
|
|
38
|
+
['test', '--verbose', '--count', '5']
|
|
39
|
+
"""
|
|
40
|
+
if positional_keys is None:
|
|
41
|
+
positional_keys = DEFAULT_POSITIONAL_KEYS
|
|
42
|
+
|
|
43
|
+
args_list = []
|
|
44
|
+
|
|
45
|
+
# First, handle positional arguments
|
|
46
|
+
for key in positional_keys:
|
|
47
|
+
if key in input_data:
|
|
48
|
+
value = input_data[key]
|
|
49
|
+
if isinstance(value, str):
|
|
50
|
+
args_list.append(value)
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
# Then handle named arguments
|
|
54
|
+
for key, value in input_data.items():
|
|
55
|
+
# Skip positional args already handled
|
|
56
|
+
normalized_key = key.replace("-", "_")
|
|
57
|
+
if normalized_key in {k.replace("-", "_") for k in positional_keys}:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Convert key to CLI format (e.g., "skill_name" -> "--skill-name")
|
|
61
|
+
cli_key = f"--{key.replace('_', '-')}"
|
|
62
|
+
|
|
63
|
+
if isinstance(value, bool):
|
|
64
|
+
# Boolean flags: only add if True
|
|
65
|
+
if value:
|
|
66
|
+
args_list.append(cli_key)
|
|
67
|
+
elif isinstance(value, list):
|
|
68
|
+
# Arrays become comma-separated
|
|
69
|
+
if value:
|
|
70
|
+
args_list.append(cli_key)
|
|
71
|
+
args_list.append(",".join(str(v) for v in value))
|
|
72
|
+
elif value is not None:
|
|
73
|
+
# Regular values
|
|
74
|
+
args_list.append(cli_key)
|
|
75
|
+
args_list.append(str(value))
|
|
76
|
+
|
|
77
|
+
return args_list
|