capacium-models 0.5.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.
@@ -0,0 +1,66 @@
1
+ """Shared domain models for the Capacium ecosystem.
2
+
3
+ Usage:
4
+ from capacium_models import Listing, TrustState, TrustMachine
5
+ """
6
+
7
+ from .models import (
8
+ MCPClient,
9
+ MCPMetadata,
10
+ TrustTransition,
11
+ Listing,
12
+ Publisher,
13
+ Category,
14
+ Subcategory,
15
+ TagCount,
16
+ Collection,
17
+ ClaimRequest,
18
+ VerificationRecord,
19
+ SearchQuery,
20
+ )
21
+ from .trust import TrustState, TrustMachine, TRUST_BADGES, get_trust_badge
22
+ from .keys import (
23
+ LICENSE_PUBLIC_KEYS,
24
+ UnknownLicenseKid,
25
+ get_license_public_key,
26
+ known_kids,
27
+ )
28
+ from .labels import (
29
+ RESOURCE_KIND_LABELS,
30
+ OPERATOR_TYPE_LABELS,
31
+ INSTALL_STATUS_LABELS,
32
+ get_kind_label,
33
+ get_operator_type_label,
34
+ get_install_status_label,
35
+ )
36
+ from .search import ExchangeSearch
37
+
38
+ __all__ = [
39
+ "MCPClient",
40
+ "MCPMetadata",
41
+ "TrustState",
42
+ "TrustTransition",
43
+ "TrustMachine",
44
+ "Listing",
45
+ "Publisher",
46
+ "Category",
47
+ "Subcategory",
48
+ "TagCount",
49
+ "Collection",
50
+ "ClaimRequest",
51
+ "VerificationRecord",
52
+ "SearchQuery",
53
+ "ExchangeSearch",
54
+ "TRUST_BADGES",
55
+ "get_trust_badge",
56
+ "RESOURCE_KIND_LABELS",
57
+ "OPERATOR_TYPE_LABELS",
58
+ "INSTALL_STATUS_LABELS",
59
+ "get_kind_label",
60
+ "get_operator_type_label",
61
+ "get_install_status_label",
62
+ "LICENSE_PUBLIC_KEYS",
63
+ "UnknownLicenseKid",
64
+ "get_license_public_key",
65
+ "known_kids",
66
+ ]
@@ -0,0 +1,123 @@
1
+ """License public-key registry for capacium-models consumers.
2
+
3
+ Consumers verify license tokens (issued by capacium-exchange `LicenseService`)
4
+ using Ed25519. The matching PRIVATE key lives ONLY in envctl on prod, under:
5
+
6
+ envctl get capacium-exchange CAPACIUM_LICENSE_SIGNING_KEY # 64-hex private
7
+ envctl get capacium-exchange CAPACIUM_LICENSE_SIGNING_KID # current kid
8
+
9
+ This module holds the corresponding PUBLIC keys, indexed by `kid` (key
10
+ identifier as embedded in license token payloads). Consumers use these to
11
+ verify signatures locally without round-tripping to the Exchange.
12
+
13
+ Key rotation lifecycle for a given kid:
14
+ - Active: issues new tokens; PRIVATE held in envctl
15
+ - Retired: no longer issues NEW tokens; OLD tokens still validate
16
+ (entry stays in this registry until last token expires)
17
+ - Removed: entry deleted from registry; OLD tokens fail validation
18
+
19
+ To add a new kid: append to LICENSE_PUBLIC_KEYS, bump the package version
20
+ (minor bump — additive); ship as a new capacium-models release. NEVER
21
+ remove an entry whose tokens may still be in circulation.
22
+
23
+ DECISION-001 reference: license-signing keypair is DISTINCT from
24
+ capability-signing keypair. kid namespace MUST start with `lic-`.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from typing import Dict, List
30
+
31
+
32
+ # ── License public-key registry ───────────────────────────────────────────
33
+ #
34
+ # Format: dict[kid -> 64-char lowercase hex] (32 bytes Ed25519 public key)
35
+ #
36
+ # Each entry MUST match the private key currently set as
37
+ # CAPACIUM_LICENSE_SIGNING_KEY in envctl on capacium-prod, under the
38
+ # matching kid CAPACIUM_LICENSE_SIGNING_KID.
39
+
40
+ LICENSE_PUBLIC_KEYS: Dict[str, str] = {
41
+ # ──────────────────────────────────────────────────────────────────────
42
+ # kid: lic-2026-q2-01
43
+ # Added: 2026-06-03 (initial v2 launch — corresponds to the FIRST
44
+ # Ed25519 keypair generated on maintainer workstation, private uploaded
45
+ # to envctl on capacium-prod)
46
+ # ──────────────────────────────────────────────────────────────────────
47
+ "lic-2026-q2-01": "8e5e33e1ab3676d239a1f89cded6afca9aca0e8ba24c3b4b50ad4b71ef84e978",
48
+ }
49
+
50
+
51
+ # ── Public API ────────────────────────────────────────────────────────────
52
+
53
+
54
+ class UnknownLicenseKid(KeyError):
55
+ """Raised when a license token's `kid` is not in the public-key registry.
56
+
57
+ Consumers MUST reject the token in this case (treat as invalid signature).
58
+ Receiving this exception is the signal to upgrade `capacium-models` to a
59
+ newer version that includes the missing kid.
60
+ """
61
+
62
+
63
+ def get_license_public_key(kid: str) -> str:
64
+ """Return the 64-char hex public key for a license kid.
65
+
66
+ Args:
67
+ kid: Key identifier as embedded in license token payload.
68
+
69
+ Returns:
70
+ 64-char lowercase hex string representing the Ed25519 public key.
71
+
72
+ Raises:
73
+ UnknownLicenseKid: when `kid` is not in `LICENSE_PUBLIC_KEYS`, OR
74
+ when its registered value is not a valid 64-char hex string
75
+ (e.g., still a placeholder, malformed registry entry).
76
+ Consumer code MUST reject the token in this case.
77
+ """
78
+ try:
79
+ pubkey = LICENSE_PUBLIC_KEYS[kid]
80
+ except KeyError as e:
81
+ raise UnknownLicenseKid(
82
+ f"kid={kid!r} not in LICENSE_PUBLIC_KEYS. "
83
+ f"Known kids: {known_kids()}. "
84
+ f"Upgrade capacium-models to support newer kids."
85
+ ) from e
86
+ _validate_hex(kid, pubkey)
87
+ return pubkey
88
+
89
+
90
+ def known_kids() -> List[str]:
91
+ """Return all registered kids, sorted newest-first (lexicographic reverse).
92
+
93
+ Naming convention `lic-YYYY-qN-NN` makes lex reverse equal to
94
+ chronological reverse.
95
+ """
96
+ return sorted(LICENSE_PUBLIC_KEYS.keys(), reverse=True)
97
+
98
+
99
+ # ── Internal helpers ──────────────────────────────────────────────────────
100
+
101
+
102
+ def _validate_hex(kid: str, pubkey: str) -> None:
103
+ """Validate that `pubkey` is a 64-char lowercase hex string.
104
+
105
+ Raises UnknownLicenseKid (not ValueError) so consumers handling kid
106
+ lookup errors have a single exception type to catch.
107
+ """
108
+ if not isinstance(pubkey, str):
109
+ raise UnknownLicenseKid(
110
+ f"kid={kid!r} has non-string pubkey of type {type(pubkey).__name__}"
111
+ )
112
+ if len(pubkey) != 64:
113
+ raise UnknownLicenseKid(
114
+ f"kid={kid!r} pubkey length is {len(pubkey)}, expected 64 hex chars. "
115
+ f"Likely placeholder — registry entry must be filled in before release."
116
+ )
117
+ try:
118
+ int(pubkey, 16)
119
+ except ValueError as e:
120
+ raise UnknownLicenseKid(
121
+ f"kid={kid!r} pubkey is not valid hex: {e}. "
122
+ f"Registry entry may still contain a placeholder."
123
+ ) from e
@@ -0,0 +1,75 @@
1
+ """Canonical label registries for all Capacium surfaces.
2
+
3
+ These dicts serve as single sources of truth — all UIs, badges, APIs, and CLI
4
+ surfaces derive their display labels from these exactly maps.
5
+ """
6
+
7
+ RESOURCE_KIND_LABELS = {
8
+ "skill": "Skill",
9
+ "mcp-server": "MCP Server",
10
+ "bundle": "Bundle",
11
+ "tool": "Tool",
12
+ "prompt": "Prompt",
13
+ "template": "Template",
14
+ "workflow": "Workflow",
15
+ "connector-pack": "Connector Pack",
16
+ "operator": "Operator",
17
+ "checkpoint": "Checkpoint",
18
+ "policy": "Policy",
19
+ "resource": "Resource",
20
+ }
21
+
22
+ OPERATOR_TYPE_LABELS = {
23
+ "ai": "AI",
24
+ "human": "Human",
25
+ "hybrid": "Hybrid",
26
+ }
27
+
28
+ INSTALL_STATUS_LABELS = {
29
+ "not_installed": "Not Installed",
30
+ "installing": "Installing",
31
+ "installed": "Installed",
32
+ "failed": "Failed",
33
+ "update_available": "Update Available",
34
+ "removed": "Removed",
35
+ }
36
+
37
+ TRUST_BADGES = {
38
+ "discovered": "Discovered",
39
+ "pending_review": "Pending Review",
40
+ "verified": "Verified",
41
+ "signed": "Signed",
42
+ "deprecated": "Deprecated",
43
+ }
44
+
45
+
46
+ def get_kind_label(kind: str) -> str:
47
+ """Return the human-readable label for a capability kind.
48
+
49
+ Falls back to the raw string if the kind is unknown.
50
+ """
51
+ return RESOURCE_KIND_LABELS.get(kind, kind)
52
+
53
+
54
+ def get_operator_type_label(op_type: str) -> str:
55
+ """Return the human-readable label for an operator type.
56
+
57
+ Falls back to the raw string if the type is unknown.
58
+ """
59
+ return OPERATOR_TYPE_LABELS.get(op_type, op_type)
60
+
61
+
62
+ def get_install_status_label(status: str) -> str:
63
+ """Return the human-readable label for an install status.
64
+
65
+ Falls back to the raw string if the status is unknown.
66
+ """
67
+ return INSTALL_STATUS_LABELS.get(status, status)
68
+
69
+
70
+ def get_trust_badge(state: str) -> str:
71
+ """Return the human-readable trust badge label for a trust state.
72
+
73
+ Falls back to the raw string if the state is unknown.
74
+ """
75
+ return TRUST_BADGES.get(state, state)
@@ -0,0 +1,389 @@
1
+ """Exchange Core domain models.
2
+
3
+ Defines the central data structures for the Capacium Exchange:
4
+ Listing, MCPMetadata, Publisher, Category, Subcategory, TagCount,
5
+ Collection, ClaimRequest, VerificationRecord, TrustTransition, SearchQuery.
6
+ """
7
+
8
+ import json
9
+ import uuid
10
+ from dataclasses import dataclass, field, asdict
11
+ from datetime import datetime, timezone
12
+ from enum import Enum
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ class MCPClient(Enum):
17
+ CLAUDE_DESKTOP = "claude-desktop"
18
+ CLAUDE_CODE = "claude-code"
19
+ CURSOR = "cursor"
20
+ OPENCODE = "opencode"
21
+ CODEX = "codex"
22
+ CONTINUE = "continue"
23
+ WINDSURF = "windsurf"
24
+ GENERIC = "generic-mcp-client"
25
+
26
+
27
+ @dataclass
28
+ class MCPMetadata:
29
+ supported_clients: List[str] = field(default_factory=list)
30
+ transport: str = "stdio"
31
+ runtime: Optional[str] = None
32
+ hosted: bool = False
33
+ auth_model: Optional[str] = None
34
+ required_env_vars: List[str] = field(default_factory=list)
35
+ permission_scopes: List[str] = field(default_factory=list)
36
+ health_endpoint: Optional[str] = None
37
+ health_status: Optional[str] = None
38
+ last_health_check: Optional[str] = None
39
+ setup_command: Optional[str] = None
40
+ documentation_url: Optional[str] = None
41
+ command_examples: List[str] = field(default_factory=list)
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ return asdict(self)
45
+
46
+ @classmethod
47
+ def from_dict(cls, data: Dict[str, Any]) -> "MCPMetadata":
48
+ if data is None:
49
+ return cls()
50
+ known = {f.name for f in cls.__dataclass_fields__.values()}
51
+ filtered = {k: v for k, v in data.items() if k in known}
52
+ return cls(**filtered)
53
+
54
+ def to_json(self) -> str:
55
+ return json.dumps(self.to_dict())
56
+
57
+ @classmethod
58
+ def from_json(cls, text: str) -> "MCPMetadata":
59
+ if not text:
60
+ return cls()
61
+ return cls.from_dict(json.loads(text))
62
+
63
+
64
+ @dataclass
65
+ class TrustTransition:
66
+ from_state: str
67
+ to_state: str
68
+ reason: str = ""
69
+ triggered_by: str = "system"
70
+ timestamp: str = ""
71
+
72
+ def __post_init__(self):
73
+ if not self.timestamp:
74
+ self.timestamp = datetime.now(timezone.utc).isoformat()
75
+
76
+ def to_dict(self) -> Dict[str, Any]:
77
+ return asdict(self)
78
+
79
+ @classmethod
80
+ def from_dict(cls, data: Dict[str, Any]) -> "TrustTransition":
81
+ return cls(**{k: v for k, v in data.items() if k in {
82
+ "from_state", "to_state", "reason", "triggered_by", "timestamp"
83
+ }})
84
+
85
+
86
+ @dataclass
87
+ class Listing:
88
+ id: str = ""
89
+ canonical_name: str = ""
90
+ package_type: str = "skill"
91
+
92
+ canonical_source_url: str = ""
93
+ source_urls: List[str] = field(default_factory=list)
94
+
95
+ short_description: str = ""
96
+ long_description: str = ""
97
+
98
+ trust_state: str = "discovered"
99
+ trust_history: List[Dict[str, Any]] = field(default_factory=list)
100
+
101
+ primary_category: str = "developer-tools"
102
+ subcategory: Optional[str] = None
103
+ tags: List[str] = field(default_factory=list)
104
+
105
+ publisher_id: Optional[str] = None
106
+
107
+ target_frameworks: List[str] = field(default_factory=list)
108
+
109
+ installability: str = "local-install"
110
+ install_reference: Optional[str] = None
111
+
112
+ license: Optional[str] = None
113
+
114
+ maturity: str = "experimental"
115
+
116
+ discovered_at: str = ""
117
+ indexed_at: Optional[str] = None
118
+ updated_at: str = ""
119
+
120
+ mcp_metadata: Optional[Dict[str, Any]] = None
121
+
122
+ installed_capability_id: Optional[int] = None
123
+
124
+ # -- GitHub Enrichment --
125
+ github_owner: Optional[str] = None
126
+ github_repo: Optional[str] = None
127
+ github_stars: int = 0
128
+ github_forks: int = 0
129
+ github_watchers: int = 0
130
+ github_license: Optional[str] = None
131
+ github_topics: List[str] = field(default_factory=list)
132
+ github_language: Optional[str] = None
133
+ github_description: Optional[str] = None
134
+ github_homepage: Optional[str] = None
135
+ github_default_branch: Optional[str] = None
136
+ github_created_at: Optional[str] = None
137
+ github_pushed_at: Optional[str] = None
138
+ github_updated_at: Optional[str] = None
139
+
140
+ # -- Document Model v2 --
141
+ source_repo: Optional[str] = None # e.g. "anthropics/skills"
142
+ source_path: str = "/" # e.g. "skills/ai-ml/3d-cv-labeling"
143
+ repo_type: str = "unknown" # single_skill | multi_skill | collection | mcp_server | unknown
144
+ skill_path: Optional[str] = None # path to SKILL.md within repo
145
+ skill_name: Optional[str] = None # parsed from SKILL.md frontmatter
146
+ skill_compatibility: Optional[Dict[str, Any]] = None # from SKILL.md
147
+ skill_metadata: Optional[Dict[str, Any]] = None # from SKILL.md
148
+
149
+ # -- Manifest --
150
+ manifest_raw: Optional[str] = None # raw capability.yaml content
151
+ manifest_version: Optional[str] = None
152
+
153
+ # -- Capability IR --
154
+ capability_ir: Optional[Dict[str, Any]] = None # framework-agnostic IR (JSONB)
155
+ adaptation_targets: List[str] = field(default_factory=list) # ["mcp-server", "a2a-agent"]
156
+
157
+ # -- Capacium Trust --
158
+ fingerprint: Optional[str] = None
159
+ signature_value: Optional[str] = None
160
+ signature_public_key: Optional[str] = None
161
+
162
+ def __post_init__(self):
163
+ if not self.id:
164
+ self.id = str(uuid.uuid4())
165
+ now = datetime.now(timezone.utc).isoformat()
166
+ if not self.discovered_at:
167
+ self.discovered_at = now
168
+ if not self.updated_at:
169
+ self.updated_at = now
170
+
171
+ def to_dict(self) -> Dict[str, Any]:
172
+ return asdict(self)
173
+
174
+ @classmethod
175
+ def from_dict(cls, data: Dict[str, Any]) -> "Listing":
176
+ known = {f.name for f in cls.__dataclass_fields__.values()}
177
+ filtered = {k: v for k, v in data.items() if k in known}
178
+ for list_field in ("source_urls", "tags", "target_frameworks", "trust_history", "github_topics", "adaptation_targets"):
179
+ if list_field in filtered and isinstance(filtered[list_field], str):
180
+ try:
181
+ filtered[list_field] = json.loads(filtered[list_field])
182
+ except (json.JSONDecodeError, TypeError):
183
+ filtered[list_field] = []
184
+ for dict_field in ("mcp_metadata", "skill_compatibility", "skill_metadata", "capability_ir"):
185
+ if dict_field in filtered and isinstance(filtered[dict_field], str):
186
+ try:
187
+ filtered[dict_field] = json.loads(filtered[dict_field])
188
+ except (json.JSONDecodeError, TypeError):
189
+ filtered[dict_field] = None
190
+ return cls(**filtered)
191
+
192
+ def get_mcp_metadata_obj(self) -> Optional[MCPMetadata]:
193
+ if self.mcp_metadata:
194
+ return MCPMetadata.from_dict(self.mcp_metadata)
195
+ return None
196
+
197
+
198
+ @dataclass
199
+ class Publisher:
200
+ id: str = ""
201
+ display_name: str = ""
202
+ slug: str = ""
203
+ primary_proof_type: str = "account"
204
+ primary_proof_value: str = ""
205
+ verified: bool = False
206
+ verified_at: Optional[str] = None
207
+ verification_method: Optional[str] = None
208
+ contact_url: Optional[str] = None
209
+ created_at: str = ""
210
+ updated_at: str = ""
211
+
212
+ def __post_init__(self):
213
+ if not self.id:
214
+ self.id = str(uuid.uuid4())
215
+ now = datetime.now(timezone.utc).isoformat()
216
+ if not self.created_at:
217
+ self.created_at = now
218
+ if not self.updated_at:
219
+ self.updated_at = now
220
+
221
+ def to_dict(self) -> Dict[str, Any]:
222
+ return asdict(self)
223
+
224
+ @classmethod
225
+ def from_dict(cls, data: Dict[str, Any]) -> "Publisher":
226
+ known = {f.name for f in cls.__dataclass_fields__.values()}
227
+ return cls(**{k: v for k, v in data.items() if k in known})
228
+
229
+
230
+ @dataclass
231
+ class Category:
232
+ slug: str = ""
233
+ display_name: str = ""
234
+ description: str = ""
235
+ sort_order: int = 0
236
+
237
+ def to_dict(self) -> Dict[str, Any]:
238
+ return asdict(self)
239
+
240
+ @classmethod
241
+ def from_dict(cls, data: Dict[str, Any]) -> "Category":
242
+ known = {f.name for f in cls.__dataclass_fields__.values()}
243
+ return cls(**{k: v for k, v in data.items() if k in known})
244
+
245
+
246
+ @dataclass
247
+ class Subcategory:
248
+ slug: str = ""
249
+ category_slug: str = ""
250
+ display_name: str = ""
251
+ sort_order: int = 0
252
+
253
+ def to_dict(self) -> Dict[str, Any]:
254
+ return asdict(self)
255
+
256
+ @classmethod
257
+ def from_dict(cls, data: Dict[str, Any]) -> "Subcategory":
258
+ known = {f.name for f in cls.__dataclass_fields__.values()}
259
+ return cls(**{k: v for k, v in data.items() if k in known})
260
+
261
+
262
+ @dataclass
263
+ class TagCount:
264
+ tag: str = ""
265
+ count: int = 0
266
+
267
+ def to_dict(self) -> Dict[str, Any]:
268
+ return asdict(self)
269
+
270
+ @classmethod
271
+ def from_dict(cls, data: Dict[str, Any]) -> "TagCount":
272
+ return cls(tag=data.get("tag", ""), count=data.get("count", 0))
273
+
274
+
275
+ @dataclass
276
+ class Collection:
277
+ id: str = ""
278
+ slug: str = ""
279
+ display_name: str = ""
280
+ description: str = ""
281
+ listing_ids: List[str] = field(default_factory=list)
282
+ curated_by: Optional[str] = None
283
+ created_at: str = ""
284
+ updated_at: str = ""
285
+
286
+ def __post_init__(self):
287
+ if not self.id:
288
+ self.id = str(uuid.uuid4())
289
+ now = datetime.now(timezone.utc).isoformat()
290
+ if not self.created_at:
291
+ self.created_at = now
292
+ if not self.updated_at:
293
+ self.updated_at = now
294
+
295
+ def to_dict(self) -> Dict[str, Any]:
296
+ return asdict(self)
297
+
298
+ @classmethod
299
+ def from_dict(cls, data: Dict[str, Any]) -> "Collection":
300
+ known = {f.name for f in cls.__dataclass_fields__.values()}
301
+ filtered = {k: v for k, v in data.items() if k in known}
302
+ if "listing_ids" in filtered and isinstance(filtered["listing_ids"], str):
303
+ try:
304
+ filtered["listing_ids"] = json.loads(filtered["listing_ids"])
305
+ except (json.JSONDecodeError, TypeError):
306
+ filtered["listing_ids"] = []
307
+ return cls(**filtered)
308
+
309
+
310
+ @dataclass
311
+ class ClaimRequest:
312
+ id: str = ""
313
+ listing_id: str = ""
314
+ publisher_id: str = ""
315
+ proof_type: str = "repo-file"
316
+ proof_value: str = ""
317
+ status: str = "pending"
318
+ submitted_at: str = ""
319
+ reviewed_at: Optional[str] = None
320
+ reviewed_by: Optional[str] = None
321
+
322
+ def __post_init__(self):
323
+ if not self.id:
324
+ self.id = str(uuid.uuid4())
325
+ if not self.submitted_at:
326
+ self.submitted_at = datetime.now(timezone.utc).isoformat()
327
+
328
+ def to_dict(self) -> Dict[str, Any]:
329
+ return asdict(self)
330
+
331
+ @classmethod
332
+ def from_dict(cls, data: Dict[str, Any]) -> "ClaimRequest":
333
+ known = {f.name for f in cls.__dataclass_fields__.values()}
334
+ return cls(**{k: v for k, v in data.items() if k in known})
335
+
336
+
337
+ @dataclass
338
+ class VerificationRecord:
339
+ id: str = ""
340
+ listing_id: str = ""
341
+ method: str = "repo-token-file"
342
+ result: str = "pass"
343
+ details: str = ""
344
+ verified_at: str = ""
345
+ expires_at: Optional[str] = None
346
+ verifier: str = "system"
347
+
348
+ def __post_init__(self):
349
+ if not self.id:
350
+ self.id = str(uuid.uuid4())
351
+ if not self.verified_at:
352
+ self.verified_at = datetime.now(timezone.utc).isoformat()
353
+
354
+ def to_dict(self) -> Dict[str, Any]:
355
+ return asdict(self)
356
+
357
+ @classmethod
358
+ def from_dict(cls, data: Dict[str, Any]) -> "VerificationRecord":
359
+ known = {f.name for f in cls.__dataclass_fields__.values()}
360
+ return cls(**{k: v for k, v in data.items() if k in known})
361
+
362
+
363
+ @dataclass
364
+ class SearchQuery:
365
+ text: Optional[str] = None
366
+ package_type: Optional[str] = None
367
+ category: Optional[str] = None
368
+ subcategory: Optional[str] = None
369
+ tags: Optional[List[str]] = None
370
+ trust_state: Optional[str] = None
371
+ min_trust_state: Optional[str] = None
372
+ publisher_id: Optional[str] = None
373
+ installability: Optional[str] = None
374
+ license: Optional[str] = None
375
+ maturity: Optional[str] = None
376
+ mcp_client: Optional[str] = None
377
+ mcp_transport: Optional[str] = None
378
+ mcp_hosted: Optional[bool] = None
379
+ limit: int = 50
380
+ offset: int = 0
381
+ sort_by: str = "relevance"
382
+
383
+ def to_dict(self) -> Dict[str, Any]:
384
+ return {k: v for k, v in asdict(self).items() if v is not None}
385
+
386
+ @classmethod
387
+ def from_dict(cls, data: Dict[str, Any]) -> "SearchQuery":
388
+ known = {f.name for f in cls.__dataclass_fields__.values()}
389
+ return cls(**{k: v for k, v in data.items() if k in known})
@@ -0,0 +1,141 @@
1
+ """Faceted search engine for Exchange listings.
2
+
3
+ Builds dynamic SQL WHERE clauses from SearchQuery facets.
4
+ Supports text search, type/category/trust/tag/publisher/mcp-client filtering,
5
+ sorting, and pagination.
6
+ """
7
+
8
+ import sqlite3
9
+ from pathlib import Path
10
+ from typing import List, Optional, Tuple
11
+
12
+ from .models import Listing, SearchQuery
13
+ from .trust import TrustState
14
+
15
+
16
+ class ExchangeSearch:
17
+
18
+ def __init__(self, db_path: Optional[Path] = None):
19
+ if db_path is None:
20
+ db_path = Path.home() / ".capacium" / "registry.db"
21
+ self.db_path = db_path
22
+
23
+ def _connect(self) -> sqlite3.Connection:
24
+ conn = sqlite3.connect(str(self.db_path))
25
+ conn.row_factory = sqlite3.Row
26
+ return conn
27
+
28
+ def search(self, query: SearchQuery) -> Tuple[List[Listing], int]:
29
+ conditions = []
30
+ params = []
31
+
32
+ if query.text:
33
+ conditions.append(
34
+ "(canonical_name LIKE ? OR short_description LIKE ? OR long_description LIKE ?)"
35
+ )
36
+ like = f"%{query.text}%"
37
+ params.extend([like, like, like])
38
+
39
+ if query.package_type:
40
+ conditions.append("package_type = ?")
41
+ params.append(query.package_type)
42
+
43
+ if query.category:
44
+ conditions.append("primary_category = ?")
45
+ params.append(query.category)
46
+
47
+ if query.subcategory:
48
+ conditions.append("subcategory = ?")
49
+ params.append(query.subcategory)
50
+
51
+ if query.trust_state:
52
+ conditions.append("trust_state = ?")
53
+ params.append(query.trust_state)
54
+
55
+ if query.min_trust_state:
56
+ try:
57
+ from .trust import normalize_legacy_state, LEGACY_STATE_MAP
58
+ normalized = normalize_legacy_state(query.min_trust_state)
59
+ min_state = TrustState(normalized)
60
+ valid_states = [
61
+ s.value for s in TrustState.ordering()
62
+ if s >= min_state
63
+ ]
64
+ # Include legacy aliases that map to valid states
65
+ for legacy, modern in LEGACY_STATE_MAP.items():
66
+ if modern in valid_states and legacy not in valid_states:
67
+ valid_states.append(legacy)
68
+ if valid_states:
69
+ placeholders = ", ".join("?" for _ in valid_states)
70
+ conditions.append(f"trust_state IN ({placeholders})")
71
+ params.extend(valid_states)
72
+ except ValueError:
73
+ pass
74
+
75
+ if query.publisher_id:
76
+ conditions.append("publisher_id = ?")
77
+ params.append(query.publisher_id)
78
+
79
+ if query.installability:
80
+ conditions.append("installability = ?")
81
+ params.append(query.installability)
82
+
83
+ if query.license:
84
+ conditions.append("license = ?")
85
+ params.append(query.license)
86
+
87
+ if query.maturity:
88
+ conditions.append("maturity = ?")
89
+ params.append(query.maturity)
90
+
91
+ if query.tags:
92
+ for tag in query.tags:
93
+ conditions.append("tags LIKE ?")
94
+ params.append(f'%"{tag}"%')
95
+
96
+ if query.mcp_client:
97
+ conditions.append("mcp_metadata LIKE ?")
98
+ params.append(f'%"{query.mcp_client}"%')
99
+
100
+ if query.mcp_transport:
101
+ conditions.append("mcp_metadata LIKE ?")
102
+ params.append(f'%"transport": "{query.mcp_transport}"%')
103
+
104
+ if query.mcp_hosted is not None:
105
+ hosted_str = "true" if query.mcp_hosted else "false"
106
+ conditions.append("mcp_metadata LIKE ?")
107
+ params.append(f'%"hosted": {hosted_str}%')
108
+
109
+ where = ""
110
+ if conditions:
111
+ where = " WHERE " + " AND ".join(conditions)
112
+
113
+ conn = self._connect()
114
+ try:
115
+ count_row = conn.execute(
116
+ f"SELECT count(*) FROM listings{where}", params
117
+ ).fetchone()
118
+ total_count = count_row[0] if count_row else 0
119
+
120
+ sort_map = {
121
+ "name": "canonical_name ASC",
122
+ "trust": "trust_state ASC",
123
+ "updated": "updated_at DESC",
124
+ "relevance": "updated_at DESC",
125
+ }
126
+ order_by = sort_map.get(query.sort_by, "updated_at DESC")
127
+
128
+ full_query = f"SELECT * FROM listings{where} ORDER BY {order_by} LIMIT ? OFFSET ?"
129
+ params.extend([query.limit, query.offset])
130
+
131
+ rows = conn.execute(full_query, params).fetchall()
132
+ listings = [Listing.from_dict(dict(row)) for row in rows]
133
+
134
+ return listings, total_count
135
+ finally:
136
+ conn.close()
137
+
138
+ def search_simple(self, text: str, limit: int = 50) -> List[Listing]:
139
+ q = SearchQuery(text=text, limit=limit)
140
+ listings, _ = self.search(q)
141
+ return listings
@@ -0,0 +1,211 @@
1
+ """TrustState machine for Exchange listings.
2
+
3
+ Enforces valid state transitions, records history, and validates entry criteria.
4
+
5
+ v2.1: Unified 5-state model (discovered → pending_review → verified → signed → deprecated).
6
+ Legacy states (indexed, claimed, audited) are mapped via normalize_legacy_state().
7
+ """
8
+
9
+ from datetime import datetime, timezone
10
+ from enum import Enum
11
+ from typing import Dict, List
12
+
13
+ from .models import Listing, TrustTransition
14
+
15
+
16
+ class TrustState(str, Enum):
17
+ DISCOVERED = "discovered" # Found by crawler, minimal validation
18
+ PENDING_REVIEW = "pending_review" # Schema validated, composite score computed (formerly "audited")
19
+ VERIFIED = "verified" # Publisher claimed and ownership verified
20
+ SIGNED = "signed" # Cryptographic signature present and valid
21
+ DEPRECATED = "deprecated" # End-of-life, superseded, or abandoned
22
+
23
+ @classmethod
24
+ def ordering(cls) -> List["TrustState"]:
25
+ return [
26
+ cls.DISCOVERED,
27
+ cls.PENDING_REVIEW,
28
+ cls.VERIFIED,
29
+ cls.SIGNED,
30
+ cls.DEPRECATED,
31
+ ]
32
+
33
+ def __lt__(self, other):
34
+ if self.__class__ is other.__class__:
35
+ ordering = self.ordering()
36
+ return ordering.index(self) < ordering.index(other)
37
+ return NotImplemented
38
+
39
+ def __gt__(self, other):
40
+ if self.__class__ is other.__class__:
41
+ ordering = self.ordering()
42
+ return ordering.index(self) > ordering.index(other)
43
+ return NotImplemented
44
+
45
+ def __ge__(self, other):
46
+ if self.__class__ is other.__class__:
47
+ ordering = self.ordering()
48
+ return ordering.index(self) >= ordering.index(other)
49
+ return NotImplemented
50
+
51
+ def __le__(self, other):
52
+ if self.__class__ is other.__class__:
53
+ ordering = self.ordering()
54
+ return ordering.index(self) <= ordering.index(other)
55
+ return NotImplemented
56
+
57
+
58
+ # Legacy state mapping for backward compatibility
59
+ LEGACY_STATE_MAP: Dict[str, str] = {
60
+ "indexed": "pending_review",
61
+ "claimed": "verified",
62
+ "audited": "pending_review", # v1.0 → v2.1 migration
63
+ }
64
+
65
+
66
+ def normalize_legacy_state(state: str) -> str:
67
+ """Map old trust states to current model.
68
+
69
+ indexed/audited → pending_review, claimed → verified.
70
+ All other states pass through unchanged.
71
+ """
72
+ return LEGACY_STATE_MAP.get(state, state)
73
+
74
+
75
+ class TrustTransitionError(Exception):
76
+ pass
77
+
78
+
79
+ VALID_TRANSITIONS = {
80
+ TrustState.DISCOVERED: {TrustState.PENDING_REVIEW},
81
+ TrustState.PENDING_REVIEW: {TrustState.VERIFIED, TrustState.DISCOVERED},
82
+ TrustState.VERIFIED: {TrustState.SIGNED, TrustState.PENDING_REVIEW},
83
+ TrustState.SIGNED: {TrustState.VERIFIED, TrustState.DEPRECATED},
84
+ TrustState.DEPRECATED: {TrustState.PENDING_REVIEW},
85
+ }
86
+
87
+
88
+ class TrustMachine:
89
+
90
+ @staticmethod
91
+ def normalize_legacy_state(state: str) -> str:
92
+ """Map old trust states to new ones (convenience wrapper)."""
93
+ return normalize_legacy_state(state)
94
+
95
+ @staticmethod
96
+ def can_transition(from_state: TrustState, to_state: TrustState) -> bool:
97
+ allowed = VALID_TRANSITIONS.get(from_state, set())
98
+ return to_state in allowed
99
+
100
+ @staticmethod
101
+ def validate_entry_criteria(listing: Listing, target_state: TrustState) -> list:
102
+ errors = []
103
+
104
+ if target_state == TrustState.PENDING_REVIEW:
105
+ if not listing.canonical_name:
106
+ errors.append("Pending review state requires canonical_name")
107
+ if not listing.short_description:
108
+ errors.append("Pending review state requires short_description")
109
+ if not listing.canonical_source_url:
110
+ errors.append("Pending review state requires canonical_source_url")
111
+
112
+ elif target_state == TrustState.VERIFIED:
113
+ if not listing.publisher_id:
114
+ errors.append("Verified state requires publisher_id")
115
+
116
+ elif target_state == TrustState.SIGNED:
117
+ if not listing.fingerprint:
118
+ errors.append("Signed state requires fingerprint")
119
+ if not listing.signature_value:
120
+ errors.append("Signed state requires signature_value")
121
+
122
+ return errors
123
+
124
+ @classmethod
125
+ def transition(
126
+ cls,
127
+ listing: Listing,
128
+ to_state: TrustState,
129
+ reason: str = "",
130
+ triggered_by: str = "system",
131
+ ) -> Listing:
132
+ # Normalize legacy states before processing
133
+ current_state_str = normalize_legacy_state(listing.trust_state)
134
+ from_state = TrustState(current_state_str)
135
+
136
+ if from_state == to_state:
137
+ raise TrustTransitionError(
138
+ f"Listing is already in state '{to_state.value}'"
139
+ )
140
+
141
+ if not cls.can_transition(from_state, to_state):
142
+ raise TrustTransitionError(
143
+ f"Cannot transition from '{from_state.value}' to '{to_state.value}'. "
144
+ f"Valid targets: {[s.value for s in VALID_TRANSITIONS.get(from_state, set())]}"
145
+ )
146
+
147
+ criteria_errors = cls.validate_entry_criteria(listing, to_state)
148
+ if criteria_errors:
149
+ raise TrustTransitionError(
150
+ f"Entry criteria not met for '{to_state.value}': {'; '.join(criteria_errors)}"
151
+ )
152
+
153
+ transition = TrustTransition(
154
+ from_state=from_state.value,
155
+ to_state=to_state.value,
156
+ reason=reason,
157
+ triggered_by=triggered_by,
158
+ )
159
+
160
+ listing.trust_state = to_state.value
161
+ listing.trust_history.append(transition.to_dict())
162
+ listing.updated_at = datetime.now(timezone.utc).isoformat()
163
+
164
+ return listing
165
+
166
+
167
+ # ── V2 Trust Badge Dictionary (single source of truth for all surfaces) ──
168
+
169
+ TRUST_BADGES: Dict[str, Dict[str, str]] = {
170
+ "discovered": {
171
+ "label": "Discovered",
172
+ "symbol": "○",
173
+ "color": "grey",
174
+ "description": "Found by crawler, minimal validation",
175
+ "order": 0,
176
+ },
177
+ "pending_review": {
178
+ "label": "Pending Review",
179
+ "symbol": "◉",
180
+ "color": "yellow",
181
+ "description": "Schema validated, composite score computed",
182
+ "order": 1,
183
+ },
184
+ "verified": {
185
+ "label": "Verified",
186
+ "symbol": "✓",
187
+ "color": "green",
188
+ "description": "Publisher claimed and ownership verified",
189
+ "order": 2,
190
+ },
191
+ "signed": {
192
+ "label": "Signed",
193
+ "symbol": "✦",
194
+ "color": "blue",
195
+ "description": "Cryptographic signature present and valid",
196
+ "order": 3,
197
+ },
198
+ "deprecated": {
199
+ "label": "Deprecated",
200
+ "symbol": "✗",
201
+ "color": "red",
202
+ "description": "End-of-life, superseded, or abandoned",
203
+ "order": 4,
204
+ },
205
+ }
206
+
207
+
208
+ def get_trust_badge(trust_state: str) -> Dict[str, str]:
209
+ """Return the badge definition for a trust state. Falls back to 'discovered' for unknown states."""
210
+ normalized = normalize_legacy_state(trust_state)
211
+ return TRUST_BADGES.get(normalized, TRUST_BADGES["discovered"])
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: capacium-models
3
+ Version: 0.5.0
4
+ Summary: Shared domain models for the Capacium ecosystem — Listing, TrustState, TrustMachine, and more
5
+ Author: Capacium
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Capacium/capacium-models
8
+ Project-URL: Repository, https://github.com/Capacium/capacium-models
9
+ Project-URL: Issues, https://github.com/Capacium/capacium-models/issues
10
+ Keywords: capacium,models,listing,trust,ai-agents
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # capacium-models
28
+
29
+ > [!NOTE]
30
+ > **Public mirror.** The canonical repository is hosted on our self-hosted git.
31
+ > This GitHub copy is a read-only mirror kept in sync for visibility and
32
+ > installation. Bug reports are welcome via Issues; pull requests are applied
33
+ > upstream and synced back here.
34
+
35
+ Shared domain models for the Capacium ecosystem. Used by `capacium-exchange` and `capacium-crawler`.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install capacium-models
41
+ ```
42
+
43
+ ## Models
44
+
45
+ | Model | Description |
46
+ |-------|-------------|
47
+ | `TrustState` | Enum: DISCOVERED → PENDING_REVIEW → VERIFIED → SIGNED → DEPRECATED |
48
+ | `TrustMachine` | State machine with VALID_TRANSITIONS, can_transition(), transition() |
49
+ | `Listing` | Full listing model (24 fields, auto-UUID, auto-timestamps) |
50
+ | `Publisher` | Publisher profile with verification support |
51
+ | `MCPClient` | Enum of supported MCP clients |
52
+ | `MCPMetadata` | MCP-specific metadata (transport, runtime, auth_model, etc.) |
53
+ | `Category` / `Subcategory` / `TagCount` | Taxonomy models |
54
+ | `Collection` | Curated collection of listing IDs |
55
+ | `ClaimRequest` / `VerificationRecord` | Trust workflow models |
56
+ | `SearchQuery` | Faceted search query model (16 fields) |
57
+ | `ExchangeSearch` | Faceted search SQL engine |
58
+
59
+ The package-level `TRUST_BADGES` and `get_trust_badge()` exports retain the
60
+ rich badge metadata introduced in v0.4.0. UI-facing canonical text labels,
61
+ including the string-only trust labels, live in `capacium_models.labels`.
62
+
63
+ ## Usage
64
+
65
+ ```python
66
+ from capacium_models import Listing, TrustState, TrustMachine
67
+
68
+ # Create a discovered listing
69
+ listing = Listing(
70
+ canonical_name="org/my-mcp-server",
71
+ package_type="mcp-server",
72
+ canonical_source_url="https://github.com/org/my-mcp-server",
73
+ short_description="A cool MCP server",
74
+ trust_state=TrustState.DISCOVERED,
75
+ )
76
+
77
+ # Transition through trust states
78
+ machine = TrustMachine()
79
+ if machine.can_transition(listing.trust_state, TrustState.PENDING_REVIEW):
80
+ listing = machine.transition(
81
+ listing,
82
+ TrustState.PENDING_REVIEW,
83
+ "Validated metadata",
84
+ )
85
+ ```
86
+
87
+ ## Dependencies
88
+
89
+ Zero runtime dependencies — stdlib only (`dataclasses`, `enum`, `uuid`, `datetime`, `json`, `typing`).
90
+
91
+ ## License
92
+
93
+ MIT
94
+
95
+ ## Development
96
+
97
+ Canonical repository: **self-hosted Forgejo** — `git.langevc.com/capacium/capacium-models`
98
+ (`git clone git@git.langevc.com:capacium/capacium-models.git`). Develop against the Forgejo
99
+ clone and open pull requests there. The GitHub copy is a read-only mirror.
@@ -0,0 +1,11 @@
1
+ capacium_models/__init__.py,sha256=WPuprI0Bx8EcYYFMLgvRxgygdXX3JLfV1-7PZpjWjF0,1375
2
+ capacium_models/keys.py,sha256=u98gw9FFTSBBdv9JM0_o4xWUyFdUa8GuiZIDEbJs750,5299
3
+ capacium_models/labels.py,sha256=sopRzLjyB7PyWnRLKO8caduu_RwPH1masmCpyMIDaCg,1956
4
+ capacium_models/models.py,sha256=19Xtw7cAs4RIXLyhcq1BjKbqQwVdguLFxyZjtnmLbno,12350
5
+ capacium_models/search.py,sha256=OPNM7RlZ8sx8ipT_XE6C9VA-JwGNWKLJM3yvxEz0c7w,4863
6
+ capacium_models/trust.py,sha256=z8yom8kpcrV6KGRo6FWEai_56TibSOKEVtj3HIx4z7Q,7111
7
+ capacium_models-0.5.0.dist-info/licenses/LICENSE,sha256=XU1ruW3qAVD_Kyt2yvyVNadjak_NPX6YE8oRdnkxn7g,1065
8
+ capacium_models-0.5.0.dist-info/METADATA,sha256=Z7jDfVtXcMPxu1ZzrEkRjSPvJObc_aDe-J-p8bpfW6M,3608
9
+ capacium_models-0.5.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
10
+ capacium_models-0.5.0.dist-info/top_level.txt,sha256=HlNIpG2DeFPM7x6qXh8d_0s6Bvjh8cmKmZn7XT1tDg0,16
11
+ capacium_models-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Capacium
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ capacium_models