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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Execution Context - Encapsulates all configuration for a single execution.
|
|
3
|
+
|
|
4
|
+
This module provides the ExecutionContext class which is the single source of truth
|
|
5
|
+
for execution configuration. It reads from environment variables at runtime,
|
|
6
|
+
ensuring that any changes to environment variables are immediately reflected.
|
|
7
|
+
|
|
8
|
+
Design Principles:
|
|
9
|
+
1. Read configuration at runtime, not at initialization
|
|
10
|
+
2. Never cache configuration values
|
|
11
|
+
3. Support temporary overrides via with_override()
|
|
12
|
+
4. Immutable - create new instances for modifications
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Default configuration values
|
|
21
|
+
DEFAULT_SANDBOX_LEVEL = "3"
|
|
22
|
+
DEFAULT_TIMEOUT = 120
|
|
23
|
+
DEFAULT_MAX_MEMORY_MB = 512
|
|
24
|
+
DEFAULT_ALLOW_NETWORK = False
|
|
25
|
+
DEFAULT_AUTO_APPROVE = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_bool_env(key: str, default: bool) -> bool:
|
|
29
|
+
"""Parse a boolean value from environment variable."""
|
|
30
|
+
value = os.environ.get(key)
|
|
31
|
+
if value is None:
|
|
32
|
+
return default
|
|
33
|
+
value_lower = value.lower().strip()
|
|
34
|
+
if value_lower in ("true", "1", "yes", "on"):
|
|
35
|
+
return True
|
|
36
|
+
elif value_lower in ("false", "0", "no", "off", ""):
|
|
37
|
+
return False
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ExecutionContext:
|
|
43
|
+
"""
|
|
44
|
+
Execution context - all configuration for a single execution.
|
|
45
|
+
|
|
46
|
+
This class is immutable (frozen=True). To modify, use with_override()
|
|
47
|
+
which returns a new instance.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
sandbox_level: Sandbox security level ("1", "2", or "3")
|
|
51
|
+
allow_network: Whether to allow network access
|
|
52
|
+
timeout: Execution timeout in seconds
|
|
53
|
+
max_memory_mb: Maximum memory limit in MB
|
|
54
|
+
auto_approve: Whether to auto-approve security prompts
|
|
55
|
+
confirmed: Whether user has confirmed execution (for security flow)
|
|
56
|
+
scan_id: Scan ID from security scan (for verification)
|
|
57
|
+
requires_elevated: Whether skill requires elevated permissions
|
|
58
|
+
"""
|
|
59
|
+
sandbox_level: str = DEFAULT_SANDBOX_LEVEL
|
|
60
|
+
allow_network: bool = DEFAULT_ALLOW_NETWORK
|
|
61
|
+
timeout: int = DEFAULT_TIMEOUT
|
|
62
|
+
max_memory_mb: int = DEFAULT_MAX_MEMORY_MB
|
|
63
|
+
auto_approve: bool = DEFAULT_AUTO_APPROVE
|
|
64
|
+
confirmed: bool = False
|
|
65
|
+
scan_id: Optional[str] = None
|
|
66
|
+
requires_elevated: bool = False
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_current_env(cls) -> "ExecutionContext":
|
|
70
|
+
"""
|
|
71
|
+
Create context from current environment variables.
|
|
72
|
+
|
|
73
|
+
This method reads from environment variables at call time,
|
|
74
|
+
ensuring the latest values are used.
|
|
75
|
+
|
|
76
|
+
Environment Variables:
|
|
77
|
+
SKILLBOX_SANDBOX_LEVEL: Sandbox level (1/2/3, default: 3)
|
|
78
|
+
SKILLBOX_ALLOW_NETWORK: Allow network access (true/false)
|
|
79
|
+
SKILLBOX_TIMEOUT_SECS: Execution timeout in seconds
|
|
80
|
+
SKILLBOX_MAX_MEMORY_MB: Maximum memory in MB
|
|
81
|
+
SKILLBOX_AUTO_APPROVE: Auto-approve security prompts
|
|
82
|
+
"""
|
|
83
|
+
return cls(
|
|
84
|
+
sandbox_level=os.environ.get("SKILLBOX_SANDBOX_LEVEL", DEFAULT_SANDBOX_LEVEL),
|
|
85
|
+
allow_network=_parse_bool_env("SKILLBOX_ALLOW_NETWORK", DEFAULT_ALLOW_NETWORK),
|
|
86
|
+
timeout=int(os.environ.get("SKILLBOX_TIMEOUT_SECS", str(DEFAULT_TIMEOUT))),
|
|
87
|
+
max_memory_mb=int(os.environ.get("SKILLBOX_MAX_MEMORY_MB", str(DEFAULT_MAX_MEMORY_MB))),
|
|
88
|
+
auto_approve=_parse_bool_env("SKILLBOX_AUTO_APPROVE", DEFAULT_AUTO_APPROVE),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def with_override(
|
|
92
|
+
self,
|
|
93
|
+
sandbox_level: Optional[str] = None,
|
|
94
|
+
allow_network: Optional[bool] = None,
|
|
95
|
+
timeout: Optional[int] = None,
|
|
96
|
+
max_memory_mb: Optional[int] = None,
|
|
97
|
+
auto_approve: Optional[bool] = None,
|
|
98
|
+
confirmed: bool = False,
|
|
99
|
+
scan_id: Optional[str] = None,
|
|
100
|
+
requires_elevated: Optional[bool] = None,
|
|
101
|
+
) -> "ExecutionContext":
|
|
102
|
+
"""
|
|
103
|
+
Create a new context with specified overrides.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
sandbox_level: Override sandbox level
|
|
107
|
+
allow_network: Override network setting
|
|
108
|
+
timeout: Override timeout
|
|
109
|
+
max_memory_mb: Override memory limit
|
|
110
|
+
auto_approve: Override auto-approve setting
|
|
111
|
+
confirmed: Set confirmed flag
|
|
112
|
+
scan_id: Set scan ID
|
|
113
|
+
requires_elevated: Set requires elevated flag
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
New ExecutionContext with overrides applied
|
|
117
|
+
"""
|
|
118
|
+
return ExecutionContext(
|
|
119
|
+
sandbox_level=sandbox_level if sandbox_level is not None else self.sandbox_level,
|
|
120
|
+
allow_network=allow_network if allow_network is not None else self.allow_network,
|
|
121
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
122
|
+
max_memory_mb=max_memory_mb if max_memory_mb is not None else self.max_memory_mb,
|
|
123
|
+
auto_approve=auto_approve if auto_approve is not None else self.auto_approve,
|
|
124
|
+
confirmed=confirmed if confirmed else self.confirmed,
|
|
125
|
+
scan_id=scan_id if scan_id is not None else self.scan_id,
|
|
126
|
+
requires_elevated=requires_elevated if requires_elevated is not None else self.requires_elevated,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def with_user_confirmation(self, scan_id: str) -> "ExecutionContext":
|
|
130
|
+
"""
|
|
131
|
+
Create a new context after user confirmation.
|
|
132
|
+
|
|
133
|
+
This downgrades sandbox level to 1 (no sandbox) since user has approved.
|
|
134
|
+
"""
|
|
135
|
+
return self.with_override(
|
|
136
|
+
sandbox_level="1",
|
|
137
|
+
confirmed=True,
|
|
138
|
+
scan_id=scan_id,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def with_elevated_permissions(self) -> "ExecutionContext":
|
|
142
|
+
"""
|
|
143
|
+
Create a new context with elevated permissions.
|
|
144
|
+
|
|
145
|
+
This downgrades sandbox level to 1 for skills that require
|
|
146
|
+
elevated permissions (e.g., skill-creator).
|
|
147
|
+
"""
|
|
148
|
+
return self.with_override(
|
|
149
|
+
sandbox_level="1",
|
|
150
|
+
requires_elevated=True,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = ["ExecutionContext", "DEFAULT_SANDBOX_LEVEL", "DEFAULT_TIMEOUT", "DEFAULT_MAX_MEMORY_MB"]
|
|
155
|
+
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Execution Service - High-level execution service for all entry points.
|
|
3
|
+
|
|
4
|
+
This module provides the UnifiedExecutionService class which is the single entry point
|
|
5
|
+
for all skill execution. It integrates security scanning, user confirmation, and
|
|
6
|
+
execution into a unified flow.
|
|
7
|
+
|
|
8
|
+
All entry points (AgenticLoop, LangChain, LlamaIndex, MCP) should use this service.
|
|
9
|
+
|
|
10
|
+
Key Features:
|
|
11
|
+
1. Unified security scanning
|
|
12
|
+
2. Unified confirmation flow
|
|
13
|
+
3. Unified execution
|
|
14
|
+
4. Context management
|
|
15
|
+
5. Temporary context overrides
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable, Dict, Optional, TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from .base import ExecutionResult
|
|
24
|
+
from .context import ExecutionContext
|
|
25
|
+
from .unified_executor import UnifiedExecutor
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from ..core.skill_info import SkillInfo
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Type alias for confirmation callback
|
|
32
|
+
ConfirmationCallback = Callable[[str, str], bool]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UnifiedExecutionService:
|
|
36
|
+
"""
|
|
37
|
+
Unified execution service - single entry point for all skill execution.
|
|
38
|
+
|
|
39
|
+
This service integrates:
|
|
40
|
+
1. Security scanning (via UnifiedSecurityScanner)
|
|
41
|
+
2. User confirmation flow
|
|
42
|
+
3. Skill execution (via UnifiedExecutor)
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
service = UnifiedExecutionService.get_instance()
|
|
46
|
+
result = service.execute_skill(skill_info, input_data, confirmation_callback)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
_instance: Optional["UnifiedExecutionService"] = None
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_instance(cls) -> "UnifiedExecutionService":
|
|
53
|
+
"""Get singleton instance of the service."""
|
|
54
|
+
if cls._instance is None:
|
|
55
|
+
cls._instance = cls()
|
|
56
|
+
return cls._instance
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def reset_instance(cls) -> None:
|
|
60
|
+
"""Reset singleton instance (for testing)."""
|
|
61
|
+
cls._instance = None
|
|
62
|
+
|
|
63
|
+
def __init__(self):
|
|
64
|
+
"""Initialize the service."""
|
|
65
|
+
self._executor = UnifiedExecutor()
|
|
66
|
+
self._scanner = None # Lazy initialization
|
|
67
|
+
|
|
68
|
+
def _get_scanner(self):
|
|
69
|
+
"""Get security scanner (lazy initialization)."""
|
|
70
|
+
if self._scanner is None:
|
|
71
|
+
from ..core.security import SecurityScanner
|
|
72
|
+
self._scanner = SecurityScanner()
|
|
73
|
+
return self._scanner
|
|
74
|
+
|
|
75
|
+
def execute_skill(
|
|
76
|
+
self,
|
|
77
|
+
skill_info: "SkillInfo",
|
|
78
|
+
input_data: Dict[str, Any],
|
|
79
|
+
entry_point: Optional[str] = None,
|
|
80
|
+
confirmation_callback: Optional[ConfirmationCallback] = None,
|
|
81
|
+
allow_network: Optional[bool] = None,
|
|
82
|
+
timeout: Optional[int] = None,
|
|
83
|
+
) -> ExecutionResult:
|
|
84
|
+
"""
|
|
85
|
+
Execute a skill with unified security and confirmation flow.
|
|
86
|
+
|
|
87
|
+
Flow:
|
|
88
|
+
1. Read current execution context from environment
|
|
89
|
+
2. Check if skill requires elevated permissions
|
|
90
|
+
3. Perform security scan (if Level 3)
|
|
91
|
+
4. Request user confirmation (if high-severity issues)
|
|
92
|
+
5. Adjust context based on confirmation
|
|
93
|
+
6. Execute skill
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
skill_info: SkillInfo object with skill metadata
|
|
97
|
+
input_data: Input data for the skill
|
|
98
|
+
entry_point: Optional specific script to execute
|
|
99
|
+
confirmation_callback: Callback for security confirmation
|
|
100
|
+
allow_network: Override network setting
|
|
101
|
+
timeout: Override timeout setting
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
ExecutionResult with output or error
|
|
105
|
+
"""
|
|
106
|
+
# 1. Read current context from environment
|
|
107
|
+
context = ExecutionContext.from_current_env()
|
|
108
|
+
|
|
109
|
+
# 2. Apply overrides
|
|
110
|
+
if allow_network is not None or timeout is not None:
|
|
111
|
+
context = context.with_override(
|
|
112
|
+
allow_network=allow_network,
|
|
113
|
+
timeout=timeout,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 3. Check if skill requires elevated permissions
|
|
117
|
+
requires_elevated = self._requires_elevated_permissions(skill_info)
|
|
118
|
+
if requires_elevated:
|
|
119
|
+
context = context.with_elevated_permissions()
|
|
120
|
+
|
|
121
|
+
# 4. Security scan and confirmation (Level 3 only)
|
|
122
|
+
if context.sandbox_level == "3":
|
|
123
|
+
scan_result = self._perform_security_scan(skill_info, input_data)
|
|
124
|
+
|
|
125
|
+
if scan_result and scan_result.requires_confirmation:
|
|
126
|
+
if confirmation_callback:
|
|
127
|
+
report = scan_result.format_report()
|
|
128
|
+
confirmed = confirmation_callback(report, scan_result.scan_id)
|
|
129
|
+
|
|
130
|
+
if confirmed:
|
|
131
|
+
# User confirmed -> downgrade to Level 1
|
|
132
|
+
context = context.with_user_confirmation(scan_result.scan_id)
|
|
133
|
+
else:
|
|
134
|
+
return ExecutionResult(
|
|
135
|
+
success=False,
|
|
136
|
+
error="Execution cancelled by user after security review",
|
|
137
|
+
exit_code=1,
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
# No callback, return security report
|
|
141
|
+
return ExecutionResult(
|
|
142
|
+
success=False,
|
|
143
|
+
error=f"Security confirmation required:\n{scan_result.format_report()}",
|
|
144
|
+
exit_code=2,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 5. Execute skill
|
|
148
|
+
return self._executor.execute(
|
|
149
|
+
context=context,
|
|
150
|
+
skill_dir=skill_info.path,
|
|
151
|
+
input_data=input_data,
|
|
152
|
+
entry_point=entry_point,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def execute_with_context(
|
|
156
|
+
self,
|
|
157
|
+
context: ExecutionContext,
|
|
158
|
+
skill_dir: Path,
|
|
159
|
+
input_data: Dict[str, Any],
|
|
160
|
+
entry_point: Optional[str] = None,
|
|
161
|
+
args: Optional[list] = None,
|
|
162
|
+
) -> ExecutionResult:
|
|
163
|
+
"""
|
|
164
|
+
Execute with explicit context (bypasses security scan).
|
|
165
|
+
|
|
166
|
+
Use this when you've already performed security checks
|
|
167
|
+
and have a prepared context.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
context: Pre-configured execution context
|
|
171
|
+
skill_dir: Path to skill directory
|
|
172
|
+
input_data: Input data for the skill
|
|
173
|
+
entry_point: Optional specific script to execute
|
|
174
|
+
args: Optional command line arguments
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ExecutionResult with output or error
|
|
178
|
+
"""
|
|
179
|
+
return self._executor.execute(
|
|
180
|
+
context=context,
|
|
181
|
+
skill_dir=skill_dir,
|
|
182
|
+
input_data=input_data,
|
|
183
|
+
entry_point=entry_point,
|
|
184
|
+
args=args,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _requires_elevated_permissions(self, skill_info: "SkillInfo") -> bool:
|
|
188
|
+
"""Check if skill requires elevated permissions."""
|
|
189
|
+
if skill_info.metadata:
|
|
190
|
+
return getattr(skill_info.metadata, 'requires_elevated_permissions', False)
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def _perform_security_scan(
|
|
194
|
+
self,
|
|
195
|
+
skill_info: "SkillInfo",
|
|
196
|
+
input_data: Dict[str, Any],
|
|
197
|
+
):
|
|
198
|
+
"""Perform security scan on skill."""
|
|
199
|
+
try:
|
|
200
|
+
scanner = self._get_scanner()
|
|
201
|
+
return scanner.scan_skill(skill_info, input_data)
|
|
202
|
+
except Exception:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
@contextmanager
|
|
206
|
+
def temporary_context(
|
|
207
|
+
self,
|
|
208
|
+
sandbox_level: Optional[str] = None,
|
|
209
|
+
allow_network: Optional[bool] = None,
|
|
210
|
+
):
|
|
211
|
+
"""
|
|
212
|
+
Context manager for temporary execution context changes.
|
|
213
|
+
|
|
214
|
+
This temporarily modifies environment variables and restores
|
|
215
|
+
them when the context exits.
|
|
216
|
+
|
|
217
|
+
Usage:
|
|
218
|
+
with service.temporary_context(sandbox_level="1"):
|
|
219
|
+
result = service.execute_skill(...)
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
sandbox_level: Temporary sandbox level
|
|
223
|
+
allow_network: Temporary network setting
|
|
224
|
+
"""
|
|
225
|
+
old_sandbox_level = os.environ.get("SKILLBOX_SANDBOX_LEVEL")
|
|
226
|
+
old_allow_network = os.environ.get("SKILLBOX_ALLOW_NETWORK")
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
if sandbox_level is not None:
|
|
230
|
+
os.environ["SKILLBOX_SANDBOX_LEVEL"] = sandbox_level
|
|
231
|
+
if allow_network is not None:
|
|
232
|
+
os.environ["SKILLBOX_ALLOW_NETWORK"] = "true" if allow_network else "false"
|
|
233
|
+
yield
|
|
234
|
+
finally:
|
|
235
|
+
# Restore original values
|
|
236
|
+
if sandbox_level is not None:
|
|
237
|
+
if old_sandbox_level is not None:
|
|
238
|
+
os.environ["SKILLBOX_SANDBOX_LEVEL"] = old_sandbox_level
|
|
239
|
+
elif "SKILLBOX_SANDBOX_LEVEL" in os.environ:
|
|
240
|
+
del os.environ["SKILLBOX_SANDBOX_LEVEL"]
|
|
241
|
+
|
|
242
|
+
if allow_network is not None:
|
|
243
|
+
if old_allow_network is not None:
|
|
244
|
+
os.environ["SKILLBOX_ALLOW_NETWORK"] = old_allow_network
|
|
245
|
+
elif "SKILLBOX_ALLOW_NETWORK" in os.environ:
|
|
246
|
+
del os.environ["SKILLBOX_ALLOW_NETWORK"]
|
|
247
|
+
|
|
248
|
+
def get_current_context(self) -> ExecutionContext:
|
|
249
|
+
"""Get current execution context from environment."""
|
|
250
|
+
return ExecutionContext.from_current_env()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
__all__ = ["UnifiedExecutionService", "ConfirmationCallback"]
|
|
254
|
+
|
|
@@ -156,14 +156,18 @@ class SkillboxExecutor(SandboxExecutor):
|
|
|
156
156
|
def _build_skill_env(self, skill_dir: Path, timeout: Optional[int] = None) -> Dict[str, str]:
|
|
157
157
|
"""
|
|
158
158
|
Build environment variables for skill execution.
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
Args:
|
|
161
161
|
skill_dir: Path to the skill directory
|
|
162
162
|
timeout: Optional timeout override
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
Returns:
|
|
165
165
|
Environment dictionary
|
|
166
166
|
"""
|
|
167
|
+
# Allow runtime override of sandbox level via environment variable
|
|
168
|
+
# This enables Python-layer security confirmation to skip skillbox confirmation
|
|
169
|
+
effective_sandbox_level = os.environ.get("SKILLBOX_SANDBOX_LEVEL", self.sandbox_level)
|
|
170
|
+
|
|
167
171
|
return {
|
|
168
172
|
**os.environ,
|
|
169
173
|
"PYTHONUNBUFFERED": "1",
|
|
@@ -171,12 +175,81 @@ class SkillboxExecutor(SandboxExecutor):
|
|
|
171
175
|
"SKILL_ASSETS_DIR": str(skill_dir / "assets"),
|
|
172
176
|
"SKILL_REFERENCES_DIR": str(skill_dir / "references"),
|
|
173
177
|
"SKILL_SCRIPTS_DIR": str(skill_dir / "scripts"),
|
|
174
|
-
"SKILLBOX_SANDBOX_LEVEL":
|
|
178
|
+
"SKILLBOX_SANDBOX_LEVEL": effective_sandbox_level,
|
|
175
179
|
"SKILLBOX_MAX_MEMORY_MB": str(self.max_memory_mb),
|
|
176
180
|
"SKILLBOX_TIMEOUT_SECS": str(timeout if timeout is not None else self.execution_timeout),
|
|
177
181
|
"SKILLBOX_AUTO_APPROVE": os.environ.get("SKILLBOX_AUTO_APPROVE", ""),
|
|
178
182
|
}
|
|
179
|
-
|
|
183
|
+
|
|
184
|
+
def _format_sandbox_error(self, error_msg: str) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Format sandbox restriction errors into user-friendly messages.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
error_msg: Raw error message from subprocess
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Formatted error message
|
|
193
|
+
"""
|
|
194
|
+
# Check for common sandbox restriction patterns
|
|
195
|
+
sandbox_errors = {
|
|
196
|
+
"BlockingIOError": "🔒 Sandbox blocked process creation (fork/exec not allowed)",
|
|
197
|
+
"Resource temporarily unavailable": "🔒 Sandbox blocked system resource access",
|
|
198
|
+
"Operation not permitted": "🔒 Sandbox blocked this operation",
|
|
199
|
+
"Permission denied": "🔒 Sandbox denied file/resource access",
|
|
200
|
+
"sandbox-exec": "🔒 Sandbox restriction triggered",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for pattern, friendly_msg in sandbox_errors.items():
|
|
204
|
+
if pattern in error_msg:
|
|
205
|
+
# Return only the friendly message, hide the traceback
|
|
206
|
+
return f"{friendly_msg}\n\n💡 This skill requires operations that are blocked by the sandbox for security reasons."
|
|
207
|
+
|
|
208
|
+
return error_msg
|
|
209
|
+
|
|
210
|
+
def _extract_json_from_output(self, output: str) -> Optional[Any]:
|
|
211
|
+
"""
|
|
212
|
+
Try to extract JSON from skillbox output that may contain log lines.
|
|
213
|
+
|
|
214
|
+
Skillbox output format may include:
|
|
215
|
+
- [INFO] ... log lines
|
|
216
|
+
- [WARN] ... log lines
|
|
217
|
+
- Error: ... messages
|
|
218
|
+
- Then the actual JSON object
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
output: Raw output from skillbox
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Parsed JSON object if found, None otherwise
|
|
225
|
+
"""
|
|
226
|
+
if not output:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# First try: parse entire output as JSON
|
|
230
|
+
try:
|
|
231
|
+
return json.loads(output.strip())
|
|
232
|
+
except json.JSONDecodeError:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
# Second try: find JSON object by looking for { and matching }
|
|
236
|
+
# This handles cases where JSON contains newlines (like \n in strings)
|
|
237
|
+
brace_start = output.rfind('{')
|
|
238
|
+
if brace_start == -1:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
brace_end = output.rfind('}')
|
|
242
|
+
if brace_end == -1 or brace_end < brace_start:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
json_str = output[brace_start:brace_end + 1]
|
|
246
|
+
try:
|
|
247
|
+
return json.loads(json_str)
|
|
248
|
+
except json.JSONDecodeError:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
return None
|
|
252
|
+
|
|
180
253
|
def _parse_output(self, stdout: str, stderr: str, returncode: int) -> ExecutionResult:
|
|
181
254
|
"""
|
|
182
255
|
Parse subprocess output into ExecutionResult.
|
|
@@ -209,9 +282,13 @@ class SkillboxExecutor(SandboxExecutor):
|
|
|
209
282
|
stderr=stderr
|
|
210
283
|
)
|
|
211
284
|
else:
|
|
285
|
+
# Check for sandbox restriction errors and provide friendly messages
|
|
286
|
+
error_msg = stderr or stdout or f"Exit code: {returncode}"
|
|
287
|
+
error_msg = self._format_sandbox_error(error_msg)
|
|
288
|
+
|
|
212
289
|
return ExecutionResult(
|
|
213
290
|
success=False,
|
|
214
|
-
error=
|
|
291
|
+
error=error_msg,
|
|
215
292
|
exit_code=returncode,
|
|
216
293
|
stdout=stdout,
|
|
217
294
|
stderr=stderr
|
|
@@ -284,10 +361,12 @@ class SkillboxExecutor(SandboxExecutor):
|
|
|
284
361
|
|
|
285
362
|
if allow_network:
|
|
286
363
|
cmd.append("--allow-network")
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
364
|
+
|
|
365
|
+
# Use --sandbox-level instead of --enable-sandbox
|
|
366
|
+
# sandbox_level: 1=no sandbox, 2=sandbox only, 3=sandbox+scan
|
|
367
|
+
if self.sandbox_level:
|
|
368
|
+
cmd.extend(["--sandbox-level", str(self.sandbox_level)])
|
|
369
|
+
|
|
291
370
|
if self.cache_dir:
|
|
292
371
|
cmd.extend(["--cache-dir", self.cache_dir])
|
|
293
372
|
|
|
@@ -537,20 +616,25 @@ class SkillboxExecutor(SandboxExecutor):
|
|
|
537
616
|
cmd.extend(["--cache-dir", self.cache_dir])
|
|
538
617
|
|
|
539
618
|
skill_env = self._build_skill_env(skill_dir, timeout)
|
|
540
|
-
|
|
619
|
+
|
|
620
|
+
# Get effective sandbox level (may be overridden by environment variable)
|
|
621
|
+
effective_sandbox_level = skill_env.get("SKILLBOX_SANDBOX_LEVEL", self.sandbox_level)
|
|
622
|
+
|
|
541
623
|
# Execute with Level 3 user interaction support
|
|
542
624
|
try:
|
|
543
|
-
if
|
|
625
|
+
if effective_sandbox_level == "3":
|
|
626
|
+
# Level 3: Allow user interaction for authorization prompts
|
|
544
627
|
result = subprocess.run(
|
|
545
628
|
cmd,
|
|
546
629
|
stdin=None,
|
|
547
630
|
stdout=subprocess.PIPE,
|
|
548
|
-
stderr=None,
|
|
631
|
+
stderr=None, # Let stderr flow to terminal for authorization prompts
|
|
549
632
|
text=True,
|
|
550
633
|
timeout=effective_timeout,
|
|
551
634
|
env=skill_env
|
|
552
635
|
)
|
|
553
636
|
else:
|
|
637
|
+
# Level 1/2: Capture all output
|
|
554
638
|
result = subprocess.run(
|
|
555
639
|
cmd,
|
|
556
640
|
capture_output=True,
|
|
@@ -560,10 +644,14 @@ class SkillboxExecutor(SandboxExecutor):
|
|
|
560
644
|
)
|
|
561
645
|
|
|
562
646
|
stderr = result.stderr if hasattr(result, 'stderr') and result.stderr else ""
|
|
563
|
-
|
|
647
|
+
|
|
648
|
+
# Combine stdout and stderr for JSON extraction
|
|
649
|
+
# skillbox may output JSON to either stream
|
|
650
|
+
combined_output = result.stdout + stderr
|
|
651
|
+
|
|
564
652
|
if result.returncode == 0:
|
|
565
|
-
|
|
566
|
-
|
|
653
|
+
output = self._extract_json_from_output(combined_output)
|
|
654
|
+
if output is not None:
|
|
567
655
|
return ExecutionResult(
|
|
568
656
|
success=True,
|
|
569
657
|
output=output,
|
|
@@ -571,18 +659,35 @@ class SkillboxExecutor(SandboxExecutor):
|
|
|
571
659
|
stdout=result.stdout,
|
|
572
660
|
stderr=stderr
|
|
573
661
|
)
|
|
574
|
-
|
|
662
|
+
else:
|
|
663
|
+
# No JSON found, return raw output as success
|
|
575
664
|
return ExecutionResult(
|
|
576
|
-
success=
|
|
577
|
-
|
|
665
|
+
success=True,
|
|
666
|
+
output={"raw_output": result.stdout.strip()},
|
|
578
667
|
exit_code=result.returncode,
|
|
579
668
|
stdout=result.stdout,
|
|
580
669
|
stderr=stderr
|
|
581
670
|
)
|
|
582
671
|
else:
|
|
672
|
+
# Check if there's valid JSON in combined output (skillbox may still output results)
|
|
673
|
+
output = self._extract_json_from_output(combined_output)
|
|
674
|
+
if output is not None:
|
|
675
|
+
# If we found valid JSON with exit_code=0, treat as success
|
|
676
|
+
if isinstance(output, dict) and output.get("exit_code") == 0:
|
|
677
|
+
return ExecutionResult(
|
|
678
|
+
success=True,
|
|
679
|
+
output=output,
|
|
680
|
+
exit_code=0,
|
|
681
|
+
stdout=result.stdout,
|
|
682
|
+
stderr=stderr
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
error_msg = stderr or result.stdout or f"Exit code: {result.returncode}"
|
|
686
|
+
error_msg = self._format_sandbox_error(error_msg)
|
|
687
|
+
|
|
583
688
|
return ExecutionResult(
|
|
584
689
|
success=False,
|
|
585
|
-
error=
|
|
690
|
+
error=error_msg,
|
|
586
691
|
exit_code=result.returncode,
|
|
587
692
|
stdout=result.stdout,
|
|
588
693
|
stderr=stderr
|