skillpool 4.3.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.
- skillpool/__init__.py +74 -0
- skillpool/__main__.py +6 -0
- skillpool/adapters/__init__.py +8 -0
- skillpool/adapters/base.py +41 -0
- skillpool/adapters/claude_adapter.py +36 -0
- skillpool/adapters/codex_adapter.py +92 -0
- skillpool/adapters/hermes_adapter.py +38 -0
- skillpool/audit/__init__.py +651 -0
- skillpool/bridge/__init__.py +16 -0
- skillpool/bridge/freeze_detector.py +134 -0
- skillpool/bridge/maintenance.py +119 -0
- skillpool/bridge/wal_manager.py +136 -0
- skillpool/clawmem_client.py +176 -0
- skillpool/cli.py +700 -0
- skillpool/combiner/__init__.py +31 -0
- skillpool/combiner/lifecycle.py +453 -0
- skillpool/combiner/models.py +99 -0
- skillpool/config.py +34 -0
- skillpool/cost/__init__.py +111 -0
- skillpool/cost/audit_hash.py +51 -0
- skillpool/cost/budget_tracker.py +66 -0
- skillpool/cost/dashboard.py +189 -0
- skillpool/cost/models.py +129 -0
- skillpool/cost/token_governor.py +264 -0
- skillpool/cost/trace_ceiling.py +38 -0
- skillpool/csdf.py +126 -0
- skillpool/evolver/__init__.py +978 -0
- skillpool/gain/__init__.py +285 -0
- skillpool/gate.py +282 -0
- skillpool/gate_policy/__init__.py +31 -0
- skillpool/gate_policy/incremental.py +157 -0
- skillpool/gate_policy/parser.py +258 -0
- skillpool/gate_policy/state_machine.py +432 -0
- skillpool/graph/__init__.py +14 -0
- skillpool/graph/ppr.py +279 -0
- skillpool/health/__init__.py +73 -0
- skillpool/health/check.py +85 -0
- skillpool/health/degradation.py +90 -0
- skillpool/health/models.py +43 -0
- skillpool/hooks/__init__.py +4 -0
- skillpool/hooks/security_scanner.py +288 -0
- skillpool/lifecycle.py +150 -0
- skillpool/materializer/__init__.py +124 -0
- skillpool/materializer/budget_cropper.py +178 -0
- skillpool/materializer/csdf_loader.py +114 -0
- skillpool/materializer/lazy_loader.py +265 -0
- skillpool/materializer/lifecycle_filter.py +93 -0
- skillpool/materializer/mapper.py +178 -0
- skillpool/materializer/models.py +66 -0
- skillpool/mcp_server.py +2005 -0
- skillpool/monitor/__init__.py +576 -0
- skillpool/monitor/bug_collector.py +392 -0
- skillpool/monitor/defect_classifier.py +218 -0
- skillpool/monitor/self_healing.py +530 -0
- skillpool/monitor/telemetry_bridge.py +197 -0
- skillpool/paradigm/__init__.py +312 -0
- skillpool/paradigm/override.py +285 -0
- skillpool/profile.py +94 -0
- skillpool/quality.py +254 -0
- skillpool/registry/__init__.py +509 -0
- skillpool/registry/models.py +98 -0
- skillpool/resolver/__init__.py +320 -0
- skillpool/resolver/cache.py +103 -0
- skillpool/resolver/circuit_breaker.py +103 -0
- skillpool/resolver/conflict_detector.py +111 -0
- skillpool/resolver/health_filter.py +38 -0
- skillpool/resolver/models.py +154 -0
- skillpool/resolver/rate_limiter.py +48 -0
- skillpool/resolver/skill_graph.py +183 -0
- skillpool/review/__init__.py +242 -0
- skillpool/review/async_queue.py +96 -0
- skillpool/review/checkpoint_runner.py +345 -0
- skillpool/review/models.py +164 -0
- skillpool/review/suspect_marker.py +39 -0
- skillpool/review/veto_evaluator.py +94 -0
- skillpool/router/__init__.py +481 -0
- skillpool/schemas.py +119 -0
- skillpool/synergy/__init__.py +240 -0
- skillpool/synergy/detector.py +5 -0
- skillpool/telemetry.py +126 -0
- skillpool/utils/__init__.py +21 -0
- skillpool/utils/changelog.py +218 -0
- skillpool/utils/logger.py +273 -0
- skillpool/utils/runtime_audit.py +163 -0
- skillpool/utils/time_utils.py +13 -0
- skillpool-4.3.0.dist-info/METADATA +21 -0
- skillpool-4.3.0.dist-info/RECORD +90 -0
- skillpool-4.3.0.dist-info/WHEEL +5 -0
- skillpool-4.3.0.dist-info/entry_points.txt +3 -0
- skillpool-4.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Hook-layer security checks for SkillPool skill materialization.
|
|
2
|
+
|
|
3
|
+
Runs before any skill is materialized to ensure safety:
|
|
4
|
+
1. YAML syntax safety — no unsafe tags or constructors
|
|
5
|
+
2. Dangerous pattern scanning — exec/os.system/eval/subprocess
|
|
6
|
+
3. Signature verification — sigstore placeholder (production needs cosign)
|
|
7
|
+
|
|
8
|
+
Part of SkillPool — independent infrastructure, shared by all agents.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ThreatLevel(Enum):
|
|
20
|
+
"""Threat severity classification."""
|
|
21
|
+
|
|
22
|
+
SAFE = "safe"
|
|
23
|
+
WARNING = "warning"
|
|
24
|
+
CRITICAL = "critical"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class SecurityCheckResult:
|
|
29
|
+
"""Result of a security check on skill content."""
|
|
30
|
+
|
|
31
|
+
threat_level: ThreatLevel
|
|
32
|
+
checks_passed: list[str] = field(default_factory=list)
|
|
33
|
+
warnings: list[str] = field(default_factory=list)
|
|
34
|
+
blockers: list[str] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_safe(self) -> bool:
|
|
38
|
+
return self.threat_level != ThreatLevel.CRITICAL
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Dangerous patterns to detect in skill content (NOT executed — regex strings for scanning only)
|
|
42
|
+
# These patterns are used to flag unsafe code in skill YAML/MD content before materialization.
|
|
43
|
+
_DANGEROUS_PATTERNS: list[tuple[str, str]] = [
|
|
44
|
+
(r"\bexec\s*\(", "exec() call — arbitrary code execution"), # NOSONAR: regex for detection, not a call
|
|
45
|
+
(r"\beval\s*\(", "eval() call — arbitrary code execution"), # NOSONAR: regex for detection, not a call
|
|
46
|
+
(r"\bos\.system\s*\(", "os.system() call — shell injection risk"), # NOSONAR: regex for detection, not a call
|
|
47
|
+
(r"\bos\.popen\s*\(", "os.popen() call — shell injection risk"), # NOSONAR: regex for detection, not a call
|
|
48
|
+
(r"\bsubprocess\.\w+\s*\(", "subprocess call — external process execution"),
|
|
49
|
+
(r"\b__import__\s*\(", "__import__() call — dynamic import risk"),
|
|
50
|
+
(r"\bcompile\s*\(", "compile() call — dynamic code compilation"),
|
|
51
|
+
(r"\bopen\s*\(.*['\"]w", "file write — potential data modification"),
|
|
52
|
+
(r"\bshutil\.rmtree\s*\(", "shutil.rmtree() — directory deletion"),
|
|
53
|
+
(r"\bos\.remove\s*\(", "os.remove() — file deletion"),
|
|
54
|
+
(r"\bos\.unlink\s*\(", "os.unlink() — file deletion"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# Safe contexts that should be excluded from dangerous matches
|
|
58
|
+
_SAFE_CONTEXTS = [
|
|
59
|
+
r"#\s*", # commented out code
|
|
60
|
+
r'"""', # inside docstring
|
|
61
|
+
r"'''", # inside docstring
|
|
62
|
+
r"re\.compile", # re.compile() is safe (not builtins.compile)
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _extract_code_blocks(content: str) -> str:
|
|
67
|
+
"""Extract code blocks from Markdown content for scanning.
|
|
68
|
+
|
|
69
|
+
Handles:
|
|
70
|
+
- Triple-backtick fences (```...```)
|
|
71
|
+
- Tilde fences (~~~...~~~)
|
|
72
|
+
- Unclosed code blocks (fallback: scan entire content)
|
|
73
|
+
- CRLF line endings
|
|
74
|
+
- Multiple consecutive code blocks
|
|
75
|
+
"""
|
|
76
|
+
# Normalize CRLF to LF
|
|
77
|
+
normalized = content.replace("\r\n", "\n")
|
|
78
|
+
|
|
79
|
+
# Line-by-line parser: correctly handles multiple blocks
|
|
80
|
+
blocks: list[str] = []
|
|
81
|
+
in_block = False
|
|
82
|
+
fence_char: str | None = None
|
|
83
|
+
fence_len = 0
|
|
84
|
+
current_lines: list[str] = []
|
|
85
|
+
|
|
86
|
+
for line in normalized.split("\n"):
|
|
87
|
+
stripped = line.strip()
|
|
88
|
+
is_fence = False
|
|
89
|
+
detected_char: str | None = None
|
|
90
|
+
detected_len = 0
|
|
91
|
+
|
|
92
|
+
if stripped.startswith("```"):
|
|
93
|
+
detected_char = "`"
|
|
94
|
+
for ch in stripped:
|
|
95
|
+
if ch == "`":
|
|
96
|
+
detected_len += 1
|
|
97
|
+
else:
|
|
98
|
+
break
|
|
99
|
+
is_fence = detected_len >= 3
|
|
100
|
+
elif stripped.startswith("~~~"):
|
|
101
|
+
detected_char = "~"
|
|
102
|
+
for ch in stripped:
|
|
103
|
+
if ch == "~":
|
|
104
|
+
detected_len += 1
|
|
105
|
+
else:
|
|
106
|
+
break
|
|
107
|
+
is_fence = detected_len >= 3
|
|
108
|
+
|
|
109
|
+
if is_fence:
|
|
110
|
+
if not in_block:
|
|
111
|
+
# Opening fence
|
|
112
|
+
in_block = True
|
|
113
|
+
fence_char = detected_char
|
|
114
|
+
fence_len = detected_len
|
|
115
|
+
current_lines = []
|
|
116
|
+
elif detected_char == fence_char and detected_len >= fence_len:
|
|
117
|
+
# Closing fence matching opening
|
|
118
|
+
blocks.append("\n".join(current_lines))
|
|
119
|
+
in_block = False
|
|
120
|
+
current_lines = []
|
|
121
|
+
else:
|
|
122
|
+
# Different fence inside a block — treat as content
|
|
123
|
+
current_lines.append(line)
|
|
124
|
+
elif in_block:
|
|
125
|
+
current_lines.append(line)
|
|
126
|
+
|
|
127
|
+
# Handle unclosed fence
|
|
128
|
+
if in_block and current_lines:
|
|
129
|
+
blocks.append("\n".join(current_lines))
|
|
130
|
+
|
|
131
|
+
# If no fenced blocks found, try inline code
|
|
132
|
+
if not blocks:
|
|
133
|
+
inline_blocks = re.findall(r"`([^`]+)`", normalized)
|
|
134
|
+
if inline_blocks:
|
|
135
|
+
return "\n".join(inline_blocks)
|
|
136
|
+
# No code blocks at all — scan the entire content
|
|
137
|
+
return normalized
|
|
138
|
+
|
|
139
|
+
return "\n".join(blocks)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SecurityScanner:
|
|
143
|
+
"""Scans skill content for security threats before materialization.
|
|
144
|
+
|
|
145
|
+
Part of SkillPool — independent infrastructure, shared by all agents.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
custom_patterns: list[tuple[str, str]] | None = None,
|
|
151
|
+
evidence_tier: str | None = None,
|
|
152
|
+
):
|
|
153
|
+
self._patterns = _DANGEROUS_PATTERNS.copy()
|
|
154
|
+
if custom_patterns:
|
|
155
|
+
self._patterns.extend(custom_patterns)
|
|
156
|
+
self._evidence_tier = evidence_tier
|
|
157
|
+
|
|
158
|
+
def check_yaml_safety(self, content: str) -> SecurityCheckResult:
|
|
159
|
+
"""Check YAML content for unsafe constructs.
|
|
160
|
+
|
|
161
|
+
Blocks:
|
|
162
|
+
- YAML tags that invoke Python constructors (!!python/object, etc.)
|
|
163
|
+
- Custom YAML constructors that aren't in the safe list
|
|
164
|
+
"""
|
|
165
|
+
result = SecurityCheckResult(threat_level=ThreatLevel.SAFE)
|
|
166
|
+
result.checks_passed.append("yaml_syntax")
|
|
167
|
+
|
|
168
|
+
# Check for dangerous YAML tags
|
|
169
|
+
dangerous_tags = [
|
|
170
|
+
"!!python/object",
|
|
171
|
+
"!!python/object/apply",
|
|
172
|
+
"!!python/object/new",
|
|
173
|
+
"!!python/module",
|
|
174
|
+
"!!python/name",
|
|
175
|
+
"!!python/object/subclass",
|
|
176
|
+
]
|
|
177
|
+
for tag in dangerous_tags:
|
|
178
|
+
if tag in content:
|
|
179
|
+
result.blockers.append(f"Dangerous YAML tag: {tag}")
|
|
180
|
+
result.threat_level = ThreatLevel.CRITICAL
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
def scan_dangerous_patterns(self, content: str) -> SecurityCheckResult:
|
|
185
|
+
"""Scan for dangerous code patterns in skill content.
|
|
186
|
+
|
|
187
|
+
Scans code blocks and inline code for patterns that could indicate
|
|
188
|
+
security risks. Uses _extract_code_blocks for robust code block
|
|
189
|
+
extraction (handles unclosed fences, tildes, CRLF).
|
|
190
|
+
Applies _SAFE_CONTEXTS to exclude commented-out code and
|
|
191
|
+
re.compile() calls.
|
|
192
|
+
"""
|
|
193
|
+
result = SecurityCheckResult(threat_level=ThreatLevel.SAFE)
|
|
194
|
+
result.checks_passed.append("pattern_scan")
|
|
195
|
+
|
|
196
|
+
# Extract code blocks for scanning
|
|
197
|
+
scan_text = _extract_code_blocks(content)
|
|
198
|
+
|
|
199
|
+
for pattern, description in self._patterns:
|
|
200
|
+
matches = list(re.finditer(pattern, scan_text))
|
|
201
|
+
for match in matches:
|
|
202
|
+
# Get the line containing the match
|
|
203
|
+
line_start = scan_text.rfind("\n", 0, match.start()) + 1
|
|
204
|
+
line_end = scan_text.find("\n", match.end())
|
|
205
|
+
if line_end == -1:
|
|
206
|
+
line_end = len(scan_text)
|
|
207
|
+
line = scan_text[line_start:line_end].strip()
|
|
208
|
+
|
|
209
|
+
# Skip if in a safe context (commented out, re.compile, etc.)
|
|
210
|
+
is_safe_context = False
|
|
211
|
+
for safe_pattern in _SAFE_CONTEXTS:
|
|
212
|
+
if re.search(safe_pattern, line):
|
|
213
|
+
is_safe_context = True
|
|
214
|
+
break
|
|
215
|
+
if is_safe_context:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
result.warnings.append(f"{description} at position {match.start()}: {match.group()}")
|
|
219
|
+
# Patterns that are always critical
|
|
220
|
+
critical_patterns = {r"\bexec\s*\(", r"\beval\s*\(", r"\bos\.system\s*\("}
|
|
221
|
+
if pattern in critical_patterns:
|
|
222
|
+
result.blockers.append(f"Critical: {description}")
|
|
223
|
+
result.threat_level = ThreatLevel.CRITICAL
|
|
224
|
+
elif result.threat_level == ThreatLevel.SAFE:
|
|
225
|
+
result.threat_level = ThreatLevel.WARNING
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
def verify_signature(self, skill_path: Path) -> SecurityCheckResult:
|
|
230
|
+
"""Verify skill signature.
|
|
231
|
+
|
|
232
|
+
Tier-dependent behavior:
|
|
233
|
+
- dev: placeholder passes with informational note
|
|
234
|
+
- prod: BLOCKS materialization — requires cosign/sigstore signature
|
|
235
|
+
- ci: WARNING (strict mode, signature required but may use test key)
|
|
236
|
+
"""
|
|
237
|
+
import os
|
|
238
|
+
|
|
239
|
+
tier = getattr(self, "_evidence_tier", None) or os.environ.get("SKILLPOOL_EVIDENCE_TIER", "dev")
|
|
240
|
+
result = SecurityCheckResult(threat_level=ThreatLevel.SAFE)
|
|
241
|
+
result.checks_passed.append("signature_check")
|
|
242
|
+
|
|
243
|
+
if tier == "prod":
|
|
244
|
+
# Production: signature is REQUIRED — block if not verified
|
|
245
|
+
result.blockers.append(
|
|
246
|
+
"Signature verification REQUIRED in prod tier — configure cosign/sigstore before deployment"
|
|
247
|
+
)
|
|
248
|
+
result.threat_level = ThreatLevel.CRITICAL
|
|
249
|
+
elif tier == "ci":
|
|
250
|
+
result.warnings.append(
|
|
251
|
+
"Signature verification is a placeholder in CI — production deployment requires cosign/sigstore"
|
|
252
|
+
)
|
|
253
|
+
result.threat_level = ThreatLevel.WARNING
|
|
254
|
+
else:
|
|
255
|
+
result.warnings.append(
|
|
256
|
+
"Signature verification skipped (dev tier) — set SKILLPOOL_EVIDENCE_TIER=prod for strict checking"
|
|
257
|
+
)
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
def full_check(self, content: str, skill_path: Path | None = None) -> SecurityCheckResult:
|
|
261
|
+
"""Run all security checks and return aggregated result.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
content: The SKILL.md or CSDF YAML content to check.
|
|
265
|
+
skill_path: Optional path to the skill directory for signature verification.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Aggregated SecurityCheckResult with the highest threat level found.
|
|
269
|
+
"""
|
|
270
|
+
results = [
|
|
271
|
+
self.check_yaml_safety(content),
|
|
272
|
+
self.scan_dangerous_patterns(content),
|
|
273
|
+
]
|
|
274
|
+
if skill_path:
|
|
275
|
+
results.append(self.verify_signature(skill_path))
|
|
276
|
+
|
|
277
|
+
# Aggregate: take the highest threat level
|
|
278
|
+
aggregated = SecurityCheckResult(threat_level=ThreatLevel.SAFE)
|
|
279
|
+
for r in results:
|
|
280
|
+
if r.threat_level == ThreatLevel.CRITICAL:
|
|
281
|
+
aggregated.threat_level = ThreatLevel.CRITICAL
|
|
282
|
+
elif r.threat_level == ThreatLevel.WARNING and aggregated.threat_level == ThreatLevel.SAFE:
|
|
283
|
+
aggregated.threat_level = ThreatLevel.WARNING
|
|
284
|
+
aggregated.checks_passed.extend(r.checks_passed)
|
|
285
|
+
aggregated.warnings.extend(r.warnings)
|
|
286
|
+
aggregated.blockers.extend(r.blockers)
|
|
287
|
+
|
|
288
|
+
return aggregated
|
skillpool/lifecycle.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Lifecycle State Machine — 9-state enumeration with transition validation.
|
|
3
|
+
|
|
4
|
+
States: DRAFT → PROPOSED → UNDER_REVIEW → APPROVED → ACTIVE → DEPRECATED → ARCHIVED → REMOVED
|
|
5
|
+
↗ ↘
|
|
6
|
+
REJECTED ←───────────────────────────────────────────────────────────────────────
|
|
7
|
+
↓
|
|
8
|
+
DRAFT (rework cycle)
|
|
9
|
+
|
|
10
|
+
Terminal state: REMOVED (no outgoing transitions)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from enum import IntEnum
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SkillLifecycleState(IntEnum):
|
|
18
|
+
"""9-state lifecycle for skills in the pool."""
|
|
19
|
+
|
|
20
|
+
DRAFT = 0
|
|
21
|
+
PROPOSED = 1
|
|
22
|
+
UNDER_REVIEW = 2
|
|
23
|
+
APPROVED = 3
|
|
24
|
+
REJECTED = 4
|
|
25
|
+
ACTIVE = 5
|
|
26
|
+
DEPRECATED = 6
|
|
27
|
+
ARCHIVED = 7
|
|
28
|
+
REMOVED = 8
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Transition table: from_state → set of valid to_states
|
|
32
|
+
_TRANSITIONS: dict[SkillLifecycleState, list[SkillLifecycleState]] = {
|
|
33
|
+
SkillLifecycleState.DRAFT: [
|
|
34
|
+
SkillLifecycleState.PROPOSED,
|
|
35
|
+
SkillLifecycleState.REMOVED,
|
|
36
|
+
],
|
|
37
|
+
SkillLifecycleState.PROPOSED: [
|
|
38
|
+
SkillLifecycleState.UNDER_REVIEW,
|
|
39
|
+
SkillLifecycleState.DRAFT,
|
|
40
|
+
SkillLifecycleState.REMOVED,
|
|
41
|
+
],
|
|
42
|
+
SkillLifecycleState.UNDER_REVIEW: [
|
|
43
|
+
SkillLifecycleState.APPROVED,
|
|
44
|
+
SkillLifecycleState.REJECTED,
|
|
45
|
+
SkillLifecycleState.PROPOSED,
|
|
46
|
+
],
|
|
47
|
+
SkillLifecycleState.APPROVED: [
|
|
48
|
+
SkillLifecycleState.ACTIVE,
|
|
49
|
+
SkillLifecycleState.REJECTED,
|
|
50
|
+
],
|
|
51
|
+
SkillLifecycleState.REJECTED: [
|
|
52
|
+
SkillLifecycleState.DRAFT,
|
|
53
|
+
SkillLifecycleState.REMOVED,
|
|
54
|
+
],
|
|
55
|
+
SkillLifecycleState.ACTIVE: [
|
|
56
|
+
SkillLifecycleState.DEPRECATED,
|
|
57
|
+
SkillLifecycleState.ARCHIVED,
|
|
58
|
+
],
|
|
59
|
+
SkillLifecycleState.DEPRECATED: [
|
|
60
|
+
SkillLifecycleState.ARCHIVED,
|
|
61
|
+
SkillLifecycleState.ACTIVE,
|
|
62
|
+
SkillLifecycleState.REMOVED,
|
|
63
|
+
],
|
|
64
|
+
SkillLifecycleState.ARCHIVED: [
|
|
65
|
+
SkillLifecycleState.ACTIVE,
|
|
66
|
+
SkillLifecycleState.REMOVED,
|
|
67
|
+
],
|
|
68
|
+
SkillLifecycleState.REMOVED: [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def validate_transition(
|
|
73
|
+
from_state: SkillLifecycleState,
|
|
74
|
+
to_state: SkillLifecycleState,
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Check if a transition from from_state to to_state is valid."""
|
|
77
|
+
if from_state == to_state:
|
|
78
|
+
return False
|
|
79
|
+
return to_state in _TRANSITIONS.get(from_state, [])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_valid_transitions(
|
|
83
|
+
from_state: SkillLifecycleState,
|
|
84
|
+
) -> list[SkillLifecycleState]:
|
|
85
|
+
"""Return sorted list of valid target states from from_state."""
|
|
86
|
+
return sorted(_TRANSITIONS.get(from_state, []), key=lambda s: s.value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_terminal(state: SkillLifecycleState) -> bool:
|
|
90
|
+
"""Check if a state is terminal (no outgoing transitions)."""
|
|
91
|
+
return len(_TRANSITIONS.get(state, [])) == 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_state_name(state: SkillLifecycleState) -> str:
|
|
95
|
+
"""Return lowercase snake_case name of a state."""
|
|
96
|
+
return state.name.lower()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_state(name: str) -> Optional[SkillLifecycleState]:
|
|
100
|
+
"""Parse a state name (case-insensitive) into SkillLifecycleState.
|
|
101
|
+
Returns None if the name is not a valid state.
|
|
102
|
+
"""
|
|
103
|
+
if not name:
|
|
104
|
+
return None
|
|
105
|
+
upper = name.strip().upper()
|
|
106
|
+
try:
|
|
107
|
+
return SkillLifecycleState[upper]
|
|
108
|
+
except KeyError:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_auto_deprecation(skill_id: str) -> bool:
|
|
113
|
+
"""Check if an ACTIVE skill should auto-transition to DEPRECATED.
|
|
114
|
+
|
|
115
|
+
Based on execution feedback data from GainTracker:
|
|
116
|
+
- avg_effectiveness < 3.0 with >= 5 executions → deprecate
|
|
117
|
+
|
|
118
|
+
Also cascades: skill deprecated → all its PROMOTED combinations deprecated.
|
|
119
|
+
|
|
120
|
+
Returns True if auto-deprecated, False otherwise.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
from skillpool.gain import GainTracker
|
|
124
|
+
from skillpool.combiner import CombinationLifecycleManager
|
|
125
|
+
|
|
126
|
+
tracker = GainTracker()
|
|
127
|
+
report = tracker.report(skill_id, last_n=50)
|
|
128
|
+
|
|
129
|
+
# No executions — not enough data to deprecate
|
|
130
|
+
if report.execution_count == 0:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# Low effectiveness with sufficient data → deprecate
|
|
134
|
+
if report.avg_effectiveness < 3.0 and report.execution_count >= 5:
|
|
135
|
+
# Cascade: deprecate all PROMOTED combinations involving this skill
|
|
136
|
+
lifecycle_mgr = CombinationLifecycleManager()
|
|
137
|
+
combos = lifecycle_mgr.get_combinations_for_skill(skill_id)
|
|
138
|
+
for combo in combos:
|
|
139
|
+
if combo.state.value == 2: # PROMOTED
|
|
140
|
+
lifecycle_mgr.transition(
|
|
141
|
+
combo.combination_id,
|
|
142
|
+
4, # DEPRECATED
|
|
143
|
+
reason=f"Skill {skill_id} auto-deprecated (effectiveness={report.avg_effectiveness:.1f})",
|
|
144
|
+
)
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
except (ImportError, Exception):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
return False
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Materializer — CSDF→SKILL.md 实体化引擎
|
|
3
|
+
|
|
4
|
+
将 CSDF (Canonical Skill Definition Format) YAML 定义转化为
|
|
5
|
+
SKILL.md 运行时格式,供 Agent 在协作中消费。
|
|
6
|
+
|
|
7
|
+
核心流程:
|
|
8
|
+
CSDF YAML → Mapper(14条规则) → Lifecycle Filter → Budget Cropper → SKILL.md
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from skillpool.materializer.models import (
|
|
17
|
+
MaterializationResult,
|
|
18
|
+
MaterializedSkill,
|
|
19
|
+
CSDFDocument as CSDFDocument,
|
|
20
|
+
)
|
|
21
|
+
from skillpool.materializer.mapper import CSDFMapper
|
|
22
|
+
from skillpool.materializer.lifecycle_filter import LifecycleFilter
|
|
23
|
+
from skillpool.materializer.budget_cropper import BudgetCropper
|
|
24
|
+
from skillpool.profile import AgentCapabilityProfile
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Materializer:
|
|
28
|
+
"""CSDF → SKILL.md 实体化引擎主类。
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
mat = Materializer(profile=CLAUDE_CODE_PROFILE)
|
|
32
|
+
result = mat.materialize(csdf_path=Path("S05a-security-transport.yaml"))
|
|
33
|
+
print(result.skill.markdown)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
profile: AgentCapabilityProfile,
|
|
39
|
+
context_budget: int = 4096,
|
|
40
|
+
strict_lifecycle: bool = True,
|
|
41
|
+
):
|
|
42
|
+
self.profile = profile
|
|
43
|
+
self.context_budget = context_budget
|
|
44
|
+
self.mapper = CSDFMapper()
|
|
45
|
+
self.lifecycle_filter = LifecycleFilter(strict=strict_lifecycle)
|
|
46
|
+
self.budget_cropper = BudgetCropper(max_tokens=context_budget)
|
|
47
|
+
|
|
48
|
+
def materialize(
|
|
49
|
+
self,
|
|
50
|
+
csdf_path: Optional[Path] = None,
|
|
51
|
+
csdf_dict: Optional[dict] = None,
|
|
52
|
+
) -> MaterializationResult:
|
|
53
|
+
"""执行 CSDF → SKILL.md 实体化。
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
csdf_path: CSDF YAML 文件路径
|
|
57
|
+
csdf_dict: CSDF 字典(与 csdf_path 二选一)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
MaterializationResult 包含结果状态和实体化后的 SKILL.md
|
|
61
|
+
"""
|
|
62
|
+
# 1. 加载 CSDF
|
|
63
|
+
csdf = self._load_csdf(csdf_path, csdf_dict)
|
|
64
|
+
|
|
65
|
+
# 2. 能力匹配检查
|
|
66
|
+
can_exec, reason = self.profile.can_execute(
|
|
67
|
+
{
|
|
68
|
+
"required_capabilities": csdf.get("required_agent_capabilities", set()),
|
|
69
|
+
"min_trust_level": csdf.get("min_trust_level", 0),
|
|
70
|
+
"paradigm": csdf.get("paradigm"),
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
if not can_exec:
|
|
74
|
+
return MaterializationResult(
|
|
75
|
+
status="rejected",
|
|
76
|
+
skill=None,
|
|
77
|
+
errors=[f"capability mismatch: {reason}"],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# 3. 映射规则 (14 条)
|
|
81
|
+
skill_md = self.mapper.map(csdf)
|
|
82
|
+
|
|
83
|
+
# 4. Lifecycle 过滤
|
|
84
|
+
skill_md = self.lifecycle_filter.filter(skill_md, csdf)
|
|
85
|
+
|
|
86
|
+
# 5. Budget 裁剪
|
|
87
|
+
skill_md = self.budget_cropper.crop(skill_md)
|
|
88
|
+
|
|
89
|
+
# 6. 组装结果
|
|
90
|
+
skill = MaterializedSkill(
|
|
91
|
+
id=csdf.get("id", "unknown"),
|
|
92
|
+
name=csdf.get("name", "unknown"),
|
|
93
|
+
version=csdf.get("version", "0.0.0"),
|
|
94
|
+
dimension=csdf.get("dimension", ""),
|
|
95
|
+
markdown=skill_md,
|
|
96
|
+
token_count=self.budget_cropper.estimate_tokens(skill_md),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return MaterializationResult(
|
|
100
|
+
status="success",
|
|
101
|
+
skill=skill,
|
|
102
|
+
errors=[],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def materialize_batch(
|
|
106
|
+
self,
|
|
107
|
+
csdf_paths: list[Path],
|
|
108
|
+
) -> list[MaterializationResult]:
|
|
109
|
+
"""批量实体化多个 CSDF 文件。"""
|
|
110
|
+
return [self.materialize(csdf_path=p) for p in csdf_paths]
|
|
111
|
+
|
|
112
|
+
def _load_csdf(
|
|
113
|
+
self,
|
|
114
|
+
csdf_path: Optional[Path],
|
|
115
|
+
csdf_dict: Optional[dict],
|
|
116
|
+
) -> dict:
|
|
117
|
+
"""加载 CSDF 文档,优先用 dict,否则从文件读取。"""
|
|
118
|
+
if csdf_dict is not None:
|
|
119
|
+
return csdf_dict
|
|
120
|
+
if csdf_path is not None:
|
|
121
|
+
import yaml
|
|
122
|
+
|
|
123
|
+
return yaml.safe_load(csdf_path.read_text())
|
|
124
|
+
raise ValueError("必须提供 csdf_path 或 csdf_dict")
|