sky-dev 0.0.5__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.
- sky/__init__.py +6 -0
- sky/cli.py +833 -0
- sky/config/__init__.py +19 -0
- sky/config/models.yaml +39 -0
- sky/config/schema.py +275 -0
- sky/core/__init__.py +48 -0
- sky/core/approval.py +92 -0
- sky/core/benchmark.py +39 -0
- sky/core/chat.py +211 -0
- sky/core/fast_loop.py +330 -0
- sky/core/mode_prompts.py +106 -0
- sky/core/router.py +264 -0
- sky/core/subagent.py +302 -0
- sky/core/workflow.py +662 -0
- sky/errors.py +29 -0
- sky/memory/__init__.py +27 -0
- sky/memory/indexer.py +450 -0
- sky/memory/vectorstore.py +271 -0
- sky/security/__init__.py +17 -0
- sky/security/audit.py +23 -0
- sky/security/detection.py +94 -0
- sky/security/guardrails.py +106 -0
- sky/security/prompts.py +58 -0
- sky/security/rate_limit.py +36 -0
- sky/security/sanitize.py +152 -0
- sky/storage/__init__.py +21 -0
- sky/storage/db.py +492 -0
- sky/tools/__init__.py +26 -0
- sky/tools/fs_tools.py +139 -0
- sky/tools/git_tools.py +139 -0
- sky/tools/registry.py +219 -0
- sky/tools/search_tools.py +128 -0
- sky/tools/shell_tools.py +73 -0
- sky_dev-0.0.5.dist-info/METADATA +119 -0
- sky_dev-0.0.5.dist-info/RECORD +39 -0
- sky_dev-0.0.5.dist-info/WHEEL +5 -0
- sky_dev-0.0.5.dist-info/entry_points.txt +2 -0
- sky_dev-0.0.5.dist-info/licenses/LICENSE +21 -0
- sky_dev-0.0.5.dist-info/top_level.txt +1 -0
sky/config/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Configuration schemas and loading utilities."""
|
|
2
|
+
|
|
3
|
+
from .schema import (
|
|
4
|
+
ApprovalRuleConfig,
|
|
5
|
+
DexProjectConfig,
|
|
6
|
+
ModelAssignmentConfig,
|
|
7
|
+
ModelRoutingConfig,
|
|
8
|
+
load_config,
|
|
9
|
+
load_models_config,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ApprovalRuleConfig",
|
|
14
|
+
"ModelAssignmentConfig",
|
|
15
|
+
"ModelRoutingConfig",
|
|
16
|
+
"DexProjectConfig",
|
|
17
|
+
"load_config",
|
|
18
|
+
"load_models_config",
|
|
19
|
+
]
|
sky/config/models.yaml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Model routing config based on empirical findings
|
|
2
|
+
#
|
|
3
|
+
# planning: Requires high intelligence and context window for reasoning over large tasks.
|
|
4
|
+
# fast_loop: Used for iterative coding tasks. Lower reasoning effort but fast generation.
|
|
5
|
+
# routing: Small, fast model to decide which agent to delegate to.
|
|
6
|
+
# coder: Focused model for code generation and refactoring.
|
|
7
|
+
# tester: Focused model for unit testing and execution verification.
|
|
8
|
+
# reviewer: High intelligence model to review changes.
|
|
9
|
+
|
|
10
|
+
roles:
|
|
11
|
+
planning:
|
|
12
|
+
model_id: "openai/gpt-oss-120b"
|
|
13
|
+
temperature: 0.3
|
|
14
|
+
fast_loop:
|
|
15
|
+
model_id: "qwen/qwen3.6-27b"
|
|
16
|
+
reasoning_effort: "none"
|
|
17
|
+
temperature: 0.1
|
|
18
|
+
routing:
|
|
19
|
+
model_id: "openai/gpt-oss-20b"
|
|
20
|
+
temperature: 0.0
|
|
21
|
+
coder:
|
|
22
|
+
model_id: "qwen/qwen3.6-27b"
|
|
23
|
+
reasoning_effort: "none"
|
|
24
|
+
temperature: 0.1
|
|
25
|
+
tester:
|
|
26
|
+
model_id: "qwen/qwen3.6-27b"
|
|
27
|
+
reasoning_effort: "none"
|
|
28
|
+
temperature: 0.1
|
|
29
|
+
reviewer:
|
|
30
|
+
model_id: "openai/gpt-oss-120b"
|
|
31
|
+
temperature: 0.3
|
|
32
|
+
|
|
33
|
+
fallback:
|
|
34
|
+
model_id: "qwen/qwen3.6-27b"
|
|
35
|
+
reasoning_effort: "none"
|
|
36
|
+
temperature: 0.1
|
|
37
|
+
|
|
38
|
+
timeout_seconds: 30
|
|
39
|
+
parallel_tool_calls: true
|
sky/config/schema.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Configuration schemas and loading utilities."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import yaml
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApprovalRuleConfig(BaseModel):
|
|
17
|
+
"""Configuration for tool call approval rules."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
|
|
21
|
+
tool_name: Optional[str] = None
|
|
22
|
+
path_pattern: Optional[str] = None
|
|
23
|
+
max_lines_changed: Optional[int] = Field(default=None, ge=0)
|
|
24
|
+
command_prefix: Optional[str] = None
|
|
25
|
+
auto_approve: bool = True
|
|
26
|
+
|
|
27
|
+
def matches(self, tool_name: str, args: Dict[str, Any]) -> bool:
|
|
28
|
+
"""Check if this rule matches a given tool call."""
|
|
29
|
+
if self.tool_name and self.tool_name != tool_name:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
if self.path_pattern:
|
|
33
|
+
# Assuming args might contain 'path' or 'file_path' indicating the file being modified
|
|
34
|
+
path = args.get("path") or args.get("file_path") or args.get("TargetFile") or args.get("AbsolutePath")
|
|
35
|
+
if not path or not fnmatch.fnmatch(str(path), self.path_pattern):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
if self.max_lines_changed is not None:
|
|
39
|
+
# Rough heuristic: count newlines in diff or content args
|
|
40
|
+
content = args.get("content") or args.get("diff") or args.get("CodeContent") or args.get("ReplacementContent")
|
|
41
|
+
if content is not None:
|
|
42
|
+
lines = len(re.findall(r"\n", str(content))) + 1
|
|
43
|
+
if lines > self.max_lines_changed:
|
|
44
|
+
return False
|
|
45
|
+
elif args.get("ReplacementChunks"):
|
|
46
|
+
lines = 0
|
|
47
|
+
for chunk in args.get("ReplacementChunks", []):
|
|
48
|
+
lines += len(re.findall(r"\n", str(chunk.get("ReplacementContent", "")))) + 1
|
|
49
|
+
if lines > self.max_lines_changed:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
if self.command_prefix:
|
|
53
|
+
# Assuming args might contain 'command' or 'CommandLine'
|
|
54
|
+
command = args.get("command") or args.get("CommandLine")
|
|
55
|
+
if not command or not str(command).startswith(self.command_prefix):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ModelInfoConfig(BaseModel):
|
|
62
|
+
"""Information about an available model."""
|
|
63
|
+
id: str
|
|
64
|
+
description: Optional[str] = None
|
|
65
|
+
context_window: Optional[int] = None
|
|
66
|
+
best_for: Optional[List[str]] = None
|
|
67
|
+
provider: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ProviderConfig(BaseModel):
|
|
71
|
+
"""Configuration for an LLM provider."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(extra="forbid")
|
|
74
|
+
|
|
75
|
+
base_url: Optional[str] = None
|
|
76
|
+
timeout: int = 60
|
|
77
|
+
requires_api_key: bool = True
|
|
78
|
+
free_tier: bool = False
|
|
79
|
+
default_model: str
|
|
80
|
+
models: List[ModelInfoConfig] = Field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ModelAssignmentConfig(BaseModel):
|
|
84
|
+
"""Configuration for assigning a specific model to a role."""
|
|
85
|
+
|
|
86
|
+
model_config = ConfigDict(extra="forbid")
|
|
87
|
+
|
|
88
|
+
provider: str = Field(default="groq")
|
|
89
|
+
model_id: str
|
|
90
|
+
reasoning_effort: Optional[Literal["none", "low", "medium", "high"]] = None
|
|
91
|
+
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
|
|
92
|
+
max_tokens: Optional[int] = Field(default=None, ge=1)
|
|
93
|
+
description: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ModelRoutingConfig(BaseModel):
|
|
97
|
+
"""Configuration for routing tasks to different models."""
|
|
98
|
+
|
|
99
|
+
model_config = ConfigDict(extra="forbid")
|
|
100
|
+
|
|
101
|
+
providers: Dict[str, ProviderConfig] = Field(default_factory=dict)
|
|
102
|
+
roles: Dict[str, ModelAssignmentConfig]
|
|
103
|
+
fallback: Optional[ModelAssignmentConfig] = None
|
|
104
|
+
timeout_seconds: int = Field(default=30, ge=1)
|
|
105
|
+
parallel_tool_calls: bool = True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DexProjectConfig(BaseModel):
|
|
109
|
+
"""Main project configuration for SKY."""
|
|
110
|
+
|
|
111
|
+
model_config = ConfigDict(extra="forbid")
|
|
112
|
+
|
|
113
|
+
project_name: str = "sky-project"
|
|
114
|
+
approval_rules: List[ApprovalRuleConfig] = Field(default_factory=list)
|
|
115
|
+
max_retries: int = Field(default=3, ge=1, le=10)
|
|
116
|
+
docker_image: str = "python:3.11-slim"
|
|
117
|
+
docker_network: str = "none"
|
|
118
|
+
max_turn_limit: int = Field(default=20, ge=1)
|
|
119
|
+
context_chunk_size: int = Field(default=1000, ge=100)
|
|
120
|
+
context_top_k: int = Field(default=5, ge=1)
|
|
121
|
+
verbose: bool = False
|
|
122
|
+
|
|
123
|
+
# Memory Settings
|
|
124
|
+
memory_enabled: bool = Field(default=True, description="Enable project memory")
|
|
125
|
+
memory_provider: str = Field(default="fastembed", description="Embedding provider (fastembed or sentence-transformers)")
|
|
126
|
+
memory_chunk_size: int = Field(default=1000, description="Tokens per chunk")
|
|
127
|
+
memory_chunk_overlap: int = Field(default=50, description="Overlap between chunks")
|
|
128
|
+
memory_top_k: int = Field(default=5, description="Number of chunks to retrieve")
|
|
129
|
+
memory_model: str = Field(default="all-MiniLM-L6-v2", description="Embedding model")
|
|
130
|
+
memory_auto_index: bool = Field(default=False, description="Auto-index on first run")
|
|
131
|
+
memory_auto_inject_on_ask: bool = Field(default=False, description="Auto-inject context on ask command")
|
|
132
|
+
memory_exclude_patterns: List[str] = Field(
|
|
133
|
+
default=[".git", "node_modules", "venv", "__pycache__", ".sky", ".pytest_cache", "*.pyc"],
|
|
134
|
+
description="Excluded directories"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Workflow Settings
|
|
138
|
+
workflow_max_retries: int = Field(default=3, description="Maximum test-and-reflect retries")
|
|
139
|
+
workflow_max_subtasks: int = Field(default=20, description="Max subtasks for decomposition")
|
|
140
|
+
workflow_parallel_limit: int = Field(default=3, description="Limit for parallel subagents")
|
|
141
|
+
workflow_enable_subagents: bool = Field(default=True, description="Enable subagents")
|
|
142
|
+
workflow_enable_test_and_reflect: bool = Field(default=True, description="Enable test and reflect")
|
|
143
|
+
workflow_checkpoint_interval: int = Field(default=10, description="Checkpoint interval")
|
|
144
|
+
|
|
145
|
+
# Security Settings
|
|
146
|
+
security_enabled: bool = Field(default=True, description="Enable security guardrails")
|
|
147
|
+
security_strict_mode: bool = Field(default=True, description="Strict mode for guardrails")
|
|
148
|
+
security_max_input_length: int = Field(default=10000, description="Max prompt length")
|
|
149
|
+
security_allowed_extensions: List[str] = Field(
|
|
150
|
+
default=[".py", ".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".sh", ".js", ".ts", ".html", ".css"],
|
|
151
|
+
description="Allowed file extensions for writing"
|
|
152
|
+
)
|
|
153
|
+
security_dangerous_commands: List[str] = Field(
|
|
154
|
+
default=["rm -rf", "dd if=", "mkfs", "format", "shred"],
|
|
155
|
+
description="Dangerous commands to block"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@field_validator("memory_exclude_patterns")
|
|
159
|
+
@classmethod
|
|
160
|
+
def validate_exclude_patterns(cls, v: List[str]) -> List[str]:
|
|
161
|
+
return [p for p in v if p.strip()]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _get_config_paths() -> Tuple[Path, Path]:
|
|
165
|
+
"""Get the paths to global and local config files."""
|
|
166
|
+
global_path = Path.home() / ".sky" / "config.yaml"
|
|
167
|
+
local_path = Path("sky.yaml")
|
|
168
|
+
return global_path, local_path
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def load_config() -> DexProjectConfig:
|
|
172
|
+
"""Load and merge global and project configurations. Project config takes precedence."""
|
|
173
|
+
global_path, local_path = _get_config_paths()
|
|
174
|
+
config_data: Dict[str, Any] = {}
|
|
175
|
+
|
|
176
|
+
if global_path.exists():
|
|
177
|
+
try:
|
|
178
|
+
with open(global_path, "r", encoding="utf-8") as f:
|
|
179
|
+
global_data = yaml.safe_load(f)
|
|
180
|
+
if global_data:
|
|
181
|
+
config_data.update(global_data)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.warning(f"Failed to load global config from {global_path}: {e}")
|
|
184
|
+
|
|
185
|
+
if local_path.exists():
|
|
186
|
+
try:
|
|
187
|
+
with open(local_path, "r", encoding="utf-8") as f:
|
|
188
|
+
local_data = yaml.safe_load(f)
|
|
189
|
+
if local_data:
|
|
190
|
+
config_data.update(local_data)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(f"Failed to load local config from {local_path}: {e}")
|
|
193
|
+
|
|
194
|
+
# Flatten memory settings if present
|
|
195
|
+
if "memory" in config_data and isinstance(config_data["memory"], dict):
|
|
196
|
+
mem_settings = config_data.pop("memory")
|
|
197
|
+
for k, v in mem_settings.items():
|
|
198
|
+
config_data[f"memory_{k}"] = v
|
|
199
|
+
|
|
200
|
+
# Flatten workflow settings if present
|
|
201
|
+
if "workflow" in config_data and isinstance(config_data["workflow"], dict):
|
|
202
|
+
wf_settings = config_data.pop("workflow")
|
|
203
|
+
for k, v in wf_settings.items():
|
|
204
|
+
config_data[f"workflow_{k}"] = v
|
|
205
|
+
|
|
206
|
+
# Flatten security settings if present
|
|
207
|
+
if "security" in config_data and isinstance(config_data["security"], dict):
|
|
208
|
+
sec_settings = config_data.pop("security")
|
|
209
|
+
for k, v in sec_settings.items():
|
|
210
|
+
config_data[f"security_{k}"] = v
|
|
211
|
+
|
|
212
|
+
config = DexProjectConfig(**config_data)
|
|
213
|
+
if os.getenv("SKY_VERBOSE", "").lower() == "true":
|
|
214
|
+
config.verbose = True
|
|
215
|
+
|
|
216
|
+
return config
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def load_models_config() -> ModelRoutingConfig:
|
|
220
|
+
"""Load model routing configuration from local or global models.yaml."""
|
|
221
|
+
local_path = Path("models.yaml")
|
|
222
|
+
global_path = Path.home() / ".sky" / "models.yaml"
|
|
223
|
+
|
|
224
|
+
target_path = local_path if local_path.exists() else global_path
|
|
225
|
+
|
|
226
|
+
if target_path.exists():
|
|
227
|
+
try:
|
|
228
|
+
with open(target_path, "r", encoding="utf-8") as f:
|
|
229
|
+
data = yaml.safe_load(f)
|
|
230
|
+
if data:
|
|
231
|
+
return ModelRoutingConfig(**data)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.warning(f"Failed to load models config from {target_path}: {e}")
|
|
234
|
+
|
|
235
|
+
# Fallback default configuration
|
|
236
|
+
return ModelRoutingConfig(
|
|
237
|
+
providers={
|
|
238
|
+
"groq": ProviderConfig(
|
|
239
|
+
base_url="https://api.groq.com/openai/v1",
|
|
240
|
+
timeout=30,
|
|
241
|
+
requires_api_key=True,
|
|
242
|
+
free_tier=True,
|
|
243
|
+
default_model="openai/gpt-oss-120b",
|
|
244
|
+
models=[
|
|
245
|
+
ModelInfoConfig(id="groq/compound-mini", description="Ultra-fast routing (0.1s)", context_window=8192, best_for=["routing", "classification"], provider="groq"),
|
|
246
|
+
ModelInfoConfig(id="openai/gpt-oss-120b", description="Best general conversation", context_window=128000, best_for=["general", "chat"], provider="groq"),
|
|
247
|
+
ModelInfoConfig(id="meta-models/Muse-Glimmer-30B", description="Dedicated reasoning & planning", context_window=32768, best_for=["planning", "reviewing"], provider="groq"),
|
|
248
|
+
ModelInfoConfig(id="qwen/qwen3.6-27b", description="Best-in-class tool calling", context_window=32768, best_for=["tool_calling", "execution"], provider="groq")
|
|
249
|
+
]
|
|
250
|
+
),
|
|
251
|
+
"nim": ProviderConfig(
|
|
252
|
+
base_url="https://integrate.api.nvidia.com/v1",
|
|
253
|
+
timeout=60,
|
|
254
|
+
requires_api_key=True,
|
|
255
|
+
free_tier=True,
|
|
256
|
+
default_model="mistralai/devstral-2",
|
|
257
|
+
models=[
|
|
258
|
+
ModelInfoConfig(id="mistralai/devstral-2", description="Purpose-built for agentic coding", context_window=131072, best_for=["coding", "tool_use"], provider="nim"),
|
|
259
|
+
ModelInfoConfig(id="nvidia/llama-3.1-nemotron-70b-instruct", description="Reliable backup model", context_window=131072, best_for=["fallback"], provider="nim")
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
},
|
|
263
|
+
roles={
|
|
264
|
+
"general": ModelAssignmentConfig(provider="groq", model_id="openai/gpt-oss-120b", temperature=0.7),
|
|
265
|
+
"planning": ModelAssignmentConfig(provider="groq", model_id="meta-models/Muse-Glimmer-30B", temperature=0.3),
|
|
266
|
+
"reviewer": ModelAssignmentConfig(provider="groq", model_id="meta-models/Muse-Glimmer-30B", temperature=0.3),
|
|
267
|
+
"routing": ModelAssignmentConfig(provider="groq", model_id="groq/compound-mini", temperature=0.0),
|
|
268
|
+
"fast_loop": ModelAssignmentConfig(provider="groq", model_id="qwen/qwen3.6-27b", temperature=0.1),
|
|
269
|
+
"coder": ModelAssignmentConfig(provider="nim", model_id="mistralai/devstral-2", temperature=0.1),
|
|
270
|
+
"tester": ModelAssignmentConfig(provider="nim", model_id="mistralai/devstral-2", temperature=0.1),
|
|
271
|
+
},
|
|
272
|
+
fallback=ModelAssignmentConfig(provider="nim", model_id="nvidia/llama-3.1-nemotron-70b-instruct", temperature=0.1),
|
|
273
|
+
timeout_seconds=30,
|
|
274
|
+
parallel_tool_calls=True,
|
|
275
|
+
)
|
sky/core/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""SKY Core logic including Fast Loop, Model Router, and Approval Gate."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .approval import ApprovalGate
|
|
7
|
+
from .fast_loop import FastLoopEngine
|
|
8
|
+
from .router import ModelRouter
|
|
9
|
+
from .workflow import WorkflowEngine, WorkflowState, WorkflowStatus
|
|
10
|
+
from .subagent import SubagentRunner, SubagentRole, SubagentConfig
|
|
11
|
+
|
|
12
|
+
def get_approval_gate(*args, **kwargs):
|
|
13
|
+
from .approval import ApprovalGate
|
|
14
|
+
return ApprovalGate(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
def get_fast_loop_engine(*args, **kwargs):
|
|
17
|
+
from .fast_loop import FastLoopEngine
|
|
18
|
+
return FastLoopEngine(*args, **kwargs)
|
|
19
|
+
|
|
20
|
+
def get_model_router(*args, **kwargs):
|
|
21
|
+
from .router import ModelRouter
|
|
22
|
+
return ModelRouter(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def get_workflow_engine(*args, **kwargs):
|
|
25
|
+
from .workflow import WorkflowEngine
|
|
26
|
+
return WorkflowEngine(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
def get_subagent_runner(*args, **kwargs):
|
|
29
|
+
from .subagent import SubagentRunner
|
|
30
|
+
return SubagentRunner(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
def get_mode_prompt(*args, **kwargs):
|
|
33
|
+
from .mode_prompts import get_mode_prompt as _get_mode_prompt
|
|
34
|
+
return _get_mode_prompt(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ApprovalGate",
|
|
38
|
+
"FastLoopEngine",
|
|
39
|
+
"ModelRouter",
|
|
40
|
+
"get_mode_prompt",
|
|
41
|
+
"WorkflowEngine",
|
|
42
|
+
"WorkflowState",
|
|
43
|
+
"WorkflowStatus",
|
|
44
|
+
"SubagentRunner",
|
|
45
|
+
"SubagentRole",
|
|
46
|
+
"SubagentConfig",
|
|
47
|
+
"ROLE_CONFIGS",
|
|
48
|
+
]
|
sky/core/approval.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Human Approval Gate for intercepting and approving destructive tool calls."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.prompt import Prompt
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from sky.config import DexProjectConfig
|
|
13
|
+
from sky.storage import DatabaseManager
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
class ApprovalGate:
|
|
18
|
+
"""Manages manual and automatic approvals for tool executions."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: DexProjectConfig, db: DatabaseManager, session_id: str):
|
|
21
|
+
self.config = config
|
|
22
|
+
self.db = db
|
|
23
|
+
self.session_id = session_id
|
|
24
|
+
|
|
25
|
+
def check_auto_approve(self, tool_name: str, args: Dict[str, Any]) -> Optional[bool]:
|
|
26
|
+
"""Check if a tool call matches any auto-approve rules in the config."""
|
|
27
|
+
for rule in self.config.approval_rules:
|
|
28
|
+
if not rule.auto_approve:
|
|
29
|
+
continue
|
|
30
|
+
if rule.matches(tool_name, args):
|
|
31
|
+
return True
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def render_diff(self, content: str, filepath: str) -> Syntax:
|
|
35
|
+
"""Render unified diff with syntax highlighting."""
|
|
36
|
+
return Syntax(content, "diff", theme="monokai", line_numbers=True)
|
|
37
|
+
|
|
38
|
+
def render_command(self, command: str) -> Syntax:
|
|
39
|
+
"""Render bash command with syntax highlighting."""
|
|
40
|
+
return Syntax(command, "bash", theme="monokai", line_numbers=False)
|
|
41
|
+
|
|
42
|
+
def request_approval(self, tool_name: str, args: Dict[str, Any]) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
|
|
43
|
+
"""Request manual approval via rich interactive prompt."""
|
|
44
|
+
console.print("\n")
|
|
45
|
+
table = Table(title="[bold yellow]Tool Execution Requires Approval[/bold yellow]", show_header=False)
|
|
46
|
+
table.add_column("Property", style="cyan", justify="right")
|
|
47
|
+
table.add_column("Value", style="white")
|
|
48
|
+
|
|
49
|
+
table.add_row("Tool", f"[bold]{tool_name}[/bold]")
|
|
50
|
+
|
|
51
|
+
if tool_name == "edit_file" and "diff" in args:
|
|
52
|
+
table.add_row("File", args.get("path", "unknown"))
|
|
53
|
+
console.print(table)
|
|
54
|
+
console.print(Panel(self.render_diff(args["diff"], args.get("path", "")), title="Diff Preview"))
|
|
55
|
+
elif tool_name == "bash" and "command" in args:
|
|
56
|
+
table.add_row("Command", "")
|
|
57
|
+
console.print(table)
|
|
58
|
+
console.print(Panel(self.render_command(args["command"]), title="Bash Command"))
|
|
59
|
+
else:
|
|
60
|
+
args_str = json.dumps(args, indent=2)
|
|
61
|
+
table.add_row("Arguments", args_str)
|
|
62
|
+
console.print(table)
|
|
63
|
+
|
|
64
|
+
while True:
|
|
65
|
+
response = Prompt.ask(
|
|
66
|
+
"[bold yellow]Approve execution?[/bold yellow]",
|
|
67
|
+
choices=["y", "n", "e"],
|
|
68
|
+
default="y"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if response == "y":
|
|
72
|
+
return True, "User approved", args
|
|
73
|
+
elif response == "n":
|
|
74
|
+
return False, "User rejected", None
|
|
75
|
+
elif response == "e":
|
|
76
|
+
console.print("[dim]Editing parameters is not fully supported in this stub. Rejecting.[/dim]")
|
|
77
|
+
return False, "User aborted to edit", None
|
|
78
|
+
|
|
79
|
+
def process(self, tool_name: str, args: Dict[str, Any]) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
|
|
80
|
+
"""Main entry point to process a tool call through the approval gate."""
|
|
81
|
+
# Check auto-approve first
|
|
82
|
+
if self.check_auto_approve(tool_name, args):
|
|
83
|
+
self.db.log_tool_call(self.session_id, tool_name, args, "destructive", "auto_approved")
|
|
84
|
+
return True, "Auto-approved by rules", args
|
|
85
|
+
|
|
86
|
+
# Fallback to manual approval
|
|
87
|
+
approved, reason, final_args = self.request_approval(tool_name, args)
|
|
88
|
+
|
|
89
|
+
decision = "approved" if approved else "rejected"
|
|
90
|
+
self.db.log_tool_call(self.session_id, tool_name, final_args or args, "destructive", decision, approved_by="user")
|
|
91
|
+
|
|
92
|
+
return approved, reason, final_args
|
sky/core/benchmark.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Performance benchmarking utilities."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Benchmark:
|
|
12
|
+
"""Simple performance benchmark."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def measure_startup() -> Dict[str, float]:
|
|
16
|
+
"""Measure CLI startup time."""
|
|
17
|
+
start = time.perf_counter()
|
|
18
|
+
|
|
19
|
+
# Import core modules
|
|
20
|
+
import sky.core # noqa
|
|
21
|
+
import sky.memory # noqa
|
|
22
|
+
|
|
23
|
+
end = time.perf_counter()
|
|
24
|
+
return {"startup_ms": (end - start) * 1000}
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def measure_embedding() -> Dict[str, float]:
|
|
28
|
+
"""Measure embedding generation time."""
|
|
29
|
+
from sky.memory.vectorstore import get_vector_store
|
|
30
|
+
import tempfile
|
|
31
|
+
|
|
32
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
33
|
+
store = get_vector_store(Path(tmpdir))
|
|
34
|
+
|
|
35
|
+
start = time.perf_counter()
|
|
36
|
+
embedding = store._get_embedding("test query")
|
|
37
|
+
end = time.perf_counter()
|
|
38
|
+
|
|
39
|
+
return {"embedding_ms": (end - start) * 1000, "dimension": len(embedding)}
|