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,509 @@
1
+ """Registry Layer — Skill metadata and version truth source.
2
+
3
+ Architecture constraint:
4
+ - Registry MUST NOT execute skills
5
+ - Registry stores metadata, versions, state, signatures, SBOM, licenses
6
+ - State transitions require Audit record
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__ = [
12
+ "AuditUnavailableError",
13
+ "IllegalStateTransitionError",
14
+ "PolicyDeniedError",
15
+ "ProblemDetail",
16
+ "Registry",
17
+ "SandboxRequiredError",
18
+ "SkillNotFoundError",
19
+ "SkillRecord",
20
+ "SupplyChainEvidenceMissingError",
21
+ ]
22
+
23
+ import json
24
+ import logging
25
+ import os
26
+ import sqlite3
27
+ from dataclasses import dataclass, field
28
+ from datetime import UTC, datetime
29
+ from pathlib import Path
30
+
31
+ from skillpool.registry.models import (
32
+ ProblemDetail,
33
+ RegisterSkillRequest,
34
+ RegisterSkillResponse,
35
+ SkillMetadata,
36
+ SkillStatus,
37
+ StateTransitionRequest,
38
+ StateTransitionResponse,
39
+ )
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ @dataclass
45
+ class SkillRecord:
46
+ """Internal skill record in Registry."""
47
+
48
+ metadata: SkillMetadata
49
+ created_at: datetime
50
+ updated_at: datetime
51
+ evidence: set[str] = field(default_factory=set)
52
+ audit_refs: list[str] = field(default_factory=list)
53
+
54
+ def to_dict(self) -> dict:
55
+ """Serialize SkillRecord to dict for JSON persistence."""
56
+ return {
57
+ "metadata": {
58
+ "skill_id": self.metadata.skill_id,
59
+ "name": self.metadata.name,
60
+ "version": self.metadata.version,
61
+ "status": self.metadata.status.value,
62
+ "description": self.metadata.description,
63
+ "author": self.metadata.author,
64
+ "created_at": self.metadata.created_at,
65
+ "updated_at": self.metadata.updated_at,
66
+ "tags": self.metadata.tags,
67
+ "dependencies": self.metadata.dependencies,
68
+ "security": self.metadata.security,
69
+ "quality_score": self.metadata.quality_score,
70
+ },
71
+ "created_at": self.created_at.isoformat(),
72
+ "updated_at": self.updated_at.isoformat(),
73
+ "evidence": sorted(self.evidence),
74
+ "audit_refs": list(self.audit_refs),
75
+ }
76
+
77
+ @classmethod
78
+ def from_dict(cls, data: dict) -> SkillRecord:
79
+ """Deserialize SkillRecord from dict."""
80
+ meta = data.get("metadata", {})
81
+ if isinstance(meta, dict):
82
+ status_val = meta.get("status", "draft")
83
+ try:
84
+ status_enum = SkillStatus(status_val)
85
+ except ValueError:
86
+ status_enum = SkillStatus.DRAFT
87
+ meta_obj = SkillMetadata(
88
+ skill_id=meta.get("skill_id", ""),
89
+ name=meta.get("name", ""),
90
+ version=meta.get("version", ""),
91
+ status=status_enum,
92
+ description=meta.get("description", ""),
93
+ author=meta.get("author", ""),
94
+ created_at=meta.get("created_at", ""),
95
+ updated_at=meta.get("updated_at", ""),
96
+ tags=meta.get("tags", []),
97
+ dependencies=meta.get("dependencies", []),
98
+ security=meta.get("security", {}),
99
+ quality_score=meta.get("quality_score", 0.0),
100
+ )
101
+ else:
102
+ meta_obj = meta
103
+ return cls(
104
+ metadata=meta_obj,
105
+ created_at=datetime.fromisoformat(data["created_at"]),
106
+ updated_at=datetime.fromisoformat(data["updated_at"]),
107
+ evidence=set(data.get("evidence", [])),
108
+ audit_refs=data.get("audit_refs", []),
109
+ )
110
+
111
+
112
+ # Legal state transitions
113
+ LEGAL_TRANSITIONS = {
114
+ ("draft", "imported"),
115
+ ("imported", "testing"),
116
+ ("testing", "enabled"),
117
+ ("testing", "disabled"),
118
+ ("enabled", "disabled"),
119
+ ("enabled", "deprecated"),
120
+ ("deprecated", "disabled"),
121
+ ("disabled", "testing"),
122
+ }
123
+
124
+ # Illegal transitions
125
+ ILLEGAL_TRANSITIONS = {
126
+ ("draft", "enabled"),
127
+ ("imported", "enabled"),
128
+ }
129
+
130
+ # Required evidence per environment (SLSA-aligned)
131
+ # dev=L0 (no requirements), ci=L1 (source pin + SBOM), prod=L2+ (full evidence)
132
+ SUPPLY_CHAIN_PROFILES = {
133
+ "dev": set(), # SLSA Build L0 — no requirements for local development
134
+ "ci": {"source pin", "SPDX SBOM"}, # SLSA Build L1 — provenance exists
135
+ "prod": {"SPDX SBOM", "SLSA provenance", "source pin", "signature"}, # SLSA Build L2+
136
+ }
137
+
138
+ # Default: production-level evidence (backward compatible)
139
+ REQUIRED_EVIDENCE = SUPPLY_CHAIN_PROFILES["prod"]
140
+
141
+
142
+ class AuditUnavailableError(Exception):
143
+ """Audit unavailable — fail closed."""
144
+
145
+ pass
146
+
147
+
148
+ class SupplyChainEvidenceMissingError(Exception):
149
+ """Missing SPDX/SLSA/signature evidence."""
150
+
151
+ pass
152
+
153
+
154
+ class IllegalStateTransitionError(Exception):
155
+ """Illegal lifecycle transition."""
156
+
157
+ pass
158
+
159
+
160
+ class SkillNotFoundError(Exception):
161
+ """Skill not found in Registry."""
162
+
163
+ pass
164
+
165
+
166
+ class SandboxRequiredError(Exception):
167
+ """Sandbox pass required."""
168
+
169
+ pass
170
+
171
+
172
+ class PolicyDeniedError(Exception):
173
+ """Policy approval denied."""
174
+
175
+ pass
176
+
177
+
178
+ class Registry:
179
+ """
180
+ Registry layer — skill metadata and lifecycle governance.
181
+
182
+ Hard rules:
183
+ - Requires SPDX SBOM, SLSA provenance, source pin, signature
184
+ - Audit must be available for all mutations
185
+ - State transitions follow legal paths only
186
+
187
+ Lookup supports both skill_id (e.g., "S09") and name (e.g., "resilience-degradation").
188
+ """
189
+
190
+ def __init__(self, audit_layer, registry_path: str | None = None) -> None:
191
+ self._skills: dict[str, SkillRecord] = {}
192
+ self._by_name: dict[str, str] = {} # name -> skill_id index for dual lookup
193
+ self._audit = audit_layer
194
+ self._registry_path = Path(registry_path) if registry_path else None
195
+ self._evidence_profile: str = os.environ.get("SKILLPOOL_EVIDENCE_TIER", "prod")
196
+ self._required_evidence: set[str] = SUPPLY_CHAIN_PROFILES.get(
197
+ self._evidence_profile, SUPPLY_CHAIN_PROFILES["prod"]
198
+ )
199
+ self._load()
200
+
201
+ def _load(self) -> None:
202
+ """Load registry from persistent storage if path is configured.
203
+
204
+ Supports three formats:
205
+ - .db / .sqlite → SQLite backend (preferred for production)
206
+ - .jsonl → JSONL format (one record per line, legacy)
207
+ - other → JSON object format (single dict, legacy)
208
+ """
209
+ if not self._registry_path:
210
+ return
211
+ if not self._registry_path.exists():
212
+ return
213
+ suffix = self._registry_path.suffix.lower()
214
+ try:
215
+ if suffix in (".db", ".sqlite"):
216
+ self._load_sqlite()
217
+ else:
218
+ self._load_json()
219
+ except Exception as exc:
220
+ logger.warning("Registry load failed from %s: %s", self._registry_path, exc)
221
+
222
+ def _load_sqlite(self) -> None:
223
+ """Load skills from SQLite database."""
224
+ conn = sqlite3.connect(str(self._registry_path))
225
+ try:
226
+ conn.execute("CREATE TABLE IF NOT EXISTS skills (skill_id TEXT PRIMARY KEY, data TEXT NOT NULL)")
227
+ for row in conn.execute("SELECT skill_id, data FROM skills"):
228
+ rec = SkillRecord.from_dict(json.loads(row[1]))
229
+ self._skills[row[0]] = rec
230
+ self._by_name[rec.metadata.name] = row[0]
231
+ finally:
232
+ conn.close()
233
+
234
+ def _load_json(self) -> None:
235
+ """Load skills from JSON/JSONL file."""
236
+ content = self._registry_path.read_text(encoding="utf-8").strip()
237
+ if not content:
238
+ return
239
+
240
+ # Try JSON object format first (standard Registry format)
241
+ try:
242
+ data = json.loads(content)
243
+ if isinstance(data, dict):
244
+ for sid, sdata in data.items():
245
+ rec = SkillRecord.from_dict(sdata)
246
+ self._skills[sid] = rec
247
+ self._by_name[rec.metadata.name] = sid
248
+ return
249
+ except (json.JSONDecodeError, KeyError, TypeError):
250
+ pass
251
+
252
+ # Try JSONL format (one record per line)
253
+ for line in content.splitlines():
254
+ line = line.strip()
255
+ if not line:
256
+ continue
257
+ try:
258
+ rec = SkillRecord.from_dict(json.loads(line))
259
+ self._skills[rec.metadata.skill_id] = rec
260
+ self._by_name[rec.metadata.name] = rec.metadata.skill_id
261
+ except (json.JSONDecodeError, KeyError, TypeError):
262
+ continue
263
+
264
+ def _save(self) -> None:
265
+ """Persist registry to disk if path is configured."""
266
+ if not self._registry_path:
267
+ return
268
+ suffix = self._registry_path.suffix.lower()
269
+ try:
270
+ if suffix in (".db", ".sqlite"):
271
+ self._save_sqlite()
272
+ else:
273
+ self._save_json()
274
+ except OSError as exc:
275
+ logger.warning("Registry save failed to %s: %s", self._registry_path, exc)
276
+
277
+ def _save_sqlite(self) -> None:
278
+ """Persist skills to SQLite database."""
279
+ self._registry_path.parent.mkdir(parents=True, exist_ok=True)
280
+ conn = sqlite3.connect(str(self._registry_path))
281
+ try:
282
+ conn.execute("CREATE TABLE IF NOT EXISTS skills (skill_id TEXT PRIMARY KEY, data TEXT NOT NULL)")
283
+ # Upsert all records in a single transaction
284
+ conn.execute("DELETE FROM skills")
285
+ for sid, rec in self._skills.items():
286
+ conn.execute(
287
+ "INSERT INTO skills (skill_id, data) VALUES (?, ?)",
288
+ (sid, json.dumps(rec.to_dict(), ensure_ascii=False)),
289
+ )
290
+ conn.commit()
291
+ finally:
292
+ conn.close()
293
+
294
+ def _save_json(self) -> None:
295
+ """Persist skills to JSON file (legacy format)."""
296
+ self._registry_path.parent.mkdir(parents=True, exist_ok=True)
297
+ data = {sid: rec.to_dict() for sid, rec in self._skills.items()}
298
+ self._registry_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
299
+
300
+ def _check_audit_available(self) -> bool:
301
+ """Check if Audit layer is available."""
302
+ return self._audit.is_available()
303
+
304
+ def register_candidate(
305
+ self,
306
+ request: RegisterSkillRequest,
307
+ ) -> RegisterSkillResponse:
308
+ """
309
+ Register a skill candidate into testing state.
310
+
311
+ Prerequisites:
312
+ - SPDX SBOM present
313
+ - SLSA provenance present
314
+ - Source pin present
315
+ - Signature present
316
+ - Audit available
317
+
318
+ Returns skill in 'testing' state, NOT production routable.
319
+ """
320
+ skill_id = request.skill_metadata.skill_id
321
+
322
+ if not self._check_audit_available():
323
+ raise AuditUnavailableError("Audit unavailable - cannot register skill")
324
+
325
+ security = request.skill_metadata.security
326
+ evidence = set()
327
+
328
+ if security.get("sbom_ref") or security.get("sbom"):
329
+ evidence.add("SPDX SBOM")
330
+ if security.get("provenance_ref") or security.get("provenance"):
331
+ evidence.add("SLSA provenance")
332
+ if security.get("source_pin") or security.get("source_ref") or security.get("source"):
333
+ evidence.add("source pin")
334
+ if security.get("signature_ref") or security.get("signature") or security.get("digest"):
335
+ evidence.add("signature")
336
+
337
+ missing = self._required_evidence - evidence
338
+ if missing:
339
+ raise SupplyChainEvidenceMissingError(
340
+ f"Missing required evidence: {missing}. "
341
+ f"Provided fields: {list(security.keys())}. "
342
+ f"Current profile: {self._evidence_profile} "
343
+ f"(set SKILLPOOL_EVIDENCE_TIER=dev to relax)"
344
+ )
345
+
346
+ now = datetime.now(UTC)
347
+ record = SkillRecord(
348
+ metadata=request.skill_metadata,
349
+ created_at=now,
350
+ updated_at=now,
351
+ evidence=evidence,
352
+ )
353
+ record.metadata.status = SkillStatus.TESTING
354
+
355
+ audit_ref = self._audit.append(
356
+ action="register_skill_candidate",
357
+ object_id=skill_id,
358
+ result="success",
359
+ )
360
+ record.audit_refs.append(audit_ref)
361
+
362
+ self._skills[skill_id] = record
363
+ self._by_name[request.skill_metadata.name] = skill_id
364
+ self._save()
365
+
366
+ return RegisterSkillResponse(
367
+ context=request.context,
368
+ skill_id=skill_id,
369
+ status="testing",
370
+ audit_ref=audit_ref,
371
+ )
372
+
373
+ def transition_state(
374
+ self,
375
+ skill_id: str,
376
+ request: StateTransitionRequest,
377
+ sandbox_result: str | None = None,
378
+ policy_approval: bool = False,
379
+ ) -> StateTransitionResponse:
380
+ """
381
+ Transition skill state with Audit fail-closed and prerequisites.
382
+
383
+ Prerequisites for 'enabled':
384
+ - Sandbox L1, L2, L3 pass
385
+ - Policy approval exists
386
+ - Audit available
387
+ """
388
+ if not self._check_audit_available():
389
+ raise AuditUnavailableError("Audit unavailable - cannot transition state")
390
+
391
+ record = self._skills.get(skill_id)
392
+ if not record:
393
+ raise SkillNotFoundError(f"Skill not found: {skill_id}")
394
+
395
+ from_status = request.from_status.value
396
+ to_status = request.to_status.value
397
+
398
+ if record.metadata.status.value != from_status:
399
+ raise IllegalStateTransitionError(
400
+ f"Current state {record.metadata.status.value} != requested {from_status}"
401
+ )
402
+
403
+ if (from_status, to_status) in ILLEGAL_TRANSITIONS:
404
+ self._audit.append(
405
+ action="illegal_state_transition",
406
+ object_id=skill_id,
407
+ result="denied",
408
+ )
409
+ raise IllegalStateTransitionError(f"Illegal transition: {from_status} -> {to_status}")
410
+
411
+ if (from_status, to_status) not in LEGAL_TRANSITIONS:
412
+ self._audit.append(
413
+ action="illegal_state_transition",
414
+ object_id=skill_id,
415
+ result="denied",
416
+ )
417
+ raise IllegalStateTransitionError(f"Unknown transition: {from_status} -> {to_status}")
418
+
419
+ if to_status == "enabled":
420
+ if sandbox_result != "pass":
421
+ raise SandboxRequiredError("Sandbox pass required for enabled state")
422
+ if not policy_approval:
423
+ raise PolicyDeniedError("Policy approval required for enabled state")
424
+
425
+ record.metadata.status = SkillStatus(to_status)
426
+ record.updated_at = datetime.now(UTC)
427
+
428
+ audit_ref = self._audit.append(
429
+ action="transition_skill_state",
430
+ object_id=skill_id,
431
+ result="success",
432
+ )
433
+ record.audit_refs.append(audit_ref)
434
+ self._save()
435
+
436
+ return StateTransitionResponse(
437
+ context=request.context,
438
+ skill_id=skill_id,
439
+ from_status=from_status,
440
+ to_status=to_status,
441
+ audit_ref=audit_ref,
442
+ )
443
+
444
+ def get_skill(self, skill_id: str) -> SkillRecord | None:
445
+ """Get skill metadata from Registry truth source.
446
+
447
+ Supports lookup by skill_id (e.g., "S09") or name (e.g., "resilience-degradation").
448
+ """
449
+ # Try by skill_id first
450
+ record = self._skills.get(skill_id)
451
+ if record is not None:
452
+ return record
453
+
454
+ # Try by name index
455
+ mapped_id = self._by_name.get(skill_id)
456
+ if mapped_id is not None:
457
+ return self._skills.get(mapped_id)
458
+
459
+ return None
460
+
461
+ def is_enabled(self, skill_id: str) -> bool:
462
+ """Check if skill version is routable by Execution Engine."""
463
+ record = self._skills.get(skill_id)
464
+ return record is not None and record.metadata.status == SkillStatus.ENABLED
465
+
466
+ def get_supply_chain_evidence(self, skill_id: str) -> dict | None:
467
+ """Get supply chain evidence for a skill.
468
+
469
+ Returns dict with: skill_id, evidence (set), missing, is_complete.
470
+ """
471
+ record = self._skills.get(skill_id)
472
+ if not record:
473
+ return None
474
+ missing = self._required_evidence - record.evidence
475
+ return {
476
+ "skill_id": skill_id,
477
+ "evidence": sorted(record.evidence),
478
+ "missing": sorted(missing),
479
+ "is_complete": len(missing) == 0,
480
+ }
481
+
482
+ def verify_evidence_integrity(self, skill_id: str) -> list[str]:
483
+ """Verify supply chain evidence integrity for a skill.
484
+
485
+ Returns list of issues found (empty = all valid).
486
+ """
487
+ record = self._skills.get(skill_id)
488
+ if not record:
489
+ return [f"Skill not found: {skill_id}"]
490
+
491
+ issues = []
492
+ missing = self._required_evidence - record.evidence
493
+ if missing:
494
+ issues.append(f"Missing evidence: {sorted(missing)}")
495
+
496
+ # Check security metadata fields
497
+ security = record.metadata.security
498
+ if not security.get("sbom_ref") and not security.get("sbom"):
499
+ if "SPDX SBOM" in record.evidence:
500
+ issues.append("SBOM evidence claimed but no sbom_ref/sbom field")
501
+ if not security.get("signature_ref") and not security.get("signature") and not security.get("digest"):
502
+ if "signature" in record.evidence:
503
+ issues.append("Signature evidence claimed but no signature field")
504
+
505
+ return issues
506
+
507
+
508
+ # Backward-compatible alias
509
+ SkillRegistry = Registry
@@ -0,0 +1,98 @@
1
+ """Registry models — Skill metadata and lifecycle state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "ProblemDetail",
7
+ "RegisterSkillRequest",
8
+ "RegisterSkillResponse",
9
+ "SkillMetadata",
10
+ "SkillStatus",
11
+ "StateTransitionRequest",
12
+ "StateTransitionResponse",
13
+ ]
14
+
15
+ from dataclasses import dataclass, field
16
+ from enum import StrEnum
17
+ from typing import Any
18
+
19
+
20
+ class SkillStatus(StrEnum):
21
+ """Skill lifecycle states (9-state model)."""
22
+
23
+ DRAFT = "draft"
24
+ IMPORTED = "imported"
25
+ TESTING = "testing"
26
+ ENABLED = "enabled"
27
+ DISABLED = "disabled"
28
+ DEPRECATED = "deprecated"
29
+ ARCHIVED = "archived"
30
+ REJECTED = "rejected"
31
+ QUARANTINED = "quarantined"
32
+
33
+
34
+ @dataclass
35
+ class SkillMetadata:
36
+ """Skill metadata stored in Registry."""
37
+
38
+ skill_id: str
39
+ name: str
40
+ version: str
41
+ status: SkillStatus = SkillStatus.DRAFT
42
+ description: str = ""
43
+ author: str = ""
44
+ created_at: str = ""
45
+ updated_at: str = ""
46
+ tags: list[str] = field(default_factory=list)
47
+ dependencies: list[str] = field(default_factory=list)
48
+ security: dict[str, Any] = field(default_factory=dict)
49
+ quality_score: float = 0.0
50
+
51
+
52
+ @dataclass
53
+ class ProblemDetail:
54
+ """RFC 7807 Problem Detail for error responses."""
55
+
56
+ type: str
57
+ title: str
58
+ status: int
59
+ detail: str = ""
60
+ instance: str = ""
61
+
62
+
63
+ @dataclass
64
+ class RegisterSkillRequest:
65
+ """Request to register a skill candidate."""
66
+
67
+ skill_metadata: SkillMetadata
68
+ context: dict[str, Any] = field(default_factory=dict)
69
+
70
+
71
+ @dataclass
72
+ class RegisterSkillResponse:
73
+ """Response from skill registration."""
74
+
75
+ context: dict[str, Any] = field(default_factory=dict)
76
+ skill_id: str = ""
77
+ status: str = "testing"
78
+ audit_ref: str = ""
79
+
80
+
81
+ @dataclass
82
+ class StateTransitionRequest:
83
+ """Request to transition skill state."""
84
+
85
+ from_status: SkillStatus
86
+ to_status: SkillStatus
87
+ context: dict[str, Any] = field(default_factory=dict)
88
+
89
+
90
+ @dataclass
91
+ class StateTransitionResponse:
92
+ """Response from state transition."""
93
+
94
+ context: dict[str, Any] = field(default_factory=dict)
95
+ skill_id: str = ""
96
+ from_status: str = ""
97
+ to_status: str = ""
98
+ audit_ref: str = ""