devsquad 3.6.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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
DevSquad Null Providers
|
|
5
|
+
|
|
6
|
+
Provides no-op implementations for all Protocol interfaces, used for:
|
|
7
|
+
- Degradation: auto-switch when real Provider is unavailable
|
|
8
|
+
- Test mocking: quick tests without real dependencies
|
|
9
|
+
- Development: skip certain modules to focus on core logic
|
|
10
|
+
|
|
11
|
+
Characteristics:
|
|
12
|
+
- All methods succeed silently, never raise exceptions
|
|
13
|
+
- is_available() returns False (marks as degraded implementation)
|
|
14
|
+
- No actual operations performed (no side effects)
|
|
15
|
+
|
|
16
|
+
Version: v1.0
|
|
17
|
+
Created: 2026-05-01
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Optional, Dict, Any, List, Callable
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NullCacheProvider:
|
|
27
|
+
"""
|
|
28
|
+
No-op cache implementation.
|
|
29
|
+
|
|
30
|
+
Behavior:
|
|
31
|
+
- get() always returns None (cache miss)
|
|
32
|
+
- set() succeeds silently, no actual storage
|
|
33
|
+
- clear() succeeds silently
|
|
34
|
+
- is_available() returns False
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self._call_count = 0
|
|
39
|
+
logger.info("NullCacheProvider initialized (degraded mode)")
|
|
40
|
+
|
|
41
|
+
def get(self, prompt: str, backend: str, model: str) -> Optional[str]:
|
|
42
|
+
"""Retrieve cached response (always returns None)."""
|
|
43
|
+
self._call_count += 1
|
|
44
|
+
logger.debug("NullCacheProvider.get() called (miss) - call #%d", self._call_count)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def set(self, prompt: str, response: str, backend: str, model: str, ttl: Optional[int] = None) -> None:
|
|
48
|
+
"""Store response in cache (no-op)."""
|
|
49
|
+
self._call_count += 1
|
|
50
|
+
logger.debug("NullCacheProvider.set() called (no-op) - call #%d", self._call_count)
|
|
51
|
+
|
|
52
|
+
def clear(self) -> None:
|
|
53
|
+
"""Clear cache (no-op)."""
|
|
54
|
+
logger.debug("NullCacheProvider.clear() called (no-op)")
|
|
55
|
+
|
|
56
|
+
def is_available(self) -> bool:
|
|
57
|
+
"""Check if cache is available. Returns False (degraded)."""
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
61
|
+
"""Return empty cache statistics."""
|
|
62
|
+
return {
|
|
63
|
+
"hit_count": 0,
|
|
64
|
+
"miss_count": self._call_count,
|
|
65
|
+
"hit_rate": 0.0,
|
|
66
|
+
"total_size": 0,
|
|
67
|
+
"entry_count": 0,
|
|
68
|
+
"provider_type": "null",
|
|
69
|
+
"degraded": True
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class NullRetryProvider:
|
|
74
|
+
"""
|
|
75
|
+
No-op retry implementation.
|
|
76
|
+
|
|
77
|
+
Behavior:
|
|
78
|
+
- retry_with_fallback() executes function directly, no retry
|
|
79
|
+
- On failure, calls fallback if provided
|
|
80
|
+
- is_available() returns False
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self):
|
|
84
|
+
self._call_count = 0
|
|
85
|
+
self._success_count = 0
|
|
86
|
+
self._failure_count = 0
|
|
87
|
+
self._fallback_count = 0
|
|
88
|
+
logger.info("NullRetryProvider initialized (degraded mode)")
|
|
89
|
+
|
|
90
|
+
def retry_with_fallback(
|
|
91
|
+
self,
|
|
92
|
+
func: Callable[[], Any],
|
|
93
|
+
max_attempts: int = 3,
|
|
94
|
+
fallback: Optional[Callable[[], Any]] = None
|
|
95
|
+
) -> Any:
|
|
96
|
+
"""Execute function without retry. Falls back on failure."""
|
|
97
|
+
self._call_count += 1
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
result = func()
|
|
101
|
+
self._success_count += 1
|
|
102
|
+
logger.debug("NullRetryProvider: function succeeded (no retry) - call #%d", self._call_count)
|
|
103
|
+
return result
|
|
104
|
+
except Exception as e:
|
|
105
|
+
self._failure_count += 1
|
|
106
|
+
logger.debug("NullRetryProvider: function failed (no retry) - call #%d: %s", self._call_count, e)
|
|
107
|
+
|
|
108
|
+
if fallback:
|
|
109
|
+
self._fallback_count += 1
|
|
110
|
+
logger.debug("NullRetryProvider: calling fallback - call #%d", self._call_count)
|
|
111
|
+
return fallback()
|
|
112
|
+
else:
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
def is_available(self) -> bool:
|
|
116
|
+
"""Check if retry mechanism is available. Returns False (degraded)."""
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
120
|
+
"""Return retry statistics."""
|
|
121
|
+
return {
|
|
122
|
+
"total_attempts": self._call_count,
|
|
123
|
+
"success_count": self._success_count,
|
|
124
|
+
"failure_count": self._failure_count,
|
|
125
|
+
"fallback_count": self._fallback_count,
|
|
126
|
+
"avg_attempts": 1.0,
|
|
127
|
+
"provider_type": "null",
|
|
128
|
+
"degraded": True
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class NullMonitorProvider:
|
|
133
|
+
"""
|
|
134
|
+
No-op monitoring implementation.
|
|
135
|
+
|
|
136
|
+
Behavior:
|
|
137
|
+
- record_*() succeeds silently, no actual recording
|
|
138
|
+
- generate_report() writes empty report
|
|
139
|
+
- is_available() returns False
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self):
|
|
143
|
+
self._llm_call_count = 0
|
|
144
|
+
self._agent_execution_count = 0
|
|
145
|
+
logger.info("NullMonitorProvider initialized (degraded mode)")
|
|
146
|
+
|
|
147
|
+
def record_llm_call(
|
|
148
|
+
self,
|
|
149
|
+
backend: str,
|
|
150
|
+
model: str,
|
|
151
|
+
duration: float,
|
|
152
|
+
token_count: int,
|
|
153
|
+
success: bool,
|
|
154
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Record LLM call (no-op)."""
|
|
157
|
+
self._llm_call_count += 1
|
|
158
|
+
logger.debug("NullMonitorProvider.record_llm_call() called (no-op) - call #%d", self._llm_call_count)
|
|
159
|
+
|
|
160
|
+
def record_agent_execution(
|
|
161
|
+
self,
|
|
162
|
+
agent_role: str,
|
|
163
|
+
task: str,
|
|
164
|
+
duration: float,
|
|
165
|
+
success: bool,
|
|
166
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Record agent execution (no-op)."""
|
|
169
|
+
self._agent_execution_count += 1
|
|
170
|
+
logger.debug("NullMonitorProvider.record_agent_execution() called (no-op) - call #%d", self._agent_execution_count)
|
|
171
|
+
|
|
172
|
+
def generate_report(self, output_path: str) -> None:
|
|
173
|
+
"""Generate empty performance report."""
|
|
174
|
+
logger.debug("NullMonitorProvider.generate_report() called (empty report) - path: %s", output_path)
|
|
175
|
+
try:
|
|
176
|
+
with open(output_path, "w") as f:
|
|
177
|
+
f.write("# Performance Report (Degraded Mode)\n\n")
|
|
178
|
+
f.write("Monitoring is currently unavailable (NullMonitorProvider).\n")
|
|
179
|
+
f.write("No performance data was collected.\n")
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning("NullMonitorProvider: failed to write empty report: %s", e)
|
|
182
|
+
|
|
183
|
+
def is_available(self) -> bool:
|
|
184
|
+
"""Check if monitoring is available. Returns False (degraded)."""
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
188
|
+
"""Return empty monitoring statistics."""
|
|
189
|
+
return {
|
|
190
|
+
"total_llm_calls": 0,
|
|
191
|
+
"total_agent_executions": 0,
|
|
192
|
+
"avg_llm_duration": 0.0,
|
|
193
|
+
"avg_agent_duration": 0.0,
|
|
194
|
+
"total_tokens": 0,
|
|
195
|
+
"provider_type": "null",
|
|
196
|
+
"degraded": True
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class NullMemoryProvider:
|
|
201
|
+
"""
|
|
202
|
+
No-op memory implementation.
|
|
203
|
+
|
|
204
|
+
Behavior:
|
|
205
|
+
- get_rules() returns empty list
|
|
206
|
+
- add_rule() / update_rule() / delete_rule() succeed silently
|
|
207
|
+
- is_available() returns False
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(self):
|
|
211
|
+
self._call_count = 0
|
|
212
|
+
logger.info("NullMemoryProvider initialized (degraded mode)")
|
|
213
|
+
|
|
214
|
+
def get_rules(self, user_id: str, context: Optional[Dict[str, Any]] = None) -> List[str]:
|
|
215
|
+
"""Retrieve user rules (always returns empty list)."""
|
|
216
|
+
self._call_count += 1
|
|
217
|
+
logger.debug("NullMemoryProvider.get_rules() called (empty) - user: %s, call #%d", user_id, self._call_count)
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
def add_rule(self, user_id: str, rule: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
221
|
+
"""Add user rule (no-op)."""
|
|
222
|
+
self._call_count += 1
|
|
223
|
+
logger.debug("NullMemoryProvider.add_rule() called (no-op) - user: %s, call #%d", user_id, self._call_count)
|
|
224
|
+
|
|
225
|
+
def update_rule(self, user_id: str, rule_id: str, rule: str) -> None:
|
|
226
|
+
"""Update user rule (no-op)."""
|
|
227
|
+
self._call_count += 1
|
|
228
|
+
logger.debug("NullMemoryProvider.update_rule() called (no-op) - user: %s, rule: %s, call #%d", user_id, rule_id, self._call_count)
|
|
229
|
+
|
|
230
|
+
def delete_rule(self, user_id: str, rule_id: str) -> None:
|
|
231
|
+
"""Delete user rule (no-op)."""
|
|
232
|
+
self._call_count += 1
|
|
233
|
+
logger.debug("NullMemoryProvider.delete_rule() called (no-op) - user: %s, rule: %s, call #%d", user_id, rule_id, self._call_count)
|
|
234
|
+
|
|
235
|
+
def is_available(self) -> bool:
|
|
236
|
+
"""Check if memory system is available. Returns False (degraded)."""
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
240
|
+
"""Return empty memory statistics."""
|
|
241
|
+
return {
|
|
242
|
+
"total_users": 0,
|
|
243
|
+
"total_rules": 0,
|
|
244
|
+
"avg_rules_per_user": 0.0,
|
|
245
|
+
"provider_type": "null",
|
|
246
|
+
"degraded": True
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
def match_rules(self, task_description: str, user_id: str,
|
|
250
|
+
role: Optional[str] = None, max_rules: int = 5) -> List[Dict[str, Any]]:
|
|
251
|
+
"""Match rules based on task description (always returns empty list)."""
|
|
252
|
+
self._call_count += 1
|
|
253
|
+
logger.debug("NullMemoryProvider.match_rules() called (empty) - user: %s, call #%d", user_id, self._call_count)
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
def format_rules_as_prompt(self, rules: List[Dict[str, Any]]) -> str:
|
|
257
|
+
"""Format rules as prompt text (always returns empty string)."""
|
|
258
|
+
self._call_count += 1
|
|
259
|
+
logger.debug("NullMemoryProvider.format_rules_as_prompt() called (empty) - call #%d", self._call_count)
|
|
260
|
+
return ""
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ============================================================================
|
|
264
|
+
# Factory functions
|
|
265
|
+
# ============================================================================
|
|
266
|
+
|
|
267
|
+
def get_null_cache() -> NullCacheProvider:
|
|
268
|
+
"""Get a null cache instance."""
|
|
269
|
+
return NullCacheProvider()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_null_retry() -> NullRetryProvider:
|
|
273
|
+
"""Get a null retry instance."""
|
|
274
|
+
return NullRetryProvider()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_null_monitor() -> NullMonitorProvider:
|
|
278
|
+
"""Get a null monitor instance."""
|
|
279
|
+
return NullMonitorProvider()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_null_memory() -> NullMemoryProvider:
|
|
283
|
+
"""Get a null memory instance."""
|
|
284
|
+
return NullMemoryProvider()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
__version__ = "1.0.0"
|
|
288
|
+
__all__ = [
|
|
289
|
+
"NullCacheProvider",
|
|
290
|
+
"NullRetryProvider",
|
|
291
|
+
"NullMonitorProvider",
|
|
292
|
+
"NullMemoryProvider",
|
|
293
|
+
"get_null_cache",
|
|
294
|
+
"get_null_retry",
|
|
295
|
+
"get_null_monitor",
|
|
296
|
+
"get_null_memory",
|
|
297
|
+
]
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
OperationCategory Extension for PermissionGuard (P1-2)
|
|
5
|
+
|
|
6
|
+
Adds three-tier operation classification to existing 4-level permission model:
|
|
7
|
+
- ALWAYS_SAFE: Read-only, local queries (auto-approved at most levels)
|
|
8
|
+
- NEEDS_REVIEW: Write ops, external API calls (requires confirmation or AI check)
|
|
9
|
+
- FORBIDDEN: Dangerous ops (delete, secrets, eval) (denied unless BYPASS)
|
|
10
|
+
|
|
11
|
+
Spec reference: SPEC_V35_Agent_Skills_Quality_Framework.md Section 7.2
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OperationCategory(Enum):
|
|
21
|
+
"""
|
|
22
|
+
Three-tier operation classification for fine-grained permission control.
|
|
23
|
+
|
|
24
|
+
Hierarchy:
|
|
25
|
+
ALWAYS_SAFE → Auto-approved at DEFAULT/AUTO levels
|
|
26
|
+
NEEDS_REVIEW → Requires explicit approval or AI risk assessment
|
|
27
|
+
FORBIDDEN → Blocked unless BYPASS level + explicit override
|
|
28
|
+
"""
|
|
29
|
+
ALWAYS_SAFE = "always_safe"
|
|
30
|
+
NEEDS_REVIEW = "needs_review"
|
|
31
|
+
FORBIDDEN = "forbidden"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Default classification mapping for common operations
|
|
35
|
+
OPERATION_CLASSIFICATION: Dict[str, OperationCategory] = {
|
|
36
|
+
# === Always Safe Operations ===
|
|
37
|
+
"read_config": OperationCategory.ALWAYS_SAFE,
|
|
38
|
+
"read_file": OperationCategory.ALWAYS_SAFE,
|
|
39
|
+
"read_scratchpad": OperationCategory.ALWAYS_SAFE,
|
|
40
|
+
"list_directory": OperationCategory.ALWAYS_SAFE,
|
|
41
|
+
"query_status": OperationCategory.ALWAYS_SAFE,
|
|
42
|
+
"get_role_info": OperationCategory.ALWAYS_SAFE,
|
|
43
|
+
"validate_input": OperationCategory.ALWAYS_SAFE,
|
|
44
|
+
|
|
45
|
+
# === Needs Review Operations ===
|
|
46
|
+
"write_scratchpad": OperationCategory.NEEDS_REVIEW,
|
|
47
|
+
"write_file": OperationCategory.NEEDS_REVIEW,
|
|
48
|
+
"create_file": OperationCategory.NEEDS_REVIEW,
|
|
49
|
+
"modify_file": OperationCategory.NEEDS_REVIEW,
|
|
50
|
+
"call_llm": OperationCategory.NEEDS_REVIEW,
|
|
51
|
+
"network_request": OperationCategory.NEEDS_REVIEW,
|
|
52
|
+
"git_operation": OperationCategory.NEEDS_REVIEW,
|
|
53
|
+
"modify_config": OperationCategory.NEEDS_REVIEW,
|
|
54
|
+
"install_template": OperationCategory.NEEDS_REVIEW,
|
|
55
|
+
"publish_template": OperationCategory.NEEDS_REVIEW,
|
|
56
|
+
|
|
57
|
+
# === Forbidden Operations ===
|
|
58
|
+
"delete_file": OperationCategory.FORBIDDEN,
|
|
59
|
+
"execute_shell": OperationCategory.FORBIDDEN,
|
|
60
|
+
"access_secrets": OperationCategory.FORBIDDEN,
|
|
61
|
+
"eval_code": OperationCategory.FORBIDDEN,
|
|
62
|
+
"import_module": OperationCategory.FORBIDDEN,
|
|
63
|
+
"spawn_process": OperationCategory.FORBIDDEN,
|
|
64
|
+
"modify_system_path": OperationCategory.FORBIDDEN,
|
|
65
|
+
"environment_write": OperationCategory.FORBIDDEN,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ClassifiedOperation:
|
|
71
|
+
"""An operation with its category classification."""
|
|
72
|
+
operation_id: str
|
|
73
|
+
category: OperationCategory
|
|
74
|
+
description: str
|
|
75
|
+
risk_factors: List[str]
|
|
76
|
+
requires_confirmation: bool
|
|
77
|
+
override_allowed: bool
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"operation_id": self.operation_id,
|
|
82
|
+
"category": self.category.value,
|
|
83
|
+
"description": self.description,
|
|
84
|
+
"risk_factors": self.risk_factors,
|
|
85
|
+
"requires_confirmation": self.requires_confirmation,
|
|
86
|
+
"override_allowed": self.override_allowed,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class OperationClassifier:
|
|
91
|
+
"""
|
|
92
|
+
Classifies operations into three-tier categories.
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
classifier = OperationClassifier()
|
|
96
|
+
classified = classifier.classify("delete_file", "/tmp/important.txt")
|
|
97
|
+
if classified.category == OperationCategory.FORBIDDEN:
|
|
98
|
+
# Block or escalate
|
|
99
|
+
pass
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
custom_classifications: Optional[Dict[str, OperationCategory]] = None,
|
|
105
|
+
strict_mode: bool = False,
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
Initialize classifier.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
custom_classifications: Override default classifications
|
|
112
|
+
strict_mode: If True, unknown operations are classified as FORBIDDEN
|
|
113
|
+
If False (default), unknown operations are NEEDS_REVIEW
|
|
114
|
+
"""
|
|
115
|
+
self._classifications = dict(OPERATION_CLASSIFICATION)
|
|
116
|
+
if custom_classifications:
|
|
117
|
+
self._classifications.update(custom_classifications)
|
|
118
|
+
self._strict_mode = strict_mode
|
|
119
|
+
|
|
120
|
+
def classify(
|
|
121
|
+
self,
|
|
122
|
+
operation_id: str,
|
|
123
|
+
target: Optional[str] = None,
|
|
124
|
+
context: Optional[Dict[str, Any]] = None,
|
|
125
|
+
) -> ClassifiedOperation:
|
|
126
|
+
"""
|
|
127
|
+
Classify an operation into a category.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
operation_id: The operation identifier
|
|
131
|
+
target: Optional target path/URL for context
|
|
132
|
+
context: Additional context (source role, etc.)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
ClassifiedOperation with full details
|
|
136
|
+
"""
|
|
137
|
+
base_category = self._classifications.get(operation_id)
|
|
138
|
+
|
|
139
|
+
if base_category is None:
|
|
140
|
+
if self._strict_mode:
|
|
141
|
+
base_category = OperationCategory.FORBIDDEN
|
|
142
|
+
else:
|
|
143
|
+
base_category = OperationCategory.NEEDS_REVIEW
|
|
144
|
+
|
|
145
|
+
description = self._get_description(operation_id)
|
|
146
|
+
risk_factors = self._assess_risk_factors(operation_id, target, context)
|
|
147
|
+
|
|
148
|
+
return ClassifiedOperation(
|
|
149
|
+
operation_id=operation_id,
|
|
150
|
+
category=base_category,
|
|
151
|
+
description=description,
|
|
152
|
+
risk_factors=risk_factors,
|
|
153
|
+
requires_confirmation=(
|
|
154
|
+
base_category == OperationCategory.NEEDS_REVIEW or
|
|
155
|
+
base_category == OperationCategory.FORBIDDEN
|
|
156
|
+
),
|
|
157
|
+
override_allowed=base_category != OperationCategory.FORBIDDEN,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def batch_classify(
|
|
161
|
+
self,
|
|
162
|
+
operations: List[Dict[str, Any]],
|
|
163
|
+
) -> List[ClassifiedOperation]:
|
|
164
|
+
"""
|
|
165
|
+
Classify multiple operations at once.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
operations: List of dicts with 'operation_id' and optional 'target', 'context'
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of ClassifiedOperation results
|
|
172
|
+
"""
|
|
173
|
+
return [
|
|
174
|
+
self.classify(
|
|
175
|
+
op.get('operation_id', ''),
|
|
176
|
+
op.get('target'),
|
|
177
|
+
op.get('context'),
|
|
178
|
+
)
|
|
179
|
+
for op in operations
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
def is_allowed(
|
|
183
|
+
self,
|
|
184
|
+
operation_id: str,
|
|
185
|
+
permission_level: str = "DEFAULT",
|
|
186
|
+
target: Optional[str] = None,
|
|
187
|
+
) -> tuple:
|
|
188
|
+
"""
|
|
189
|
+
Quick check if operation is allowed at given permission level.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
(allowed: bool, reason: str)
|
|
193
|
+
"""
|
|
194
|
+
classified = self.classify(operation_id, target)
|
|
195
|
+
|
|
196
|
+
if classified.category == OperationCategory.ALWAYS_SAFE:
|
|
197
|
+
return True, "Operation is always safe"
|
|
198
|
+
|
|
199
|
+
if classified.category == OperationCategory.FORBIDDEN:
|
|
200
|
+
if permission_level.upper() == "BYPASS":
|
|
201
|
+
return True, "Allowed via BYPASS override"
|
|
202
|
+
return False, f"Operation '{operation_id}' is forbidden"
|
|
203
|
+
|
|
204
|
+
if classified.category == OperationCategory.NEEDS_REVIEW:
|
|
205
|
+
if permission_level.upper() in ("AUTO", "BYPASS"):
|
|
206
|
+
return True, f"Auto-approved at {permission_level} level"
|
|
207
|
+
if permission_level.upper() == "PLAN":
|
|
208
|
+
return False, "Write operations denied in PLAN mode"
|
|
209
|
+
return True, "Requires user confirmation"
|
|
210
|
+
|
|
211
|
+
return False, "Unknown category"
|
|
212
|
+
|
|
213
|
+
def get_forbidden_operations(self) -> List[str]:
|
|
214
|
+
"""Return list of all operations classified as FORBIDDEN."""
|
|
215
|
+
return [
|
|
216
|
+
op_id
|
|
217
|
+
for op_id, cat in self._classifications.items()
|
|
218
|
+
if cat == OperationCategory.FORBIDDEN
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
def get_review_required_operations(self) -> List[str]:
|
|
222
|
+
"""Return list of all operations classified as NEEDS_REVIEW."""
|
|
223
|
+
return [
|
|
224
|
+
op_id
|
|
225
|
+
for op_id, cat in self._classifications.items()
|
|
226
|
+
if cat == OperationCategory.NEEDS_REVIEW
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
def add_custom_classification(
|
|
230
|
+
self,
|
|
231
|
+
operation_id: str,
|
|
232
|
+
category: OperationCategory,
|
|
233
|
+
):
|
|
234
|
+
"""Add or update custom operation classification."""
|
|
235
|
+
self._classifications[operation_id] = category
|
|
236
|
+
|
|
237
|
+
def _get_description(self, operation_id: str) -> str:
|
|
238
|
+
descriptions = {
|
|
239
|
+
"read_config": "Read configuration values",
|
|
240
|
+
"write_file": "Write or modify file contents",
|
|
241
|
+
"delete_file": "Delete file from filesystem",
|
|
242
|
+
"execute_shell": "Execute shell command",
|
|
243
|
+
"call_llm": "Call LLM API for inference",
|
|
244
|
+
"access_secrets": "Access secret keys or credentials",
|
|
245
|
+
"eval_code": "Evaluate arbitrary code string",
|
|
246
|
+
"read_scratchpad": "Read shared scratchpad data",
|
|
247
|
+
"write_scratchpad": "Write to shared scratchpad",
|
|
248
|
+
}
|
|
249
|
+
return descriptions.get(operation_id, f"Operation: {operation_id}")
|
|
250
|
+
|
|
251
|
+
def _assess_risk_factors(
|
|
252
|
+
self,
|
|
253
|
+
operation_id: str,
|
|
254
|
+
target: Optional[str],
|
|
255
|
+
context: Optional[Dict[str, Any]],
|
|
256
|
+
) -> List[str]:
|
|
257
|
+
factors = []
|
|
258
|
+
category = self._classifications.get(operation_id, OperationCategory.NEEDS_REVIEW)
|
|
259
|
+
|
|
260
|
+
if category == OperationCategory.FORBIDDEN:
|
|
261
|
+
factors.append("High-risk operation category")
|
|
262
|
+
|
|
263
|
+
if target:
|
|
264
|
+
dangerous_patterns = ["/etc/", "/var/", ".env", "secret", "credential"]
|
|
265
|
+
for pattern in dangerous_patterns:
|
|
266
|
+
if pattern.lower() in target.lower():
|
|
267
|
+
factors.append(f"Target contains sensitive pattern: {pattern}")
|
|
268
|
+
|
|
269
|
+
if context:
|
|
270
|
+
source_role = context.get("source_role_id", "")
|
|
271
|
+
if source_role == "solo-coder" and category == OperationCategory.FORBIDDEN:
|
|
272
|
+
factors.append("Coder attempting forbidden operation")
|
|
273
|
+
|
|
274
|
+
return factors
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def create_default_classifier() -> OperationClassifier:
|
|
278
|
+
"""Create classifier with default classifications."""
|
|
279
|
+
return OperationClassifier()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def create_strict_classifier(
|
|
283
|
+
custom_classifications: Optional[Dict[str, OperationCategory]] = None,
|
|
284
|
+
) -> OperationClassifier:
|
|
285
|
+
"""Create classifier in strict mode (unknown ops = FORBIDDEN)."""
|
|
286
|
+
return OperationClassifier(
|
|
287
|
+
custom_classifications=custom_classifications,
|
|
288
|
+
strict_mode=True,
|
|
289
|
+
)
|