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.
Files changed (90) hide show
  1. skillpool/__init__.py +74 -0
  2. skillpool/__main__.py +6 -0
  3. skillpool/adapters/__init__.py +8 -0
  4. skillpool/adapters/base.py +41 -0
  5. skillpool/adapters/claude_adapter.py +36 -0
  6. skillpool/adapters/codex_adapter.py +92 -0
  7. skillpool/adapters/hermes_adapter.py +38 -0
  8. skillpool/audit/__init__.py +651 -0
  9. skillpool/bridge/__init__.py +16 -0
  10. skillpool/bridge/freeze_detector.py +134 -0
  11. skillpool/bridge/maintenance.py +119 -0
  12. skillpool/bridge/wal_manager.py +136 -0
  13. skillpool/clawmem_client.py +176 -0
  14. skillpool/cli.py +700 -0
  15. skillpool/combiner/__init__.py +31 -0
  16. skillpool/combiner/lifecycle.py +453 -0
  17. skillpool/combiner/models.py +99 -0
  18. skillpool/config.py +34 -0
  19. skillpool/cost/__init__.py +111 -0
  20. skillpool/cost/audit_hash.py +51 -0
  21. skillpool/cost/budget_tracker.py +66 -0
  22. skillpool/cost/dashboard.py +189 -0
  23. skillpool/cost/models.py +129 -0
  24. skillpool/cost/token_governor.py +264 -0
  25. skillpool/cost/trace_ceiling.py +38 -0
  26. skillpool/csdf.py +126 -0
  27. skillpool/evolver/__init__.py +978 -0
  28. skillpool/gain/__init__.py +285 -0
  29. skillpool/gate.py +282 -0
  30. skillpool/gate_policy/__init__.py +31 -0
  31. skillpool/gate_policy/incremental.py +157 -0
  32. skillpool/gate_policy/parser.py +258 -0
  33. skillpool/gate_policy/state_machine.py +432 -0
  34. skillpool/graph/__init__.py +14 -0
  35. skillpool/graph/ppr.py +279 -0
  36. skillpool/health/__init__.py +73 -0
  37. skillpool/health/check.py +85 -0
  38. skillpool/health/degradation.py +90 -0
  39. skillpool/health/models.py +43 -0
  40. skillpool/hooks/__init__.py +4 -0
  41. skillpool/hooks/security_scanner.py +288 -0
  42. skillpool/lifecycle.py +150 -0
  43. skillpool/materializer/__init__.py +124 -0
  44. skillpool/materializer/budget_cropper.py +178 -0
  45. skillpool/materializer/csdf_loader.py +114 -0
  46. skillpool/materializer/lazy_loader.py +265 -0
  47. skillpool/materializer/lifecycle_filter.py +93 -0
  48. skillpool/materializer/mapper.py +178 -0
  49. skillpool/materializer/models.py +66 -0
  50. skillpool/mcp_server.py +2005 -0
  51. skillpool/monitor/__init__.py +576 -0
  52. skillpool/monitor/bug_collector.py +392 -0
  53. skillpool/monitor/defect_classifier.py +218 -0
  54. skillpool/monitor/self_healing.py +530 -0
  55. skillpool/monitor/telemetry_bridge.py +197 -0
  56. skillpool/paradigm/__init__.py +312 -0
  57. skillpool/paradigm/override.py +285 -0
  58. skillpool/profile.py +94 -0
  59. skillpool/quality.py +254 -0
  60. skillpool/registry/__init__.py +509 -0
  61. skillpool/registry/models.py +98 -0
  62. skillpool/resolver/__init__.py +320 -0
  63. skillpool/resolver/cache.py +103 -0
  64. skillpool/resolver/circuit_breaker.py +103 -0
  65. skillpool/resolver/conflict_detector.py +111 -0
  66. skillpool/resolver/health_filter.py +38 -0
  67. skillpool/resolver/models.py +154 -0
  68. skillpool/resolver/rate_limiter.py +48 -0
  69. skillpool/resolver/skill_graph.py +183 -0
  70. skillpool/review/__init__.py +242 -0
  71. skillpool/review/async_queue.py +96 -0
  72. skillpool/review/checkpoint_runner.py +345 -0
  73. skillpool/review/models.py +164 -0
  74. skillpool/review/suspect_marker.py +39 -0
  75. skillpool/review/veto_evaluator.py +94 -0
  76. skillpool/router/__init__.py +481 -0
  77. skillpool/schemas.py +119 -0
  78. skillpool/synergy/__init__.py +240 -0
  79. skillpool/synergy/detector.py +5 -0
  80. skillpool/telemetry.py +126 -0
  81. skillpool/utils/__init__.py +21 -0
  82. skillpool/utils/changelog.py +218 -0
  83. skillpool/utils/logger.py +273 -0
  84. skillpool/utils/runtime_audit.py +163 -0
  85. skillpool/utils/time_utils.py +13 -0
  86. skillpool-4.3.0.dist-info/METADATA +21 -0
  87. skillpool-4.3.0.dist-info/RECORD +90 -0
  88. skillpool-4.3.0.dist-info/WHEEL +5 -0
  89. skillpool-4.3.0.dist-info/entry_points.txt +3 -0
  90. 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")