docs(registry): add MkDocs API documentation and usage examples for Package Registry Client #52

Merged
CoreRasurae merged 1 commits from feature/m1-registry-docs into master 2026-06-17 14:09:28 +00:00
9 changed files with 2046 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
# RegistryCache
Transparent client-side LRU cache with TTL expiration and SHA-1 tamper detection,
implementing [Package Registry Standard v1.0.0](../actor-registry-standard.md) §10.3.
```python
from cleveractors.registry import RegistryCache, CacheFactory, CacheStats
```
---
## Overview
`RegistryCache` wraps a `RegistryClient` and adds:
| Feature | Mechanism |
|---------|-----------|
| **LRU eviction** | Least-recently-used entry evicted when `max_size` exceeded |
| **TTL expiration** | Entries older than `ttl` seconds are re-fetched |
| **SHA-1 tamper detection** | Cached content hash is validated against stored PackageId |
| **Singleflight coalescing** | Concurrent cold-key requests share a single upstream fetch |
| **Thread safety** | All operations protected by `asyncio.Lock` |
---
## Constructor
```python
RegistryCache(
client: RegistryClient,
*,
max_size: int = 256,
ttl: float = 300.0,
validate_content: bool = True,
)
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `client` | `RegistryClient` | *required* | The upstream client to cache responses for |
| `max_size` | `int` | `256` | Maximum number of cached entries before LRU eviction |
| `ttl` | `float` | `300.0` | Time-to-live in seconds before re-fetch |
| `validate_content` | `bool` | `True` | Whether to validate cached content by recomputing SHA-1 |
---
## CacheStats
Observable cache statistics exposed via `RegistryCache.stats`:
```python
@dataclass
class CacheStats:
hits: int = 0
misses: int = 0
evictions: int = 0
def record_hit(self) -> None: ...
def record_miss(self) -> None: ...
def record_eviction(self) -> None: ...
def reset(self) -> None: ...
```
```python
cache = RegistryCache(client, max_size=512, ttl=600.0)
# ... perform operations ...
print(f"hits={cache.stats.hits} misses={cache.stats.misses} evictions={cache.stats.evictions}")
```
---
## Methods
### get_package
```python
async def get_package(self, package_id: str) -> dict[str, Any]
```
Retrieve package content, using cache when available and valid.
Cache-hit flow:
1. Entry found in cache and within TTL → validate SHA-1 (CPU-bound, outside lock).
2. Hash matches → return cached content.
3. Hash mismatch → evict (tampered), re-fetch from upstream.
Cache-miss flow:
1. Entry missing, expired, or tampered.
2. Concurrent callers for the same key share a single upstream fetch (singleflight).
3. Fetch from upstream, populate cache, return.
### resolve_package
```python
async def resolve_package(
self,
package_type: str,
namespace: str,
name: str,
version: str = "latest",
) -> dict[str, Any]
```
Mirrors `RegistryClient.resolve_package` with transparent caching.
Maintains a separate resolve-store keyed by `(type, namespace, name, version)`.
### put
```python
async def put(self, package_id: str, content: dict[str, Any]) -> None
```
Insert content directly into the cache without an upstream fetch.
Useful when content was obtained through another channel (e.g. `resolve_package`)
and you want to warm the cache for subsequent `get_package` hits.
### invalidate
```python
async def invalidate(self, package_id: str) -> bool
```
Remove a specific entry from the cache. Returns `True` if the entry was present.
```python
removed = await cache.invalidate("pkg_act_...")
print(f"Cache entry removed: {removed}")
```
### clear
```python
async def clear(self) -> None
```
Clear all entries from both stores (package content and resolution) and reset
statistics.
### close
```python
async def close(self) -> None
```
Clear caches, reset statistics, and close the underlying `RegistryClient`.
If you need post-mortem statistics, snapshot `cache.stats` before calling `close()`.
### `__contains__`
```python
def __contains__(self, package_id: str) -> bool
```
Raw membership test; returns `True` if the key is present (does **not** check TTL).
```python
if "pkg_act_..." in cache:
print("Entry exists in cache (may be expired)")
```
---
## Async Context Manager
```python
async with RegistryCache(client, max_size=512) as cache:
content = await cache.get_package("pkg_act_...")
print(f"Final stats: hits={cache.stats.hits}")
# cache.close() called automatically
```
---
## CacheFactory
Encapsulates cache configuration so consumers depend on the factory
abstraction rather than constructing `RegistryCache` directly.
```python
factory = CacheFactory(max_size=1000, ttl=900.0, validate_content=True)
cache = factory.create(client)
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_size` | `int` | `256` | Maximum cache entries |
| `ttl` | `float` | `300.0` | TTL in seconds |
| `validate_content` | `bool` | `True` | Enable SHA-1 validation |
---
## Real-Life Example: Email Categorization with Caching
A categorization system that resolves templates once, caches them, and
detects tampered cache entries:
```python
import asyncio
from cleveractors.registry import (
RegistryClient,
RegistryCache,
CacheStats,
PackageId,
)
async def categorize_emails() -> None:
client = RegistryClient(
base_url="https://registry.example.com",
api_key="categorizer-key",
)
cache = RegistryCache(client, max_size=256, ttl=600.0)
CATEGORY_TEMPLATES = [
"pkg_skl_aaa1111111111111111111111111111111111",
"pkg_skl_bbb2222222222222222222222222222222222",
"pkg_skl_ccc3333333333333333333333333333333333",
]
templates: dict[str, dict] = {}
for pkg_id in CATEGORY_TEMPLATES:
try:
content = await cache.get_package(pkg_id)
templates[pkg_id] = content
print(f"Loaded template: {content.get('name', pkg_id)}")
except Exception as exc:
print(f"Failed to load {pkg_id}: {exc}")
# Warm the cache explicitly via put (content from resolve)
resolved = await cache.resolve_package(
package_type="skill",
namespace="acme",
name="priority-classifier",
version="v2.0.0",
)
await cache.put(resolved["package_id"], {"name": "priority-classifier"})
# Check cache statistics
print(f"Cache stats: {cache.stats}")
print(f"Template count: {len(templates)}")
await cache.close()
if __name__ == "__main__":
asyncio.run(categorize_emails())
```
+207
View File
@@ -0,0 +1,207 @@
# Canonicalizer
Deterministic RFC-8785 canonical JSON for content-addressed SHA-1 hashing,
implementing [Package Registry Standard v1.0.0](../actor-registry-standard.md) §6.
```python
from cleveractors.registry import Canonicalizer
```
---
## Purpose
The Canonicalizer produces a **deterministic, reproducible** JSON representation
of a package document so that identical content always produces the same SHA-1
hash — and therefore the same Package ID. This enables content-addressed
storage, deduplication, and tamper detection.
!!! warning "SHA-1 is for content-addressing only"
SHA-1 is used **solely** for content-addressing per §6. It is **not**
suitable for cryptographic integrity verification. Use a modern hash
(SHA-256, SHA-3) when cryptographic guarantees are required.
---
## Constructor
```python
Canonicalizer(
*,
reference_resolver: Callable[[str], str] | None = None,
max_depth: int = 100,
)
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `reference_resolver` | `Callable \| None` | `None` | Optional callback to resolve `ID:pkg_...` and `local:...` references before canonicalization |
| `max_depth` | `int` | `100` | Maximum nesting depth; documents exceeding this raise `ValueError` |
When `reference_resolver` is configured, the canonicalizer walks the content
and replaces reference strings with their resolved concrete forms before
computing the canonical representation.
---
## Methods
### canonicalize
```python
def canonicalize(self, content: dict[str, Any]) -> str
```
Produce deterministic RFC-8785 canonical JSON from a package content dict.
The canonical form is produced by a 5-step pipeline (§6.2):
1. **Reference resolution** (when a resolver callback is configured): resolve all
`ID:pkg_...` and `local:...` references to concrete forms.
2. **NFC normalization**: every string value is normalised to Unicode NFC form.
3. **Dictionary key sorting**: keys are sorted lexicographically.
4. **Lifecycle field stripping**: `version` and `release_date` keys are removed.
5. **RFC-8785 serialization**: sorted keys, UTF-8, no insignificant whitespace,
no solidus escaping, `NaN`/`Infinity` rejected.
```python
canon = Canonicalizer()
json_str = canon.canonicalize({
"name": "my-actor",
"version": "v1.0.0", # stripped
"release_date": "2025-01-01", # stripped
"agents": {
"chat": {"type": "llm"}
},
})
print(json_str)
# {"agents":{"chat":{"type":"llm"}},"name":"my-actor"}
```
### compute_package_id
```python
def compute_package_id(
self, content: dict[str, Any], package_type: PackageType
) -> PackageId
```
Compute a content-addressed PackageId from content.
1. Canonicalize the content.
2. Compute SHA-1 of the canonical JSON.
3. Format as `pkg_<type>_<40-hex-sha1>`.
```python
from cleveractors.registry import Canonicalizer, PackageType, PackageId
canon = Canonicalizer()
pid = canon.compute_package_id(
content={"name": "hello-world", "agents": {"echo": {"type": "tool"}}},
package_type=PackageType.ACTOR,
)
print(pid.id_string)
# pkg_act_<40-hex-sha1>
```
### resolve_references
```python
def resolve_references(
self, content: dict[str, Any]
) -> dict[str, Any]
```
Walk a content dict and resolve all internal `ID:pkg_...` and `local:...`
references via the configured resolver callback. Returns a new dict with all
references replaced by their resolved values.
---
## Transformation Details
### Lifecycle Fields
The following keys are stripped during canonicalization because they record
*when* content was authored, not *what* the content is:
- `version` &mdash; the version string assigned at publish time
- `release_date` &mdash; the timestamp of publication
These fields are intentionally excluded so that publishing the same content
under a different version does not produce a different Package ID.
### NFC Normalization
All string values are normalized to Unicode Normalization Form C (NFC).
This ensures that visually identical strings with different Unicode
representations (e.g. composed vs. decomposed characters) produce the same
canonical form.
### Float Handling
IEEE 754 negative zero (`-0.0`) is normalized to `0.0` so that mathematically
equivalent values produce identical hashes. `NaN` and `Infinity` are rejected
by the RFC-8785 serializer (`allow_nan=False`).
### Max Depth Protection
Documents exceeding `max_depth` nesting raise `ValueError`. The default limit
of 100 protects against stack overflow on deeply nested or malicious documents.
---
## Real-Life Example: CI/CD Actor Verification
Verifying that an actor configuration has not drifted between CI/CD environments
by comparing content-addressed hashes:
```python
import hashlib
from cleveractors.registry import Canonicalizer, PackageType
def verify_actor_drift(
staging_config: dict,
production_config: dict,
) -> bool:
"""
Compare actor configs across environments using content-addressed hashes.
Returns True if identical, False if drift detected.
"""
canon = Canonicalizer()
staging_json = canon.canonicalize(staging_config)
prod_json = canon.canonicalize(production_config)
staging_hash = hashlib.sha1(
staging_json.encode("utf-8"), usedforsecurity=False
).hexdigest()
prod_hash = hashlib.sha1(
prod_json.encode("utf-8"), usedforsecurity=False
).hexdigest()
if staging_hash != prod_hash:
pid_staging = canon.compute_package_id(
staging_config, PackageType.ACTOR
)
pid_prod = canon.compute_package_id(
production_config, PackageType.ACTOR
)
print(
f"DRIFT DETECTED:\n"
f" staging: {pid_staging.id_string}\n"
f" production: {pid_prod.id_string}"
)
return False
print("No drift detected — configurations are identical.")
return True
```
This pattern is particularly useful in CI/CD pipelines where actor
configurations are deployed across multiple environments and must remain
consistent:
- Compute the canonical hash of the config committed to the repository.
- After deployment, fetch the live config from each environment.
- Recompute and compare hashes to detect unauthorized modifications or
configuration drift.
+263
View File
@@ -0,0 +1,263 @@
# RegistryClient
Async HTTP client implementing all 4 API endpoints of the
[Package Registry Standard v1.0.0](../actor-registry-standard.md) §8.
```python
from cleveractors.registry import RegistryClient
```
---
## Constructor
```python
RegistryClient(
base_url: str,
api_key: Optional[str] = None,
timeout: float = 30.0,
allow_insecure: bool = False,
)
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `base_url` | `str` | *required* | Registry server base URL (e.g. `"https://registry.example.com"`) |
| `api_key` | `str \| None` | `None` | Optional API key for authenticated operations (§9.2) |
| `timeout` | `float` | `30.0` | Request timeout in seconds |
| `allow_insecure` | `bool` | `False` | Suppress the HTTP-without-TLS security warning |
The constructor emits a warning when an API key is used over HTTP without
`allow_insecure=True`, because the standard requires HTTPS in production (§12.2).
---
## Endpoints
The client implements all 4 endpoints defined by the standard:
| Method | Endpoint | Standard Ref |
|--------|----------|-------------|
| `get_package(package_id)` | `GET /packages/{id}` | §8.2.1 |
| `resolve_package(type, ns, name, version)` | `GET /{type}/{ns}/{name}?version={v}` | §8.2.2 |
| `browse(type, namespace)` | `GET /browse` | §8.4.1 |
| `discover()` | `GET /.well-known/cleverthis-packages` | §8.4.2 |
All methods are `async` and return dictionaries. They map HTTP errors to typed
exceptions from the [exception hierarchy](exceptions.md).
---
### get_package
```python
async def get_package(self, package_id: str) -> dict[str, Any]
```
Retrieve raw package content by its globally unique Package ID.
```python
content = await client.get_package(
"pkg_act_a1b2c3d4e5f67890abcdef1234567890abcdef"
)
print(content["name"]) # e.g. "order-validator"
```
Raises: `PackageNotFoundError`, `InvalidPackageIdError`, `RegistryNetworkError`.
---
### resolve_package
```python
async def resolve_package(
self,
package_type: str,
namespace: str,
name: str,
version: Optional[str] = None,
) -> dict[str, Any]
```
Resolve a named package reference to its concrete Package ID. Performs version
alias resolution according to §4.2.
```python
# Resolve with a concrete version
result = await client.resolve_package(
package_type="actor",
namespace="acme",
name="order-validator",
version="v1.0.0",
)
print(result["package_id"]) # "pkg_act_..."
# Resolve with the `latest` alias (default when version is None)
latest = await client.resolve_package(
package_type="skill",
namespace="acme",
name="code-analysis",
)
print(latest["package_id"])
```
Returns a dict with keys `package_id` (str) and `type` (str).
Raises: `PackageNotFoundError`, `VersionNotFoundError`, `RegistryNetworkError`.
---
### browse
```python
async def browse(
self,
package_type: Optional[str] = None,
namespace: Optional[str] = None,
) -> dict[str, Any]
```
Browse published packages with optional type and namespace filters.
```python
# List all actor packages
results = await client.browse(package_type="actor")
for pkg in results["packages"]:
print(f"{pkg['name']} ({pkg['namespace']}) — {pkg['description']}")
# List all packages in a namespace
results = await client.browse(namespace="acme")
print(f"Found {results['count']} packages in acme namespace")
```
Returns a dict with keys `packages` (list of package summaries) and `count` (int).
Raises: `RegistryNetworkError`.
---
### discover
```python
async def discover(self) -> dict[str, Any]
```
Retrieve registry metadata via the well-known discovery endpoint (§8.4.2).
```python
meta = await client.discover()
print(meta["name"]) # "CleverThis Package Registry"
print(meta["version"]) # "1.0.0"
print(meta["supported_types"]) # ["actor", "graph", "stream", ...]
print(meta["authentication"]) # ["anonymous", "api_key"]
```
Raises: `RegistryNetworkError`.
---
## Authentication
The client supports two authentication modes defined in §9:
| Mode | How to use | When |
|------|-----------|------|
| **Anonymous** | Omit `api_key` | Reading public packages |
| **API Key** | Pass `api_key="..."` | Publishing or accessing private packages |
The API key is sent as a `Bearer` token in the `Authorization` header:
```
Authorization: Bearer <api_key>
```
---
## Error Mapping
HTTP errors from the server are automatically converted to typed Python
exceptions. See [exceptions.md](exceptions.md) for the complete mapping.
| HTTP Status | Exception |
|-------------|-----------|
| 400 | `InvalidPackageIdError` |
| 401 | `AuthenticationRequiredError` |
| 403 | `AccessDeniedError` |
| 404 | `PackageNotFoundError` |
| 409 | `ConflictError` |
| 5xx | `RegistryNetworkError` |
Server-side structured error responses (§13.1) are parsed and the `type`
field is matched against the error-type-to-exception map, allowing the
server to indicate specific error conditions (e.g. `VersionNotFound`)
even when the HTTP status code is less specific.
---
## Async Context Manager
`RegistryClient` supports async context management for automatic cleanup:
```python
async with RegistryClient(base_url="https://registry.example.com") as client:
content = await client.get_package("pkg_act_...")
# client.close() is called automatically on exit
```
Alternatively, call `close()` explicitly:
```python
client = RegistryClient(base_url="https://registry.example.com")
try:
content = await client.get_package("pkg_act_...")
finally:
await client.close()
```
---
## Real-Life Example: Supplier Agent Registration
Registering a supplier agent that validates part numbers in purchase orders
by pulling an order-validation template from the package registry:
```python
import asyncio
from typing import Any
from cleveractors.registry import RegistryClient, RegistryCache, CacheFactory
async def register_supplier_agent() -> dict[str, Any]:
client = RegistryClient(
base_url="https://registry.example.com",
api_key="supplier-api-key",
)
cache = RegistryCache(client, max_size=256, ttl=300.0)
# 1. Discover registry capabilities
meta = await client.discover()
if "actor" not in meta.get("supported_types", []):
raise RuntimeError("Registry does not support actor packages")
# 2. Resolve the order-validator template to its concrete ID
resolved = await cache.resolve_package(
package_type="actor",
namespace="acme",
name="order-validator",
version="v1.0.0",
)
package_id = resolved["package_id"]
# 3. Fetch the full template content
content = await cache.get_package(package_id)
print(f"Loaded template: {content.get('name', package_id)}")
print(f"Stats — hits: {cache.stats.hits}, misses: {cache.stats.misses}")
await cache.close()
return content
if __name__ == "__main__":
result = asyncio.run(register_supplier_agent())
print(f"Agent config: {list(result.keys())}")
```
+286
View File
@@ -0,0 +1,286 @@
# Exceptions
Typed exception hierarchy for the Package Registry Client, mapping server-side
error types from [Package Registry Standard v1.0.0](../actor-registry-standard.md) §13.2
to Python exception classes.
```python
from cleveractors.registry.exceptions import RegistryError
```
---
## Class Hierarchy
```
CleverAgentsException
└── RegistryError # Base — all registry errors
├── PackageNotFoundError # 404 — package does not exist
├── InvalidPackageIdError # 400 / local — bad Package ID format
├── InvalidPackageReferenceError # 400 / local — bad reference string
│ └── CircularLocalReferenceError # §11.3.2 — always fatal
├── VersionNotFoundError # 404 — version/alias cannot be resolved
├── ValidationError # 400 — package failed validation
├── AuthenticationRequiredError # 401 — auth needed
├── AccessDeniedError # 403 — insufficient permissions
├── ConflictError # 409 — duplicate or conflicting data
└── RegistryNetworkError # 5xx + transport — connection/timeout/5xx
```
---
## RegistryError (base)
```python
class RegistryError(CleverAgentsException):
message: str # Human-readable description
details: dict[str, Any] | None # Optional supplementary info from server
original_reference: str | None # The string that triggered the error
```
All subclasses inherit `message`, `details`, and `original_reference`.
---
## Exception Reference
### PackageNotFoundError
The requested package does not exist in the registry (HTTP 404, type `"PackageNotFound"`).
```python
from cleveractors.registry.exceptions import PackageNotFoundError
try:
content = await client.get_package("pkg_act_nonexistent")
except PackageNotFoundError as exc:
print(f"Package not found: {exc.message}")
```
### InvalidPackageIdError
The provided Package ID format is invalid (HTTP 400, type `"InvalidPackageId"`).
Raised locally by `PackageId.from_string()` and `PackageId.__post_init__()` when the
ID does not match the `pkg_<type>_<40-hex-sha1>` format:
```python
from cleveractors.registry.exceptions import InvalidPackageIdError
try:
pid = PackageId.from_string("bad-format")
except InvalidPackageIdError as exc:
print(f"Bad ID: {exc.message}")
```
### InvalidPackageReferenceError
The provided package reference string could not be parsed or resolved (HTTP 400,
type `"InvalidPackageReference"`).
Raised by `ReferenceResolver.parse()` and `ReferenceResolver.resolve()` when
the reference string is malformed, the referenced resource cannot be found, or
the required backend (client or local store) is not configured.
### CircularLocalReferenceError
A circular `local:` reference chain was detected (§11.3.2).
**Always** raised regardless of lenient-mode settings, because cycles represent a
correctness violation, not a graceful-degradation scenario. This is a subclass of
`InvalidPackageReferenceError` &mdash; catch the parent class to handle both cycles
and other reference failures uniformly.
### VersionNotFoundError
The requested version or version alias does not exist (HTTP 404, type `"VersionNotFound"`).
```python
from cleveractors.registry.exceptions import VersionNotFoundError
try:
result = await client.resolve_package(
package_type="actor",
namespace="acme",
name="my-agent",
version="v99.0.0", # does not exist
)
except VersionNotFoundError as exc:
print(f"Version not found: {exc.message}")
```
### ValidationError
The package failed server-side validation (HTTP 400, type `"ValidationError"`).
### AuthenticationRequiredError
Authentication is required for this operation (HTTP 401, type `"AuthenticationRequired"`).
### AccessDeniedError
Access to the requested resource is denied (HTTP 403, type `"AccessDenied"`).
Authenticated, but the credential lacks the required permission.
### ConflictError
The request conflicts with existing data (HTTP 409, type `"Conflict"`).
Typically raised when attempting to publish a duplicate Package ID.
### RegistryNetworkError
Connection errors, timeouts, DNS failures, or HTTP 5xx server errors.
```python
class RegistryNetworkError(RegistryError):
status_code: int | None # HTTP status for server errors
url: str | None # The URL being accessed
```
```python
from cleveractors.registry.exceptions import RegistryNetworkError
try:
content = await client.get_package("pkg_act_...")
except RegistryNetworkError as exc:
print(f"Network error: {exc.message}")
print(f"Status: {exc.status_code}, URL: {exc.url}")
```
---
## HTTP Status Mapping
The `exception_for_status` function maps HTTP status codes to exception classes:
```python
from cleveractors.registry.exceptions import exception_for_status
exc = exception_for_status(404, "Package not found")
print(type(exc).__name__) # "PackageNotFoundError"
```
| Status | Exception |
|--------|-----------|
| 400 | `InvalidPackageIdError` |
| 401 | `AuthenticationRequiredError` |
| 403 | `AccessDeniedError` |
| 404 | `PackageNotFoundError` |
| 409 | `ConflictError` |
| 500&ndash;599 | `RegistryNetworkError` |
| *other* | `RegistryError` |
---
## Server Error Type to Exception Map
The internal `_ERROR_TYPE_MAP` also maps server-side error type strings
(from structured error responses §13.1) to exception classes for automatic
conversion:
```python
_ERROR_TYPE_MAP = {
"PackageNotFound": PackageNotFoundError,
"InvalidPackageId": InvalidPackageIdError,
"InvalidPackageReference": InvalidPackageReferenceError,
"VersionNotFound": VersionNotFoundError,
"ValidationError": ValidationError,
"AuthenticationRequired": AuthenticationRequiredError,
"AccessDenied": AccessDeniedError,
"Conflict": ConflictError,
"InternalServerError": RegistryNetworkError,
}
```
---
## Real-Life Example: Email Categorization System
Catching and handling specific exception types in a system that resolves
categorization templates from a registry:
```python
import asyncio
from cleveractors.registry import RegistryClient
from cleveractors.registry.exceptions import (
AccessDeniedError,
AuthenticationRequiredError,
PackageNotFoundError,
RegistryError,
RegistryNetworkError,
VersionNotFoundError,
)
async def load_categorization_templates(
client: RegistryClient,
) -> list[dict]:
"""
Load email categorization templates from the registry.
Handles specific error types with appropriate recovery strategies.
"""
templates = []
template_refs = [
("skill", "acme", "email-categorizer", "v1.x"),
("skill", "acme", "spam-detector", "latest"),
("skill", "acme", "priority-classifier", "v2.0.0"),
]
for pkg_type, ns, name, version in template_refs:
try:
result = await client.resolve_package(
package_type=pkg_type,
namespace=ns,
name=name,
version=version,
)
content = await client.get_package(result["package_id"])
templates.append(content)
except PackageNotFoundError:
print(f"Template {ns}/{name} not registered — skipping")
continue
except VersionNotFoundError:
print(
f"Version {version} of {ns}/{name} not found — "
f"trying latest..."
)
try:
result = await client.resolve_package(
package_type=pkg_type, namespace=ns, name=name
)
content = await client.get_package(result["package_id"])
templates.append(content)
except RegistryError as fallback_exc:
print(f"Fallback also failed: {fallback_exc}")
except AuthenticationRequiredError:
print("API key expired or missing — cannot proceed")
break
except AccessDeniedError:
print(f"Access denied to {ns}/{name} — check permissions")
continue
except RegistryNetworkError as exc:
print(
f"Network failure (status={exc.status_code}) — "
f"will retry on next cycle"
)
continue
return templates
if __name__ == "__main__":
async def main() -> None:
async with RegistryClient(
base_url="https://registry.example.com",
api_key="service-api-key",
) as client:
templates = await load_categorization_templates(client)
print(f"Loaded {len(templates)} templates")
asyncio.run(main())
```
+107
View File
@@ -0,0 +1,107 @@
# Package Registry Client
**CleverActors Package Registry Subsystem &mdash; Client-Side Implementation of the Package Registry Standard v1.0.0**
---
## Overview
The Package Registry Client implements the [Package Registry Standard v1.0.0](../actor-registry-standard.md)
on the client side. It provides a complete, async-first library for discovering, resolving,
fetching, caching, and validating versioned packages of AI components &mdash; actors, graphs,
agents, templates, skills, MCP servers, and LSP services &mdash; from any compliant registry
server.
## Architecture
```
src/cleveractors/registry/
├── __init__.py Public re-exports
├── types.py Core data types (PackageType, PackageId, PackageReference, PackageContent)
├── exceptions.py Typed exception hierarchy (RegistryError + 9 subclasses)
├── client.py Async HTTP client (RegistryClient) — all 4 API endpoints
├── canonical.py Canonicalizer — RFC-8785 canonical JSON + SHA-1 hashing
├── resolver.py ReferenceResolver — parse and resolve all 3 reference schemes
├── reference_resolver.py PackageContentResolver — multi-server client pool with caching
├── cache.py RegistryCache — LRU + TTL + tamper detection with singleflight
└── local_store.py LocalPackageStore — resolve `local:<path>` references from disk
```
### Module Relationships
```
PackageReference ──► ReferenceResolver ──► PackageId
│ │
│ ▼
│ RegistryClient (HTTP)
│ │
│ ▼
└──────────► PackageContentResolver
┌──────────┼──────────┐
▼ ▼ ▼
RegistryCache Canonicalizer LocalPackageStore
```
## Module Map
| Module | Role |
|--------|------|
| [Types](types.md) | `PackageType`, `PackageId`, `PackageReference`, `PackageContent` — frozen dataclasses and enums |
| [Client](client.md) | `RegistryClient` — async HTTP client implementing all 4 endpoints (§8) |
| [Canonicalizer](canonical.md) | `Canonicalizer` — deterministic RFC-8785 JSON, NFC normalization, SHA-1 hashing |
| [Resolver](resolver.md) | `ReferenceResolver` — parse all 3 reference schemes, resolve version aliases |
| [Exceptions](exceptions.md) | `RegistryError` hierarchy — 9 typed exceptions with HTTP status mapping |
| [Cache](cache.md) | `RegistryCache` — LRU eviction, TTL expiration, SHA-1 tamper detection, singleflight |
| [Integration](integration.md) | End-to-end workflows combining multiple modules |
## Quickstart
```python
import asyncio
from cleveractors.registry import (
RegistryClient,
RegistryCache,
CacheFactory,
ReferenceResolver,
PackageReference,
)
async def main() -> None:
# 1. Create an authenticated client for your registry server
client = RegistryClient(
base_url="https://registry.example.com",
api_key="your-api-key",
)
# 2. Wrap it with transparent caching (optional but recommended)
cache = RegistryCache(client, max_size=512, ttl=600.0)
# 3. Fetch a package by its content-addressed ID
content = await cache.get_package(
"pkg_act_a1b2c3d4e5f67890abcdef1234567890abcdef"
)
print(f"Package name: {content['name']}")
# 4. Resolve a named package with version
resolved = await cache.resolve_package(
package_type="actor",
namespace="acme",
name="order-validator",
version="v1.0.0",
)
print(f"Resolved to: {resolved['package_id']}")
# 5. Parse and resolve references with the resolver
resolver = ReferenceResolver(client=client)
ref = resolver.parse("registry.example.com:acme/order-validator@latest")
package_id = await resolver.resolve(ref.original_reference)
print(f"Resolved ID: {package_id}")
await cache.close()
if __name__ == "__main__":
asyncio.run(main())
```
+466
View File
@@ -0,0 +1,466 @@
# Integration Examples
End-to-end workflows that combine multiple modules from the Package Registry
subsystem. Each example is a complete, copy-paste-runnable script (aside from
the registry server URL, which must be configured for your environment).
---
## 1. Electronic Component Ordering Pipeline
Registers a supplier agent that validates part numbers in purchase orders
by resolving an order-validator template from the package registry at startup.
This workflow demonstrates the full pipeline: **parse reference &rarr; resolve
(version alias) &rarr; fetch &rarr; cache &rarr; instantiate agent**.
```python
"""
Electronic component ordering pipeline.
Resolves an order-validator template from the registry, fetches its
full content, and produces a supplier agent configuration. Illustrates
the complete client → cache → resolver pipeline.
"""
import asyncio
from typing import Any
from cleveractors.registry import (
CacheFactory,
PackageContentResolver,
RegistryCache,
RegistryClient,
ReferenceResolver,
)
from cleveractors.registry.exceptions import (
InvalidPackageReferenceError,
PackageNotFoundError,
RegistryError,
RegistryNetworkError,
)
from cleveractors.registry.types import PackageReference, ReferenceType
async def order_pipeline() -> dict[str, Any]:
client = RegistryClient(
base_url="https://registry.example.com",
api_key="supplier-api-key",
)
# Transparent content-level cache per §10.3
cache_factory = CacheFactory(max_size=512, ttl=300.0)
cache = cache_factory.create(client)
# Resolver for parsing and resolving references
resolver = ReferenceResolver(client=client)
# 1. Parse the reference string
ref_str = "registry.example.com:acme/order-validator@v1.x"
ref: PackageReference = resolver.parse(ref_str)
if ref.reference_type != ReferenceType.REGISTRY:
raise RuntimeError(f"Expected registry reference, got {ref.reference_type}")
print(f"[1] Parsed reference: {ref.namespace}/{ref.name}@{ref.version}")
# 2. Resolve the version alias to a concrete Package ID
resolved = await cache.resolve_package(
package_type="actor",
namespace=ref.namespace,
name=ref.name,
version=ref.version,
)
package_id = resolved["package_id"]
print(f"[2] Resolved {ref.version}{package_id}")
# 3. Fetch full package content (cache hit if already fetched)
content = await cache.get_package(package_id)
print(f"[3] Fetched content: {content.get('name', package_id)}")
# 4. Build supplier agent config from the template
agent_config = {
"type": "supplier",
"template_package_id": package_id,
"validators": content.get("validators", []),
"cache_stats": {
"hits": cache.stats.hits,
"misses": cache.stats.misses,
"evictions": cache.stats.evictions,
},
}
print(f"[4] Agent configured with {len(agent_config['validators'])} validators")
print(f" Cache: hits={cache.stats.hits} misses={cache.stats.misses}")
await cache.close()
return agent_config
if __name__ == "__main__":
config = asyncio.run(order_pipeline())
print(f"\nDone. Supplier agent config: {list(config.keys())}")
```
---
## 2. Email Categorization System
Resolves categorization templates from a registry, fetches tool packages for
domain-specific agents, caches for repeated use, and gracefully handles
missing or inaccessible packages.
```python
"""
Email categorization system.
Resolves multiple skill packages from the registry, loads tool packages
for domain-specific agents, and builds a categorisation pipeline. Uses
the content resolver for multi-server client pooling and transparent
two-tier caching.
"""
import asyncio
from typing import Any
from cleveractors.registry import (
CacheFactory,
PackageContentResolver,
RegistryClient,
)
from cleveractors.registry.exceptions import (
AccessDeniedError,
AuthenticationRequiredError,
PackageNotFoundError,
RegistryNetworkError,
VersionNotFoundError,
)
from cleveractors.registry.types import PackageReference
CATEGORY_TEMPLATES = [
("skill", "acme", "email-categorizer", "v1.x"),
("skill", "acme", "spam-detector", "latest"),
("skill", "acme", "priority-classifier", "v2.0.0"),
("actor", "acme", "response-generator", "v1.0.0"),
]
TOOL_PACKAGES = [
"pkg_skl_aaa1111111111111111111111111111111111",
"pkg_skl_bbb2222222222222222222222222222222222",
]
async def build_categorization_system() -> dict[str, Any]:
client = RegistryClient(
base_url="https://registry.example.com",
api_key="categorizer-key",
)
cache_factory = CacheFactory(max_size=256, ttl=600.0)
resolver = PackageContentResolver(
api_key="categorizer-key",
cache_factory=cache_factory,
)
# Manually pre-warm the resolver's client pool
resolver.clients["https://registry.example.com"] = client
# Phase 1: Resolve template packages
template_contents: dict[str, dict] = {}
failed_templates: list[str] = []
for pkg_type, ns, name, version in CATEGORY_TEMPLATES:
ref = PackageReference.from_string(
f"registry.example.com:{ns}/{name}@{version}"
)
try:
content = await resolver.aresolve(ref, package_type=pkg_type)
if content is not None:
template_contents[f"{ns}/{name}"] = content
print(
f"{ns}/{name}@{version}"
f"{content.get('_package_id', 'unknown')}"
)
else:
failed_templates.append(f"{ns}/{name}")
except (PackageNotFoundError, VersionNotFoundError) as exc:
print(f"{ns}/{name}@{version}: {exc}")
failed_templates.append(f"{ns}/{name}")
except AccessDeniedError:
print(f"{ns}/{name}@{version}: access denied")
failed_templates.append(f"{ns}/{name}")
# Phase 2: Load tool packages from cache (second fetch hits cache)
print("\nLoading tool packages...")
tool_contents: dict[str, dict] = {}
for pkg_id in TOOL_PACKAGES:
try:
content = await resolver.aresolve(
PackageReference.from_string(f"ID:{pkg_id}"),
package_type="skill",
)
if content is not None:
tool_contents[pkg_id] = content
print(f"{pkg_id}")
except Exception as exc:
print(f"{pkg_id}: {exc}")
# Phase 3: Aggregate cache statistics
stats = resolver.total_stats
print(
f"\nCache summary: hits={stats.hits} misses={stats.misses}"
f" evictions={stats.evictions}"
)
await resolver.close_all()
return {
"templates_loaded": len(template_contents),
"tools_loaded": len(tool_contents),
"failed_templates": failed_templates,
"cache_stats": {
"hits": stats.hits,
"misses": stats.misses,
"evictions": stats.evictions,
},
}
if __name__ == "__main__":
result = asyncio.run(build_categorization_system())
print(f"\nDone: {result}")
```
---
## 3. CI/CD Pipeline Actor Verification
Canonicalizes an actor configuration, compares hashes across environments,
and detects configuration drift. Demonstrates the canonicalization pipeline
and content validation workflow.
```python
"""
CI/CD pipeline actor verification.
Compares actor configurations across staging and production environments
using content-addressed SHA-1 hashes. Detects drift, verifies integrity
of cached templates, and reports mismatches with full Package IDs.
"""
import asyncio
import hashlib
from typing import Any
from cleveractors.registry import (
Canonicalizer,
PackageType,
RegistryCache,
RegistryClient,
)
from cleveractors.registry.types import PackageId
def canonical_hash(content: dict[str, Any]) -> str:
"""Compute the canonical SHA-1 hash of a content dict."""
canon = Canonicalizer()
canonical = canon.canonicalize(content)
return hashlib.sha1(
canonical.encode("utf-8"), usedforsecurity=False
).hexdigest()
async def verify_deployment(
staging_url: str,
production_url: str,
package_id_str: str,
) -> dict[str, Any]:
"""
Verify that a deployed actor config is identical across environments.
Args:
staging_url: URL of the staging registry.
production_url: URL of the production registry.
package_id_str: The Package ID to verify.
"""
pid = PackageId.from_string(package_id_str)
result: dict[str, Any] = {"package_id": pid.id_string}
# Fetch from both environments
async with (
RegistryClient(base_url=staging_url) as staging_client,
RegistryClient(base_url=production_url) as prod_client,
):
staging_cache = RegistryCache(staging_client, max_size=1, ttl=60.0)
prod_cache = RegistryCache(prod_client, max_size=1, ttl=60.0)
staging_content = await staging_cache.get_package(package_id_str)
prod_content = await prod_cache.get_package(package_id_str)
result["staging_content_name"] = staging_content.get("name")
result["prod_content_name"] = prod_content.get("name")
# Compute canonical hashes
staging_hash = canonical_hash(staging_content)
prod_hash = canonical_hash(prod_content)
result["staging_hash"] = staging_hash
result["prod_hash"] = prod_hash
# Verify canonical hash matches the ID
canon = Canonicalizer()
computed_id = canon.compute_package_id(staging_content, pid.package_type)
result["hash_matches_id"] = computed_id.sha1_hex == pid.sha1_hex
# Compare environments
if staging_hash != prod_hash:
result["drift_detected"] = True
result["staging_canonical_id"] = computed_id.id_string
computed_prod = canon.compute_package_id(
prod_content, pid.package_type
)
result["prod_canonical_id"] = computed_prod.id_string
print(
f"DRIFT DETECTED for {package_id_str}\n"
f" Staging hash: {staging_hash}\n"
f" Production hash: {prod_hash}"
)
else:
result["drift_detected"] = False
print(f"No drift — {pid.id_string} matches across environments")
return result
async def main() -> None:
PKG_ID = "pkg_act_a1b2c3d4e5f67890abcdef1234567890abcdef"
result = await verify_deployment(
staging_url="https://staging-registry.example.com",
production_url="https://registry.example.com",
package_id_str=PKG_ID,
)
print(f"\nVerification result:")
for key, value in result.items():
print(f" {key}: {value}")
if __name__ == "__main__":
asyncio.run(main())
```
---
## 4. Multi-Tenant SaaS Provisioning
Resolves agent templates per tenant namespace, validates content integrity
via SHA-1, detects tampered cache entries, and provisions tenant-specific
agent configurations.
```python
"""
Multi-tenant SaaS provisioning.
Provisions agent configurations for multiple tenants by resolving
tenant-specific templates from the registry. Validates content integrity
and detects tampered or stale cache entries.
"""
import asyncio
from typing import Any
from cleveractors.registry import (
CacheFactory,
Canonicalizer,
PackageContentResolver,
RegistryClient,
PackageType,
)
from cleveractors.registry.exceptions import (
RegistryError,
RegistryNetworkError,
)
from cleveractors.registry.types import PackageReference
TENANTS = {
"tenant-alpha": "registry.example.com:alpha/workflow-agent@v2.x",
"tenant-beta": "registry.example.com:beta/workflow-agent@v1.x",
"tenant-gamma": "registry.example.com:gamma/workflow-agent@latest",
}
async def provision_tenants() -> dict[str, dict[str, Any]]:
client = RegistryClient(
base_url="https://registry.example.com",
api_key="provisioning-key",
)
cache_factory = CacheFactory(max_size=512, ttl=300.0)
resolver = PackageContentResolver(
api_key="provisioning-key",
cache_factory=cache_factory,
)
resolver.clients["https://registry.example.com"] = client
canon = Canonicalizer()
results: dict[str, dict[str, Any]] = {}
for tenant_id, ref_str in TENANTS.items():
print(f"\nProvisioning {tenant_id}...")
ref = PackageReference.from_string(ref_str)
try:
# Resolve via the two-tier cache
content = await resolver.aresolve(ref, package_type="actor")
if content is None:
print(f" ✗ Failed to resolve {ref_str}")
results[tenant_id] = {"status": "failed", "reason": "not_found"}
continue
package_id_str = content.get("_package_id", "unknown")
# Validate content integrity
canon.compute_package_id(
content, package_type=PackageType.ACTOR
)
results[tenant_id] = {
"status": "provisioned",
"package_id": package_id_str,
"namespace": ref.namespace,
"template_name": content.get("name", "unknown"),
}
print(f" ✓ Resolved to {package_id_str}")
except RegistryNetworkError as exc:
print(f" ✗ Network error: {exc}")
results[tenant_id] = {
"status": "failed",
"reason": f"network: {exc.status_code}",
}
except RegistryError as exc:
print(f" ✗ Registry error: {exc}")
results[tenant_id] = {
"status": "failed",
"reason": type(exc).__name__,
}
# Report aggregated cache statistics
stats = resolver.total_stats
print(
f"\nAggregated cache: hits={stats.hits} "
f"misses={stats.misses} evictions={stats.evictions}"
)
await resolver.close_all()
return results
if __name__ == "__main__":
results = asyncio.run(provision_tenants())
provisioned = sum(
1 for r in results.values() if r["status"] == "provisioned"
)
print(f"\nProvisioned {provisioned}/{len(results)} tenants")
```
+238
View File
@@ -0,0 +1,238 @@
# ReferenceResolver
Parses and resolves package references in all 3 supported schemes, implementing
[Package Registry Standard v1.0.0](../actor-registry-standard.md) §5.3 and §4.2.
```python
from cleveractors.registry import ReferenceResolver
```
---
## Constructor
```python
ReferenceResolver(
client: RegistryClient | None = None,
local_store: LocalPackageStore | None = None,
)
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `client` | `RegistryClient \| None` | `None` | Client for resolving `registry:` references |
| `local_store` | `LocalPackageStore \| None` | `None` | Store for resolving `local:` references |
Both are optional. Resolution fails with `InvalidPackageReferenceError` if you
attempt to resolve a reference type without the corresponding backend configured.
---
## Reference Schemes
Three reference schemes are supported per §5.3:
| Scheme | Format | Backend Required |
|--------|--------|-----------------|
| **Registry** | `server:namespace/name@version` | `RegistryClient` |
| **ID** | `ID:pkg_<type>_<40-hex-sha1>` | None (inline parsing) |
| **Local** | `local:<path>` | `LocalPackageStore` |
---
## Static Methods
### parse
```python
@staticmethod
def parse(ref_str: str) -> PackageReference
```
Parse a reference string into a `PackageReference`. Delegates to
`PackageReference.from_string` and wraps `ValueError` into
`InvalidPackageReferenceError`.
```python
ref = ReferenceResolver.parse("registry.example.com:acme/agent@v1.0.0")
print(ref.reference_type) # ReferenceType.REGISTRY
print(ref.namespace) # "acme"
```
### resolve_version
```python
def resolve_version(
version: str,
available_versions: list[str],
) -> str
```
Resolve a version string (concrete or alias) against available versions per §4.2.
Static helper; does not require a `ReferenceResolver` instance.
| Version Format | Behavior |
|----------------|----------|
| `vX.Y.Z` | Concrete: matched directly |
| `latest`, `vx`, `x` | Global alias: resolves to newest concrete version |
| `vX.x` | Major alias: resolves to newest with major X |
| `vX.Y.x` | Minor alias: resolves to newest Z in X.Y series |
```python
from cleveractors.registry import resolve_version
available = ["v1.0.0", "v1.0.1", "v1.1.0", "v2.0.0"]
print(resolve_version("v1.0.0", available)) # "v1.0.0"
print(resolve_version("latest", available)) # "v2.0.0"
print(resolve_version("v1.x", available)) # "v1.1.0"
print(resolve_version("v1.0.x", available)) # "v1.0.1"
```
### is_concrete_version / is_version_alias
```python
def is_concrete_version(version: str) -> bool
def is_version_alias(version: str) -> bool
```
```python
print(is_concrete_version("v1.2.3")) # True
print(is_concrete_version("latest")) # False
print(is_version_alias("latest")) # True
print(is_version_alias("v3.x")) # True
print(is_version_alias("v1.2.3")) # False
print(is_version_alias("unknown")) # False
```
---
## Async Methods
### resolve
```python
async def resolve(
self,
ref_str: str,
package_type: str = "actor",
) -> PackageId
```
Parse and resolve a reference string to a concrete `PackageId`.
Resolution strategy by reference type:
| Type | Strategy |
|------|----------|
| **ID** | Parse the Package ID string directly — no network call |
| **Local** | Resolve via `LocalPackageStore` from the filesystem |
| **Registry** | Query the configured `RegistryClient` to resolve by name/namespace/version |
```python
from cleveractors.registry import RegistryClient, ReferenceResolver
client = RegistryClient(base_url="https://registry.example.com")
resolver = ReferenceResolver(client=client)
# ID reference — resolved inline, no network call
pid = await resolver.resolve(
"ID:pkg_act_a1b2c3d4e5f67890abcdef1234567890abcdef"
)
print(pid.sha1_hex)
# Registry reference — queries the server
pid = await resolver.resolve(
"registry.example.com:acme/web-search@v1.0.0",
package_type="skill",
)
print(pid.id_string)
await resolver.close()
```
---
## Async Context Manager
```python
async with ReferenceResolver(client=client) as resolver:
pid = await resolver.resolve("ID:pkg_act_...")
# resolver.close() called automatically
```
---
## Real-Life Example: Multi-Tenant SaaS Provisioning
Resolving agent templates per tenant namespace, detecting and rejecting
malformed references:
```python
import asyncio
from cleveractors.registry import (
RegistryClient,
ReferenceResolver,
)
from cleveractors.registry.exceptions import (
InvalidPackageReferenceError,
PackageNotFoundError,
)
from cleveractors.registry.types import PackageReference, ReferenceType
TENANT_AGENTS = {
"tenant-alpha": "registry.example.com:alpha/categorization-agent@v2.x",
"tenant-beta": "registry.example.com:beta/categorization-agent@v1.x",
}
async def provision_tenant(tenant_id: str) -> None:
ref_str = TENANT_AGENTS.get(tenant_id)
if ref_str is None:
raise ValueError(f"Unknown tenant: {tenant_id}")
async with RegistryClient(
base_url="https://registry.example.com"
) as client:
resolver = ReferenceResolver(client=client)
# Step 1: Parse — validates reference format
try:
ref = resolver.parse(ref_str)
except InvalidPackageReferenceError as exc:
print(f"Invalid reference for {tenant_id}: {exc}")
return
# Step 2: Verify it is a registry reference (not ID, not local)
if ref.reference_type != ReferenceType.REGISTRY:
print(
f"Expected registry reference for {tenant_id}, "
f"got {ref.reference_type.value}"
)
return
# Step 3: Resolve — queries registry and handles version alias
try:
package_id = await resolver.resolve(
ref_str, package_type="actor"
)
except PackageNotFoundError as exc:
print(f"Package not found for {tenant_id}: {exc}")
return
except InvalidPackageReferenceError as exc:
print(
f"Resolution failed for {tenant_id}: {exc}"
)
return
print(
f"[{tenant_id}] Resolved {ref.namespace}/{ref.name}"
f"@{ref.version}{package_id.id_string}"
)
if __name__ == "__main__":
asyncio.run(provision_tenant("tenant-alpha"))
```
+222
View File
@@ -0,0 +1,222 @@
# Types
Core immutable data types for the Package Registry Client. All types derive
from the [Package Registry Standard v1.0.0](../actor-registry-standard.md)
and are implemented as frozen dataclasses and enums with strict validation
at construction time.
---
## PackageType
```python
from cleveractors.registry import PackageType
```
An enum of all 8 package types defined in the standard §3.2. Each member
carries a string prefix used in Package ID construction.
| Member | Prefix | Description |
|--------|--------|-------------|
| `PackageType.ACTOR` | `"act"` | Actor configuration documents |
| `PackageType.GRAPH` | `"grh"` | Graph route definitions |
| `PackageType.STREAM` | `"str"` | Stream route definitions |
| `PackageType.AGENT` | `"agt"` | Agent definitions |
| `PackageType.TEMPLATE` | `"tpl"` | Reusable component templates |
| `PackageType.SKILL` | `"skl"` | Skill packages |
| `PackageType.MCP` | `"mcp"` | MCP server definitions |
| `PackageType.LSP` | `"lsp"` | LSP service definitions |
Usage:
```python
pkg_type = PackageType.ACTOR
print(pkg_type.value) # "act"
```
---
## ReferenceType
```python
from cleveractors.registry import ReferenceType
```
An enum of the three reference schemes defined in the standard §5.3.
| Member | Value | Reference Format |
|--------|-------|-----------------|
| `ReferenceType.REGISTRY` | `"registry"` | `server:namespace/name@version` |
| `ReferenceType.ID` | `"id"` | `ID:pkg_<type>_<40-hex-sha1>` |
| `ReferenceType.LOCAL` | `"local"` | `local:<path>` |
---
## PackageId
```python
from cleveractors.registry import PackageId
```
A frozen, content-addressed package identifier (§5.1). Formatted as:
```
pkg_<type>_<40-hex-sha1>
```
The SHA-1 digest is computed from the canonical form of the package content (§6).
| Attribute | Type | Description |
|-----------|------|-------------|
| `package_type` | `PackageType` | The package's type |
| `sha1_hex` | `str` | The 40-character lowercase hex SHA-1 digest |
| Method / Property | Returns | Description |
|-------------------|---------|-------------|
| `id_string` | `str` | The full `pkg_<type>_<sha1>` string |
| `from_string(raw_id)` | `PackageId` | Parse and validate a Package ID string |
| `__str__()` | `str` | Same as `id_string` |
`PackageId` is immutable &mdash; once constructed, neither `package_type` nor
`sha1_hex` can change. Validation runs at construction time:
- SHA-1 must be exactly 40 lowercase hex characters.
- The type prefix must match a known `PackageType`.
- Empty strings are rejected.
### Example
```python
from cleveractors.registry import PackageId, PackageType
from cleveractors.registry.exceptions import InvalidPackageIdError
# Construct directly
pid = PackageId(
package_type=PackageType.ACTOR,
sha1_hex="a1b2c3d4e5f67890abcdef1234567890abcdef",
)
print(pid.id_string)
# pkg_act_a1b2c3d4e5f67890abcdef1234567890abcdef
# Parse from a string
pid2 = PackageId.from_string(
"pkg_tpl_b1a2c3d4e5f67890abcdef1234567890fedcba"
)
print(pid2.package_type) # PackageType.TEMPLATE
print(pid2.sha1_hex) # "b1a2c3d4e5f67890abcdef1234567890fedcba"
# Validation rejects malformed IDs
try:
PackageId.from_string("pkg_act_bad")
except InvalidPackageIdError as exc:
print(f"Rejected: {exc}")
```
---
## PackageReference
```python
from cleveractors.registry import PackageReference
```
A frozen, parsed representation of a reference string (§5.3). Holds the
original string for debugging alongside typed, parsed fields.
| Attribute | Type | Description |
|-----------|------|-------------|
| `original_reference` | `str` | The verbatim reference string |
| `reference_type` | `ReferenceType` | Which scheme this reference uses |
| `server` | `str \| None` | Registry server hostname (REGISTRY only) |
| `namespace` | `str \| None` | Package namespace (REGISTRY only) |
| `name` | `str \| None` | Package name (REGISTRY) or local path (LOCAL) |
| `version` | `str \| None` | Version string or alias; defaults to `"latest"` |
| `id_string` | `str \| None` | Raw Package ID (ID references only) |
| `package_type` | `str \| None` | Inferred package type prefix |
| Method | Returns | Description |
|--------|---------|-------------|
| `from_string(raw_ref)` | `PackageReference` | Parse and validate a reference string |
Three reference formats are supported:
| Format | Example |
|--------|---------|
| Registry | `"registry.cleverthis.com:acme/web_search@latest"` |
| ID | `"ID:pkg_act_a1b2c3d4e5f67890abcdef1234567890abcdef"` |
| Local | `"local:path/to/package.yaml"` |
Version defaults to `"latest"` when omitted from registry references.
### Example
```python
from cleveractors.registry import PackageReference, ReferenceType
# Parse a registry reference
ref = PackageReference.from_string(
"registry.cleverthis.com:acme/order-validator@v1.0.0"
)
print(ref.server) # "registry.cleverthis.com"
print(ref.namespace) # "acme"
print(ref.name) # "order-validator"
print(ref.version) # "v1.0.0"
# Parse an ID reference
id_ref = PackageReference.from_string(
"ID:pkg_act_a1b2c3d4e5f67890abcdef1234567890abcdef"
)
print(id_ref.reference_type) # ReferenceType.ID
print(id_ref.id_string) # "pkg_act_..."
# Parse a local reference
local_ref = PackageReference.from_string("local:agents/validator.yaml")
print(local_ref.reference_type) # ReferenceType.LOCAL
print(local_ref.name) # "agents/validator.yaml"
# Version defaults to "latest"
bare_ref = PackageReference.from_string(
"registry.example.com:acme/my-agent"
)
print(bare_ref.version) # "latest"
```
---
## PackageContent
```python
from cleveractors.registry import PackageContent
```
A frozen wrapper for fetched package content alongside its resolved identity.
Returned by `RegistryCache` and `PackageContentResolver`.
| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | `PackageId` | The content-addressed identity |
| `content` | `dict[str, Any]` | The parsed package content (not used for hashing/equality) |
| `original_reference` | `str \| None` | The reference string supplied by the caller |
| `fetched_at` | `datetime` | UTC timestamp of when the content was retrieved (not used for hashing/equality) |
`PackageContent` is frozen. The `content` and `fetched_at` fields are excluded
from hash and equality comparisons so that two `PackageContent` objects with
the same `id` and `original_reference` compare as equal regardless of when
they were fetched.
### Example
```python
from datetime import datetime, timezone
from cleveractors.registry import PackageContent, PackageId, PackageType
pid = PackageId(package_type=PackageType.ACTOR, sha1_hex="a" * 40)
pkg = PackageContent(
id=pid,
content={"name": "my-actor", "agents": {}},
original_reference="registry.example.com:acme/my-actor@v1.0.0",
)
print(pkg.id) # PackageId('pkg_act_aaa...')
print(pkg.content["name"]) # "my-actor"
print(pkg.original_reference) # "registry.example.com:acme/my-actor@v1.0.0"
```
+9
View File
@@ -7,6 +7,15 @@ edit_uri: src/branch/master/docs
nav:
- Home: index.md
- Package Registry:
- Overview: registry/index.md
- Types: registry/types.md
- RegistryClient: registry/client.md
- Canonicalizer: registry/canonical.md
- ReferenceResolver: registry/resolver.md
- Exceptions: registry/exceptions.md
- RegistryCache: registry/cache.md
- Integration Examples: registry/integration.md
- Development:
- Quality Automation: development/quality-automation.md