feat: add hub runtime and extension contract
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 0s

This commit is contained in:
tegwick 2026-08-21 10:58:03 +02:00
parent fce19f193f
commit 7e1ec03f0c
44 changed files with 3875 additions and 84 deletions

View file

@ -1,4 +1,4 @@
"""Reusable primitives for FOS hub services."""
"""Contracts, reusable primitives, and runtime surfaces for HelixForge hubs."""
__all__ = ["__version__"]

View file

@ -0,0 +1,15 @@
"""Reusable conformance entrypoints for HelixForge hub runtimes."""
from hub_core.conformance.harness import (
CheckResult,
ConformanceHarness,
ConformanceReport,
find_secret_violations,
)
__all__ = [
"CheckResult",
"ConformanceHarness",
"ConformanceReport",
"find_secret_violations",
]

View file

@ -0,0 +1,354 @@
from __future__ import annotations
import json
import re
from collections.abc import Mapping, Sequence
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from typing import Any, Literal, Protocol
from uuid import uuid4
from jsonschema import Draft202012Validator, FormatChecker
from hub_core.contracts import CONTRACT_VERSION, extension_contract_root
class ResponseLike(Protocol):
status_code: int
def json(self) -> Any: ...
class ConformanceTarget(Protocol):
"""Small HTTP surface shared by httpx.Client and FastAPI TestClient."""
def get(self, url: str, **kwargs: Any) -> ResponseLike: ...
def post(self, url: str, **kwargs: Any) -> ResponseLike: ...
CheckStatus = Literal["pass", "fail"]
@dataclass(frozen=True)
class CheckResult:
check_id: str
tier: int
status: CheckStatus
summary: str
@dataclass(frozen=True)
class ConformanceReport:
contract_version: str
checks: tuple[CheckResult, ...]
@property
def passed(self) -> bool:
return all(check.status == "pass" for check in self.checks)
@property
def passed_count(self) -> int:
return sum(check.status == "pass" for check in self.checks)
def to_dict(self) -> dict[str, Any]:
return {
"contract_version": self.contract_version,
"passed": self.passed,
"summary": {"passed": self.passed_count, "total": len(self.checks)},
"checks": [asdict(check) for check in self.checks],
}
class ConformanceHarness:
"""Run the implemented Tier 2/3 profile against an isolated HTTP target.
The target should be disposable or use a dedicated test namespace. The
harness writes the packaged ops-hub fixture plus one message and one event
from each implemented framework event family.
"""
def __init__(self, target: ConformanceTarget) -> None:
self.target = target
root = extension_contract_root()
self.package = _load_json(root.joinpath("fixtures", "ops-hub.extension.json"))
self.scenario = _load_json(root.joinpath("fixtures", "projection-rebuild.json"))
self.catalog = _load_json(root.joinpath("catalogs", "event-types.json"))
self.schema_root = root.joinpath("schemas")
def run(self) -> ConformanceReport:
results = [
self._check("C1", 2, "descriptor and manifest validate", self._schema_validate),
self._check("C6", 2, "fixtures contain no secret material", self._no_secrets),
self._check("C3", 2, "health endpoint passes", self._health_probe),
]
correlation_id = str(uuid4())
registration = self.target.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": correlation_id},
json=self.package,
)
duplicate = self.target.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": correlation_id},
json=self.package,
)
results.append(
self._check(
"C4",
2,
"manifest activation is idempotent",
lambda: _assert_registration_idempotent(registration, duplicate),
)
)
results.append(
self._check(
"C8",
2,
"registry write propagates correlation id",
lambda: _assert_correlation(registration, correlation_id),
)
)
progress = _materialize_event(self.scenario["authority"]["progress_events"][0])
interaction = _materialize_event(self.scenario["authority"]["interaction_events"][0])
progress_response = self.target.post("/ports/events/progress", json=progress)
interaction_response = self.target.post("/ports/events/interaction", json=interaction)
wrong_family = self.target.post("/ports/events/progress", json=interaction)
unknown_event = {**interaction, "event_type": "hub.interaction.uncataloged"}
unknown_response = self.target.post("/ports/events/interaction", json=unknown_event)
results.append(
self._check(
"C5",
2,
"cataloged events are accepted and invalid types rejected",
lambda: _assert_event_validation(
progress_response,
interaction_response,
wrong_family,
unknown_response,
),
)
)
message = _materialize_message(self.scenario["authority"]["messages"][0])
message_response = self.target.post("/ports/messaging/messages", json=message)
projections = {
projection_id: self.target.get(f"/ports/projections/{projection_id}")
for projection_id in self.scenario["expected_projections"]
}
results.append(
self._check(
"F2",
3,
"progress and interaction event families remain separate",
lambda: _assert_family_separation(
projections,
progress["correlation_id"],
interaction["correlation_id"],
),
)
)
results.append(
self._check(
"F3",
3,
"projections rebuild from authority fixture with provenance",
lambda: _assert_projection_rebuild(
projections,
self.scenario["expected_projections"],
message_response,
message["correlation_id"],
),
)
)
return ConformanceReport(contract_version=CONTRACT_VERSION, checks=tuple(results))
def _schema_validate(self) -> None:
_validator(self.schema_root.joinpath("hub-descriptor.schema.json")).validate(
self.package["descriptor"]
)
_validator(self.schema_root.joinpath("hub-manifest.schema.json")).validate(
self.package["manifest"]
)
_validator(self.schema_root.joinpath("event-type-catalog.schema.json")).validate(
self.catalog
)
if self.package["descriptor"]["reuse_surface_id"] != self.package["manifest"][
"reuse_surface_id"
]:
raise AssertionError("descriptor and manifest reuse_surface_id differ")
def _no_secrets(self) -> None:
violations = find_secret_violations(
{"package": self.package, "scenario": self.scenario, "catalog": self.catalog}
)
if violations:
raise AssertionError("secret-like material: " + ", ".join(violations))
def _health_probe(self) -> None:
response = self.target.get("/healthz")
_expect_status(response, 200, "health probe")
if response.json().get("status") != "ok":
raise AssertionError("health response status is not ok")
@staticmethod
def _check(
check_id: str,
tier: int,
summary: str,
operation: Any,
) -> CheckResult:
try:
operation()
except Exception as exc: # each check must produce a complete report
return CheckResult(check_id, tier, "fail", f"{summary}: {exc}")
return CheckResult(check_id, tier, "pass", summary)
SECRET_KEY = re.compile(
r"(?:^|_)(?:api_?(?:key|token)|access_?token|auth_?token|client_?secret|credential|passwd|password|private_?key|secret)(?:$|_)",
re.IGNORECASE,
)
SECRET_VALUE = re.compile(
r"(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis)://[^\s/:]+:[^\s/@]+@|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
re.IGNORECASE,
)
def find_secret_violations(value: Any, path: str = "$") -> list[str]:
"""Return JSON paths containing credential-shaped keys or values."""
violations: list[str] = []
if isinstance(value, Mapping):
for key, child in value.items():
child_path = f"{path}.{key}"
if SECRET_KEY.search(str(key)):
violations.append(child_path)
violations.extend(find_secret_violations(child, child_path))
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
for index, child in enumerate(value):
violations.extend(find_secret_violations(child, f"{path}[{index}]"))
elif isinstance(value, str) and SECRET_VALUE.search(value):
violations.append(path)
return violations
def _assert_registration_idempotent(
registration: ResponseLike,
duplicate: ResponseLike,
) -> None:
_expect_status(registration, 202, "initial activation")
_expect_status(duplicate, 202, "duplicate activation")
if registration.json().get("status") not in {"accepted", "duplicate"}:
raise AssertionError("initial activation did not return an activation status")
if duplicate.json().get("status") != "duplicate":
raise AssertionError("second activation was not reported as duplicate")
def _assert_correlation(response: ResponseLike, expected: str) -> None:
_expect_status(response, 202, "correlated registry write")
if response.json().get("correlation_id") != expected:
raise AssertionError("response correlation_id does not match request")
def _assert_event_validation(
progress: ResponseLike,
interaction: ResponseLike,
wrong_family: ResponseLike,
unknown: ResponseLike,
) -> None:
_expect_status(progress, 202, "progress event")
_expect_status(interaction, 202, "interaction event")
_expect_status(wrong_family, 422, "wrong-family event")
_expect_status(unknown, 422, "uncataloged event")
def _assert_family_separation(
projections: Mapping[str, ResponseLike],
progress_correlation: str,
interaction_correlation: str,
) -> None:
progress_items = _projection_items(projections["progress_events"], "progress_events")
interaction_items = _projection_items(
projections["interaction_events"], "interaction_events"
)
progress_correlations = {item.get("correlation_id") for item in progress_items}
interaction_correlations = {item.get("correlation_id") for item in interaction_items}
if progress_correlation not in progress_correlations:
raise AssertionError("progress fixture missing from progress projection")
if interaction_correlation not in interaction_correlations:
raise AssertionError("interaction fixture missing from interaction projection")
if progress_correlation in interaction_correlations or interaction_correlation in progress_correlations:
raise AssertionError("event correlation crossed family projection boundary")
if any(item.get("family") != "progress" for item in progress_items):
raise AssertionError("progress projection contains another event family")
if any(item.get("family") != "interaction" for item in interaction_items):
raise AssertionError("interaction projection contains another event family")
def _assert_projection_rebuild(
projections: Mapping[str, ResponseLike],
expectations: Mapping[str, Any],
message_response: ResponseLike,
message_correlation: str,
) -> None:
_expect_status(message_response, 202, "fixture message")
for projection_id, expected in expectations.items():
response = projections[projection_id]
_expect_status(response, 200, f"{projection_id} projection")
body = response.json()
data = body.get("data", {})
if data.get("projection_id") != projection_id:
raise AssertionError(f"{projection_id} identity was not preserved")
if data.get("rebuild_from") != expected["rebuild_from"]:
raise AssertionError(f"{projection_id} rebuild sources differ")
provenance = body.get("provenance", {})
if not provenance.get("source_ref") or not provenance.get("schema_version"):
raise AssertionError(f"{projection_id} lacks provenance")
if not provenance.get("content_hash"):
raise AssertionError(f"{projection_id} lacks content hash")
registry_items = _projection_items(projections["hub_registry"], "hub_registry")
if not any(item.get("descriptor", {}).get("hub_slug") == "ops-hub" for item in registry_items):
raise AssertionError("ops-hub authority fixture missing from registry projection")
message_items = _projection_items(projections["messages"], "messages")
if not any(item.get("correlation_id") == message_correlation for item in message_items):
raise AssertionError("message authority fixture missing from message projection")
def _projection_items(response: ResponseLike, projection_id: str) -> list[dict[str, Any]]:
_expect_status(response, 200, f"{projection_id} projection")
items = response.json().get("data", {}).get("items")
if not isinstance(items, list):
raise AssertionError(f"{projection_id} items are not a list")
return items
def _materialize_event(template: Mapping[str, Any]) -> dict[str, Any]:
return {
**template,
"correlation_id": str(uuid4()),
"occurred_at": datetime.now(timezone.utc).isoformat(),
}
def _materialize_message(template: Mapping[str, Any]) -> dict[str, Any]:
return {**template, "correlation_id": str(uuid4()), "conversation_id": str(uuid4())}
def _expect_status(response: ResponseLike, expected: int, purpose: str) -> None:
if response.status_code != expected:
detail = json.dumps(response.json(), sort_keys=True)
raise AssertionError(f"{purpose} returned {response.status_code}, expected {expected}: {detail}")
def _validator(resource: Any) -> Draft202012Validator:
schema = _load_json(resource)
Draft202012Validator.check_schema(schema)
return Draft202012Validator(schema, format_checker=FormatChecker())
def _load_json(resource: Any) -> Any:
return json.loads(resource.read_text(encoding="utf-8"))

View file

@ -0,0 +1,16 @@
"""Packaged HelixForge hub contract artifacts."""
from importlib.resources import files
from importlib.resources.abc import Traversable
CONTRACT_ID = "helixforge.hub-extension"
CONTRACT_VERSION = "0.1.0"
def extension_contract_root() -> Traversable:
"""Return the packaged root for the current hub-extension contract."""
return files("hub_core.contracts.helixforge_hub_extension.v0_1_0")
__all__ = ["CONTRACT_ID", "CONTRACT_VERSION", "extension_contract_root"]

View file

@ -0,0 +1 @@
"""Versioned ``helixforge.hub-extension`` contract packages."""

View file

@ -0,0 +1,26 @@
# helixforge.hub-extension 0.1.0
Tier 1 contract artifacts implementing `SHR-ARCH-HUB-0001` and
`SHR-ARCH-IA-0001`:
- `schemas/hub-descriptor.schema.json` — hub identity and version negotiation;
- `schemas/hub-manifest.schema.json` — provided/consumed capabilities, ports,
events, endpoints, policies, and operator surfaces;
- `schemas/event-type-catalog.schema.json` — catalog entries with event family,
ownership, sensitivity, and payload schema references;
- `openapi/ports.openapi.json` — OpenAPI 3.1 fragments for every v0.1 named port;
- `catalogs/event-types.json` — initial progress and interaction event catalog;
- `fixtures/ops-hub.extension.json` — non-secret domain/aspect hub package;
- `fixtures/projection-rebuild.json` — deterministic authority inputs and
expected rebuild sources for framework projections;
- `compatibility-matrix.json` — version and Core Hub migration compatibility.
The package is descriptive and versioned. Runtime conformance belongs to Tier
2/3 tests exposed through `hub_core.conformance` and `hub-core conformance`.
Contract changes are additive within a minor line; breaking field,
enum, or semantic changes require a new major version and a dual-run window.
Manifest and event payloads must never contain credential material. Endpoint
entries carry only discovery keys or non-secret URLs. Authorization decisions,
repository work authority, secrets, and schedules remain behind their assigned
ports.

View file

@ -0,0 +1 @@
"""Contract artifacts for ``helixforge.hub-extension`` version 0.1.0."""

View file

@ -0,0 +1,53 @@
{
"catalog_version": "0.1.0",
"event_types": [
{
"type": "hub.progress.recorded",
"display_name": "Hub Progress Recorded",
"description": "Coordination audit evidence linked to work or another governed subject.",
"family": "progress",
"schema_version": "0.1.0",
"owner": "hub-core",
"sensitivity": "operational",
"payload_schema_ref": "urn:helixforge:event:hub.progress.recorded:0.1.0",
"correlation_required": true,
"retention_class": "audit"
},
{
"type": "hub.interaction.recorded",
"display_name": "Hub Interaction Recorded",
"description": "Framework or operator interaction evidence distinct from coordination progress.",
"family": "interaction",
"schema_version": "0.1.0",
"owner": "hub-core",
"sensitivity": "operational",
"payload_schema_ref": "urn:helixforge:event:hub.interaction.recorded:0.1.0",
"correlation_required": true,
"retention_class": "audit"
},
{
"type": "repository.change.observed",
"display_name": "Repository Change Observed",
"description": "A repository revision or governed mutation observed by repo-manager.",
"family": "repository_change",
"schema_version": "0.1.0",
"owner": "repo-manager",
"sensitivity": "operational",
"payload_schema_ref": "urn:helixforge:event:repository.change.observed:0.1.0",
"correlation_required": false,
"retention_class": "audit"
},
{
"type": "ops.endpoint.verified",
"display_name": "Operations Endpoint Verified",
"description": "A domain-owned operations endpoint passed its declared verification.",
"family": "domain",
"schema_version": "0.1.0",
"owner": "ops-hub",
"sensitivity": "operational",
"payload_schema_ref": "urn:helixforge:event:ops.endpoint.verified:0.1.0",
"correlation_required": true,
"retention_class": "domain_policy"
}
]
}

View file

@ -0,0 +1,33 @@
{
"contract_id": "helixforge.hub-extension",
"current_version": "0.1.0",
"versions": [
{
"version": "0.1.0",
"status": "current",
"compatible_min": "0.1.0",
"compatible_max": "0.1.0",
"breaking": false
}
],
"migration_adapters": [
{
"source": "core-hub.hub-manifest",
"source_version": "0.1",
"target_version": "0.1.0",
"status": "required-during-dual-run",
"mapping": {
"hub_slug": "descriptor.hub_slug",
"manifest_version": "manifest.manifest_version",
"capabilities": "manifest.provides",
"endpoints": "manifest.endpoints"
},
"notes": "The adapter must supply descriptor identity/version fields and explicit consumes/event lists; it must not infer credentials from endpoint metadata."
}
],
"change_policy": {
"patch": "Clarifications and compatible constraint corrections only.",
"minor": "Additive optional fields, ports, event types, and enum values.",
"major": "Removed or renamed fields, narrowed enums, changed authority semantics, or incompatible port behavior; requires a dual-run window."
}
}

View file

@ -0,0 +1,72 @@
{
"descriptor": {
"contract_id": "helixforge.hub-extension",
"contract_version": "0.1.0",
"hub_slug": "ops-hub",
"display_name": "Operations Hub",
"description": "Operations aspect hub fixture for contract and composition tests.",
"domain": "operations",
"hub_kind": "aspect",
"status": "active",
"reuse_surface_id": "capability.operations.ops-hub",
"contract_version_min": "0.1.0",
"contract_version_max": "0.1.0",
"vsm_system": "operations",
"vsm_function": "coordination"
},
"manifest": {
"manifest_version": "0.1.0",
"schema_version": "0.1.0",
"reuse_surface_id": "capability.operations.ops-hub",
"provides": [
"capability.operations.ops-hub",
"capability.operations.service-catalog"
],
"consumes": [
"port.registry",
"port.messaging",
"port.events.interaction",
"port.projection.query",
"port.policy"
],
"events_emitted": [
"ops.endpoint.verified"
],
"events_consumed": [
"hub.progress.recorded"
],
"endpoints": [
{
"id": "api",
"discovery_key": "service.ops-hub.http"
},
{
"id": "docs",
"url": "https://ops-hub.example.invalid/docs"
}
],
"policy_scopes": [
"ops.catalog.read",
"ops.evidence.write"
],
"widgets": [
{
"id": "ops-overview",
"kind": "widget",
"description": "Non-secret operations overview surface."
}
],
"operator_surfaces": [
{
"id": "ops-console",
"kind": "console",
"description": "Operator-facing operations console."
},
{
"id": "ops-mcp",
"kind": "mcp",
"description": "Policy-bound operations MCP surface."
}
]
}
}

View file

@ -0,0 +1,37 @@
{
"contract_version": "0.1.0",
"extension_fixture": "ops-hub.extension.json",
"authority": {
"messages": [
{
"schema_version": "0.1.0",
"from_address": "hub:ops-hub",
"to_addresses": ["agent:conformance"],
"body": "Projection rebuild conformance fixture.",
"subject_refs": {"hub": "ops-hub", "fixture": "projection-rebuild"}
}
],
"progress_events": [
{
"schema_version": "0.1.0",
"event_type": "hub.progress.recorded",
"subject_refs": {"hub": "ops-hub", "fixture": "projection-rebuild"},
"payload": {"result": "fixture-progress"}
}
],
"interaction_events": [
{
"schema_version": "0.1.0",
"event_type": "hub.interaction.recorded",
"subject_refs": {"hub": "ops-hub", "fixture": "projection-rebuild"},
"payload": {"result": "fixture-interaction"}
}
]
},
"expected_projections": {
"hub_registry": {"rebuild_from": ["hub_descriptors", "hub_manifests"]},
"messages": {"rebuild_from": ["messages"]},
"progress_events": {"rebuild_from": ["progress_events"]},
"interaction_events": {"rebuild_from": ["interaction_events"]}
}
}

View file

@ -0,0 +1,669 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "HelixForge Hub Extension Ports",
"version": "0.1.0",
"description": "Implementation-neutral HTTP fragments for helixforge.hub-extension named ports. Implementations may mount compatibility aliases while these operations remain the stable contract."
},
"security": [
{
"bearerAuth": []
}
],
"paths": {
"/ports/registry/registrations": {
"post": {
"operationId": "registerHubExtension",
"summary": "Register or idempotently update a hub descriptor and manifest",
"tags": ["registry"],
"x-port-id": "port.registry",
"x-direction": "in",
"parameters": [
{
"$ref": "#/components/parameters/CorrelationHeader"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegistryRegistration"
}
}
}
},
"responses": {
"202": {
"$ref": "#/components/responses/Accepted"
},
"400": {
"$ref": "#/components/responses/InvalidRequest"
}
}
}
},
"/ports/addressing/resolve/{address}": {
"get": {
"operationId": "resolveAddress",
"summary": "Resolve a qualified agent, hub, domain, or component address",
"tags": ["addressing"],
"x-port-id": "port.addressing",
"x-direction": "out",
"parameters": [
{
"name": "address",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 240
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/Record"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/ports/messaging/messages": {
"get": {
"operationId": "listAddressedMessages",
"summary": "Read retained messages addressed to a participant",
"tags": ["messaging"],
"x-port-id": "port.messaging",
"x-direction": "out",
"parameters": [
{
"name": "address",
"in": "query",
"required": true,
"schema": {
"type": "string",
"minLength": 1
}
},
{
"name": "conversation_id",
"in": "query",
"required": false,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/Collection"
}
}
},
"post": {
"operationId": "sendAddressedMessage",
"summary": "Append an addressed message to a conversation",
"tags": ["messaging"],
"x-port-id": "port.messaging",
"x-direction": "in",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageCommand"
}
}
}
},
"responses": {
"202": {
"$ref": "#/components/responses/Accepted"
}
}
}
},
"/ports/events/progress": {
"post": {
"operationId": "appendProgressEvent",
"summary": "Append coordination progress evidence",
"tags": ["events"],
"x-port-id": "port.events.progress",
"x-direction": "in",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EventCommand"
}
}
}
},
"responses": {
"202": {
"$ref": "#/components/responses/Accepted"
}
}
}
},
"/ports/events/interaction": {
"post": {
"operationId": "appendInteractionEvent",
"summary": "Append framework or domain interaction evidence",
"tags": ["events"],
"x-port-id": "port.events.interaction",
"x-direction": "in",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EventCommand"
}
}
}
},
"responses": {
"202": {
"$ref": "#/components/responses/Accepted"
}
}
}
},
"/ports/projections/{projection_id}": {
"get": {
"operationId": "queryProjection",
"summary": "Read a rebuildable projection with provenance",
"tags": ["projections"],
"x-port-id": "port.projection.query",
"x-direction": "out",
"parameters": [
{
"name": "projection_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z][a-z0-9_.-]*$"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/Record"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/ports/repos/{repo_slug}": {
"get": {
"operationId": "getRepositoryReference",
"summary": "Resolve repository metadata through repo-manager",
"tags": ["repositories"],
"x-port-id": "port.repo",
"x-direction": "out",
"parameters": [
{
"name": "repo_slug",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/Record"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/ports/work/{record_id}": {
"get": {
"operationId": "getWorkRecordProjection",
"summary": "Read a repository-authoritative work-record projection",
"tags": ["work"],
"x-port-id": "port.work",
"x-direction": "out",
"parameters": [
{
"name": "record_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 160
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/Record"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/ports/policy/evaluations": {
"post": {
"operationId": "evaluatePolicy",
"summary": "Request an authorization or policy decision from its authority",
"tags": ["policy"],
"x-port-id": "port.policy",
"x-direction": "out",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PolicyEvaluation"
}
}
}
},
"responses": {
"200": {
"$ref": "#/components/responses/Record"
},
"503": {
"description": "Policy authority unavailable; callers fail closed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PortError"
}
}
}
}
}
}
},
"/ports/telemetry/samples": {
"post": {
"operationId": "submitTelemetrySample",
"summary": "Submit correlated cost or usage telemetry",
"tags": ["telemetry"],
"x-port-id": "port.telemetry",
"x-direction": "in",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TelemetryCommand"
}
}
}
},
"responses": {
"202": {
"$ref": "#/components/responses/Accepted"
}
}
}
},
"/ports/schedule/requests": {
"post": {
"operationId": "requestScheduledExecution",
"summary": "Request activity-core execution without embedding a scheduler",
"tags": ["schedule"],
"x-port-id": "port.schedule",
"x-direction": "out",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScheduleCommand"
}
}
}
},
"responses": {
"202": {
"$ref": "#/components/responses/Accepted"
}
}
}
}
},
"components": {
"parameters": {
"CorrelationHeader": {
"name": "X-Correlation-ID",
"in": "header",
"required": true,
"description": "UUIDv7 correlation identifier for the registration action.",
"schema": {
"$ref": "#/components/schemas/CorrelationId"
}
}
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
},
"schemas": {
"RegistryRegistration": {
"type": "object",
"additionalProperties": false,
"required": ["descriptor", "manifest"],
"properties": {
"descriptor": {
"$ref": "../schemas/hub-descriptor.schema.json"
},
"manifest": {
"$ref": "../schemas/hub-manifest.schema.json"
}
}
},
"MessageCommand": {
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "correlation_id", "from_address", "to_addresses", "body"],
"properties": {
"schema_version": {
"type": "string"
},
"correlation_id": {
"$ref": "#/components/schemas/CorrelationId"
},
"conversation_id": {
"type": "string",
"format": "uuid"
},
"from_address": {
"type": "string",
"minLength": 1
},
"to_addresses": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
},
"body": {
"type": "string",
"minLength": 1
},
"subject_refs": {
"$ref": "#/components/schemas/SubjectRefs"
}
}
},
"EventCommand": {
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "correlation_id", "event_type", "occurred_at", "payload"],
"properties": {
"schema_version": {
"type": "string"
},
"correlation_id": {
"$ref": "#/components/schemas/CorrelationId"
},
"event_type": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9][a-z0-9-]*)+$"
},
"occurred_at": {
"type": "string",
"format": "date-time"
},
"subject_refs": {
"$ref": "#/components/schemas/SubjectRefs"
},
"payload": {
"type": "object"
}
}
},
"PolicyEvaluation": {
"type": "object",
"additionalProperties": false,
"required": ["correlation_id", "subject", "action", "resource"],
"properties": {
"correlation_id": {
"$ref": "#/components/schemas/CorrelationId"
},
"subject": {
"type": "string",
"minLength": 1
},
"action": {
"type": "string",
"minLength": 1
},
"resource": {
"type": "string",
"minLength": 1
},
"context": {
"type": "object"
}
}
},
"TelemetryCommand": {
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "correlation_id", "metric", "value", "recorded_at"],
"properties": {
"schema_version": {
"type": "string"
},
"correlation_id": {
"$ref": "#/components/schemas/CorrelationId"
},
"metric": {
"type": "string",
"minLength": 1
},
"value": {
"type": "number"
},
"unit": {
"type": "string"
},
"recorded_at": {
"type": "string",
"format": "date-time"
},
"subject_refs": {
"$ref": "#/components/schemas/SubjectRefs"
}
}
},
"ScheduleCommand": {
"type": "object",
"additionalProperties": false,
"required": ["correlation_id", "capability", "requested_for", "input"],
"properties": {
"correlation_id": {
"$ref": "#/components/schemas/CorrelationId"
},
"capability": {
"type": "string",
"pattern": "^capability\\.[a-z0-9][a-z0-9-]*\\.[a-z0-9][a-z0-9-]*$"
},
"requested_for": {
"type": "string",
"format": "date-time"
},
"input": {
"type": "object"
}
}
},
"CorrelationId": {
"type": "string",
"format": "uuid",
"description": "UUIDv7 correlation identifier for one action spanning information kinds."
},
"SubjectRefs": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"PortAccepted": {
"type": "object",
"additionalProperties": false,
"required": ["id", "status", "correlation_id"],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"status": {
"enum": ["accepted", "duplicate"]
},
"correlation_id": {
"$ref": "#/components/schemas/CorrelationId"
}
}
},
"PortRecord": {
"type": "object",
"additionalProperties": false,
"required": ["id", "data", "provenance"],
"properties": {
"id": {
"type": "string"
},
"data": {
"type": "object"
},
"provenance": {
"type": "object",
"required": ["source_system", "source_ref", "schema_version"],
"properties": {
"source_system": {
"type": "string"
},
"source_ref": {
"type": "string"
},
"schema_version": {
"type": "string"
},
"content_hash": {
"type": "string"
},
"indexed_at": {
"type": "string",
"format": "date-time"
}
}
}
}
},
"PortCollection": {
"type": "object",
"additionalProperties": false,
"required": ["items"],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PortRecord"
}
},
"next_cursor": {
"type": ["string", "null"]
}
}
},
"PortError": {
"type": "object",
"additionalProperties": false,
"required": ["code", "message"],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"correlation_id": {
"$ref": "#/components/schemas/CorrelationId"
}
}
}
},
"responses": {
"Accepted": {
"description": "Command accepted idempotently",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PortAccepted"
}
}
}
},
"Record": {
"description": "Projected record with provenance",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PortRecord"
}
}
}
},
"Collection": {
"description": "Projected collection",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PortCollection"
}
}
}
},
"InvalidRequest": {
"description": "Contract validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PortError"
}
}
}
},
"NotFound": {
"description": "Requested projection or authority reference was not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PortError"
}
}
}
}
}
}
}

View file

@ -0,0 +1,84 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.helixforge.local/helixforge.hub-extension/0.1.0/event-type-catalog.schema.json",
"title": "HelixForge Event Type Catalog",
"description": "Catalog of versioned, non-secret event types with distinct semantic families.",
"type": "object",
"additionalProperties": false,
"required": ["catalog_version", "event_types"],
"properties": {
"catalog_version": {
"$ref": "#/$defs/semver"
},
"event_types": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/eventType"
}
}
},
"$defs": {
"semver": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
},
"eventType": {
"type": "object",
"additionalProperties": false,
"required": [
"type",
"display_name",
"description",
"family",
"schema_version",
"owner",
"sensitivity",
"payload_schema_ref",
"correlation_required"
],
"properties": {
"type": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9][a-z0-9-]*)+$",
"maxLength": 180
},
"display_name": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"description": {
"type": "string",
"minLength": 1,
"maxLength": 1000
},
"family": {
"enum": ["progress", "interaction", "repository_change", "domain"]
},
"schema_version": {
"$ref": "#/$defs/semver"
},
"owner": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$",
"maxLength": 80
},
"sensitivity": {
"enum": ["public_internal", "operational", "personal"]
},
"payload_schema_ref": {
"type": "string",
"minLength": 1,
"maxLength": 500
},
"correlation_required": {
"type": "boolean"
},
"retention_class": {
"enum": ["audit", "operational", "domain_policy"]
}
}
}
}
}

View file

@ -0,0 +1,84 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.helixforge.local/helixforge.hub-extension/0.1.0/hub-descriptor.schema.json",
"title": "HelixForge Hub Descriptor",
"description": "Identity and contract-version declaration for a domain or aspect hub.",
"type": "object",
"additionalProperties": false,
"required": [
"contract_id",
"contract_version",
"hub_slug",
"display_name",
"domain",
"hub_kind",
"status",
"reuse_surface_id",
"contract_version_min",
"contract_version_max"
],
"properties": {
"contract_id": {
"const": "helixforge.hub-extension"
},
"contract_version": {
"$ref": "#/$defs/semver"
},
"hub_slug": {
"$ref": "#/$defs/slug"
},
"display_name": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"description": {
"type": "string",
"maxLength": 2000
},
"domain": {
"$ref": "#/$defs/slug"
},
"hub_kind": {
"enum": ["domain", "aspect"]
},
"status": {
"enum": ["draft", "active", "deprecated", "retired"]
},
"reuse_surface_id": {
"$ref": "#/$defs/capabilityId"
},
"contract_version_min": {
"$ref": "#/$defs/semver"
},
"contract_version_max": {
"$ref": "#/$defs/semver"
},
"vsm_system": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"vsm_function": {
"type": "string",
"minLength": 1,
"maxLength": 120
}
},
"$defs": {
"slug": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$",
"maxLength": 80
},
"semver": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
},
"capabilityId": {
"type": "string",
"pattern": "^capability\\.[a-z0-9][a-z0-9-]*\\.[a-z0-9][a-z0-9-]*$",
"maxLength": 160
}
}
}

View file

@ -0,0 +1,164 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.helixforge.local/helixforge.hub-extension/0.1.0/hub-manifest.schema.json",
"title": "HelixForge Hub Capability Manifest",
"description": "Versioned capabilities, ports, events, and non-secret integration metadata for a hub.",
"type": "object",
"additionalProperties": false,
"required": [
"manifest_version",
"schema_version",
"reuse_surface_id",
"provides",
"consumes",
"events_emitted",
"events_consumed"
],
"properties": {
"manifest_version": {
"$ref": "#/$defs/semver"
},
"schema_version": {
"$ref": "#/$defs/semver"
},
"reuse_surface_id": {
"$ref": "#/$defs/capabilityId"
},
"provides": {
"type": "array",
"items": {
"$ref": "#/$defs/capabilityId"
},
"uniqueItems": true
},
"consumes": {
"type": "array",
"items": {
"$ref": "#/$defs/portId"
},
"uniqueItems": true
},
"events_emitted": {
"type": "array",
"items": {
"$ref": "#/$defs/eventType"
},
"uniqueItems": true
},
"events_consumed": {
"type": "array",
"items": {
"$ref": "#/$defs/eventType"
},
"uniqueItems": true
},
"endpoints": {
"type": "array",
"items": {
"$ref": "#/$defs/endpoint"
},
"uniqueItems": true,
"default": []
},
"policy_scopes": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9_.:-]*$",
"maxLength": 160
},
"uniqueItems": true,
"default": []
},
"widgets": {
"type": "array",
"items": {
"$ref": "#/$defs/surface"
},
"uniqueItems": true,
"default": []
},
"operator_surfaces": {
"type": "array",
"items": {
"$ref": "#/$defs/surface"
},
"uniqueItems": true,
"default": []
}
},
"$defs": {
"semver": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
},
"capabilityId": {
"type": "string",
"pattern": "^capability\\.[a-z0-9][a-z0-9-]*\\.[a-z0-9][a-z0-9-]*$",
"maxLength": 160
},
"eventType": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9][a-z0-9-]*)+$",
"maxLength": 180
},
"portId": {
"enum": [
"port.registry",
"port.addressing",
"port.messaging",
"port.events.progress",
"port.events.interaction",
"port.projection.query",
"port.repo",
"port.work",
"port.policy",
"port.telemetry",
"port.schedule"
]
},
"endpoint": {
"type": "object",
"additionalProperties": false,
"required": ["id"],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$"
},
"discovery_key": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"url": {
"type": "string",
"format": "uri",
"pattern": "^https?://"
}
},
"oneOf": [
{"required": ["discovery_key"]},
{"required": ["url"]}
]
},
"surface": {
"type": "object",
"additionalProperties": false,
"required": ["id", "kind"],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$"
},
"kind": {
"enum": ["widget", "console", "dashboard", "mcp", "cli"]
},
"description": {
"type": "string",
"maxLength": 1000
}
}
}
}
}

View file

@ -0,0 +1,21 @@
"""Primary hub-core runtime composition surfaces.
Exports are lazy so CLI commands such as migrations do not construct the ASGI
application as an import side effect.
"""
from typing import Any
__all__ = ["InMemoryPortStore", "PortStore", "create_app"]
def __getattr__(name: str) -> Any:
if name == "create_app":
from hub_core.runtime.app import create_app
return create_app
if name in {"InMemoryPortStore", "PortStore"}:
from hub_core.runtime.store import InMemoryPortStore, PortStore
return {"InMemoryPortStore": InMemoryPortStore, "PortStore": PortStore}[name]
raise AttributeError(name)

54
hub_core/runtime/app.py Normal file
View file

@ -0,0 +1,54 @@
from __future__ import annotations
from fastapi import FastAPI, Response, status
from hub_core import __version__
from hub_core.runtime.config import RuntimeSettings
from hub_core.runtime.models import HealthResponse, ReadinessResponse
from hub_core.runtime.ports import create_ports_router
from hub_core.runtime.store import InMemoryPortStore, PortStore
from hub_core.runtime.validation import ContractValidator
def create_app(
*,
settings: RuntimeSettings | None = None,
port_store: PortStore | None = None,
) -> FastAPI:
resolved_settings = settings or RuntimeSettings.from_env()
resolved_store = port_store or _create_store(resolved_settings)
app = FastAPI(
title="Hub Core Runtime",
version=__version__,
description="HelixForge hub framework and named-port runtime.",
)
app.state.settings = resolved_settings
app.state.port_store = resolved_store
app.state.contract_validator = ContractValidator()
@app.get("/healthz", response_model=HealthResponse, tags=["system"])
async def healthz() -> HealthResponse:
return HealthResponse(version=__version__)
@app.get("/readyz", response_model=ReadinessResponse, tags=["system"])
async def readyz(response: Response) -> ReadinessResponse:
ready = resolved_settings.is_ready(resolved_store.backend_name)
if not ready:
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return ReadinessResponse(
status="ok" if ready else "degraded",
checks=resolved_settings.readiness_checks(resolved_store.backend_name),
)
app.include_router(create_ports_router())
return app
def _create_store(settings: RuntimeSettings) -> PortStore:
if settings.backend == "memory":
return InMemoryPortStore()
raise RuntimeError(f"Unsupported HUB_CORE_BACKEND '{settings.backend}'")
app = create_app()

100
hub_core/runtime/cli.py Normal file
View file

@ -0,0 +1,100 @@
from __future__ import annotations
import argparse
import json
from importlib.resources import files
from typing import Sequence
from hub_core.mcp import HubCoreMCPServer
from hub_core.runtime.config import RuntimeSettings
def build_parser(settings: RuntimeSettings | None = None) -> argparse.ArgumentParser:
resolved = settings or RuntimeSettings.from_env()
parser = argparse.ArgumentParser(prog="hub-core", description="Hub Core runtime commands")
commands = parser.add_subparsers(dest="command", required=True)
api = commands.add_parser("api", help="Run the HTTP API and named ports")
api.add_argument("--host", default=resolved.api_host)
api.add_argument("--port", type=int, default=resolved.api_port)
mcp = commands.add_parser("mcp", help="Run the Hub Core MCP process")
mcp.add_argument("--host", default=resolved.mcp_host)
mcp.add_argument("--port", type=int, default=resolved.mcp_port)
mcp.add_argument("--transport", default=resolved.mcp_transport)
mcp.add_argument("--api-base", default=resolved.api_base)
migrate = commands.add_parser("migrate", help="Run packaged Alembic migrations")
migrate.add_argument("revision", nargs="?", default="head")
migrate.add_argument("--database-url", default=resolved.database_url)
conformance = commands.add_parser(
"conformance",
help="Run the implemented Tier 2/3 profile against an HTTP runtime",
)
conformance.add_argument("--base-url", default=resolved.api_base)
conformance.add_argument("--timeout", type=float, default=10.0)
conformance.add_argument("--json", action="store_true", dest="as_json")
return parser
def main(argv: Sequence[str] | None = None) -> int:
settings = RuntimeSettings.from_env()
args = build_parser(settings).parse_args(argv)
if args.command == "api":
_run_api(args.host, args.port)
return 0
if args.command == "mcp":
_run_mcp(args.host, args.port, args.transport, args.api_base)
return 0
if args.command == "migrate":
if not args.database_url:
raise SystemExit("hub-core migrate requires --database-url or HUB_CORE_DATABASE_URL")
_run_migrations(args.database_url, args.revision)
return 0
if args.command == "conformance":
return _run_conformance(args.base_url, args.timeout, args.as_json)
raise AssertionError(f"Unhandled command {args.command}")
def _run_api(host: str, port: int) -> None:
import uvicorn
uvicorn.run("hub_core.runtime.app:app", host=host, port=port)
def _run_mcp(host: str, port: int, transport: str, api_base: str) -> None:
server = HubCoreMCPServer(name="hub-core", api_base=api_base)
server.mcp.run(transport=transport, host=host, port=port)
def _run_migrations(database_url: str, revision: str) -> None:
from alembic import command
from alembic.config import Config
migration_root = files("hub_core.migrations")
config = Config()
config.set_main_option("script_location", str(migration_root))
config.set_main_option("sqlalchemy.url", _sync_database_url(database_url))
command.upgrade(config, revision)
def _run_conformance(base_url: str, timeout: float, as_json: bool) -> int:
import httpx
from hub_core.conformance import ConformanceHarness
with httpx.Client(base_url=base_url, timeout=timeout) as target:
report = ConformanceHarness(target).run()
if as_json:
print(json.dumps(report.to_dict(), indent=2, sort_keys=True))
else:
for check in report.checks:
print(f"{check.status.upper():4} Tier {check.tier} {check.check_id}: {check.summary}")
print(f"{report.passed_count}/{len(report.checks)} implemented checks passed")
return 0 if report.passed else 1
def _sync_database_url(database_url: str) -> str:
return database_url.replace("postgresql+asyncpg://", "postgresql+psycopg2://")

View file

@ -0,0 +1,59 @@
from __future__ import annotations
import os
from dataclasses import dataclass
def _env_bool(name: str, default: bool) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
@dataclass(frozen=True, slots=True)
class RuntimeSettings:
environment: str = "development"
backend: str = "memory"
allow_ephemeral: bool = True
api_host: str = "127.0.0.1"
api_port: int = 8010
api_base: str = "http://127.0.0.1:8010"
mcp_host: str = "127.0.0.1"
mcp_port: int = 8011
mcp_transport: str = "http"
database_url: str | None = None
@classmethod
def from_env(cls) -> RuntimeSettings:
environment = os.getenv("HUB_CORE_ENV", "development")
default_ephemeral = environment in {"development", "test"}
api_host = os.getenv("HUB_CORE_API_HOST", "127.0.0.1")
api_port = int(os.getenv("HUB_CORE_API_PORT", "8010"))
return cls(
environment=environment,
backend=os.getenv("HUB_CORE_BACKEND", "memory"),
allow_ephemeral=_env_bool("HUB_CORE_ALLOW_EPHEMERAL", default_ephemeral),
api_host=api_host,
api_port=api_port,
api_base=os.getenv("HUB_CORE_API_BASE", f"http://127.0.0.1:{api_port}"),
mcp_host=os.getenv("HUB_CORE_MCP_HOST", "127.0.0.1"),
mcp_port=int(os.getenv("HUB_CORE_MCP_PORT", "8011")),
mcp_transport=os.getenv("HUB_CORE_MCP_TRANSPORT", "http"),
database_url=os.getenv("HUB_CORE_DATABASE_URL") or os.getenv("DATABASE_URL"),
)
def readiness_checks(self, store_backend: str) -> dict[str, str]:
ephemeral_allowed = store_backend != "memory" or self.allow_ephemeral
return {
"environment": self.environment,
"configured_backend": self.backend,
"active_backend": store_backend,
"ephemeral_backend": "allowed" if ephemeral_allowed else "not_allowed",
"contract": "helixforge.hub-extension/0.1.0",
}
def is_ready(self, store_backend: str) -> bool:
return self.backend == store_backend and (
store_backend != "memory" or self.allow_ephemeral
)

View file

@ -0,0 +1,72 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
class RuntimeModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class RegistryRegistration(RuntimeModel):
descriptor: dict[str, Any]
manifest: dict[str, Any]
class MessageCommand(RuntimeModel):
schema_version: str
correlation_id: UUID
conversation_id: UUID | None = None
from_address: str = Field(min_length=1)
to_addresses: list[str] = Field(min_length=1)
body: str = Field(min_length=1)
subject_refs: dict[str, str] = Field(default_factory=dict)
class EventCommand(RuntimeModel):
schema_version: str
correlation_id: UUID
event_type: str = Field(pattern=r"^[a-z][a-z0-9]*(?:[.-][a-z0-9][a-z0-9-]*)+$")
occurred_at: datetime
subject_refs: dict[str, str] = Field(default_factory=dict)
payload: dict[str, Any]
class Provenance(RuntimeModel):
source_system: str
source_ref: str
schema_version: str
content_hash: str | None = None
indexed_at: datetime
class PortRecord(RuntimeModel):
id: str
data: dict[str, Any]
provenance: Provenance
class PortCollection(RuntimeModel):
items: list[PortRecord]
next_cursor: str | None = None
class PortAccepted(RuntimeModel):
id: str
status: Literal["accepted", "duplicate"]
correlation_id: UUID
class HealthResponse(RuntimeModel):
service: str = "hub-core"
status: Literal["ok"] = "ok"
version: str
class ReadinessResponse(RuntimeModel):
service: str = "hub-core"
status: Literal["ok", "degraded"]
checks: dict[str, str]

127
hub_core/runtime/ports.py Normal file
View file

@ -0,0 +1,127 @@
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from jsonschema import ValidationError
from hub_core.runtime.models import (
EventCommand,
MessageCommand,
PortAccepted,
PortCollection,
PortRecord,
RegistryRegistration,
)
from hub_core.runtime.store import PortStore
from hub_core.runtime.validation import ContractValidator
def get_port_store(request: Request) -> PortStore:
return request.app.state.port_store
def get_contract_validator(request: Request) -> ContractValidator:
return request.app.state.contract_validator
def create_ports_router() -> APIRouter:
router = APIRouter(prefix="/ports")
@router.post(
"/registry/registrations",
response_model=PortAccepted,
status_code=status.HTTP_202_ACCEPTED,
tags=["registry"],
openapi_extra={"x-port-id": "port.registry", "x-direction": "in"},
)
async def register_extension(
body: RegistryRegistration,
x_correlation_id: UUID = Header(alias="X-Correlation-ID"),
store: PortStore = Depends(get_port_store),
validator: ContractValidator = Depends(get_contract_validator),
) -> PortAccepted:
try:
validator.validate_registration(body)
except (ValidationError, ValueError) as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return await store.register_extension(body, x_correlation_id)
@router.get(
"/messaging/messages",
response_model=PortCollection,
tags=["messaging"],
openapi_extra={"x-port-id": "port.messaging", "x-direction": "out"},
)
async def list_messages(
address: str,
conversation_id: UUID | None = None,
store: PortStore = Depends(get_port_store),
) -> PortCollection:
return await store.list_messages(address, conversation_id)
@router.post(
"/messaging/messages",
response_model=PortAccepted,
status_code=status.HTTP_202_ACCEPTED,
tags=["messaging"],
openapi_extra={"x-port-id": "port.messaging", "x-direction": "in"},
)
async def send_message(
body: MessageCommand,
store: PortStore = Depends(get_port_store),
) -> PortAccepted:
return await store.send_message(body)
@router.post(
"/events/progress",
response_model=PortAccepted,
status_code=status.HTTP_202_ACCEPTED,
tags=["events"],
openapi_extra={"x-port-id": "port.events.progress", "x-direction": "in"},
)
async def append_progress(
body: EventCommand,
store: PortStore = Depends(get_port_store),
validator: ContractValidator = Depends(get_contract_validator),
) -> PortAccepted:
try:
validator.validate_event_family(body.event_type, "progress")
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return await store.append_progress(body)
@router.post(
"/events/interaction",
response_model=PortAccepted,
status_code=status.HTTP_202_ACCEPTED,
tags=["events"],
openapi_extra={"x-port-id": "port.events.interaction", "x-direction": "in"},
)
async def append_interaction(
body: EventCommand,
store: PortStore = Depends(get_port_store),
validator: ContractValidator = Depends(get_contract_validator),
) -> PortAccepted:
try:
validator.validate_event_family(body.event_type, "interaction")
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return await store.append_interaction(body)
@router.get(
"/projections/{projection_id}",
response_model=PortRecord,
tags=["projections"],
openapi_extra={"x-port-id": "port.projection.query", "x-direction": "out"},
)
async def query_projection(
projection_id: str,
store: PortStore = Depends(get_port_store),
) -> PortRecord:
projection = await store.query_projection(projection_id)
if projection is None:
raise HTTPException(status_code=404, detail=f"Projection '{projection_id}' not found")
return projection
return router

179
hub_core/runtime/store.py Normal file
View file

@ -0,0 +1,179 @@
from __future__ import annotations
import asyncio
import hashlib
import json
from collections.abc import Mapping
from copy import deepcopy
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import UUID, uuid4
from hub_core.contracts import CONTRACT_VERSION
from hub_core.runtime.models import (
EventCommand,
MessageCommand,
PortAccepted,
PortCollection,
PortRecord,
Provenance,
RegistryRegistration,
)
class PortStore(Protocol):
"""Persistence boundary for the initial hub-core runtime ports."""
backend_name: str
async def register_extension(
self,
registration: RegistryRegistration,
correlation_id: UUID,
) -> PortAccepted: ...
async def send_message(self, command: MessageCommand) -> PortAccepted: ...
async def list_messages(self, address: str, conversation_id: UUID | None) -> PortCollection: ...
async def append_progress(self, command: EventCommand) -> PortAccepted: ...
async def append_interaction(self, command: EventCommand) -> PortAccepted: ...
async def query_projection(self, projection_id: str) -> PortRecord | None: ...
class InMemoryPortStore:
"""Deterministic ephemeral backend for local runtime and conformance tests.
Production readiness rejects this backend unless explicitly allowed. The
store deliberately keeps progress and interaction event families separate.
"""
backend_name = "memory"
def __init__(self) -> None:
self._lock = asyncio.Lock()
self._registrations: dict[str, dict[str, Any]] = {}
self._messages: list[dict[str, Any]] = []
self._progress_events: list[dict[str, Any]] = []
self._interaction_events: list[dict[str, Any]] = []
async def register_extension(
self,
registration: RegistryRegistration,
correlation_id: UUID,
) -> PortAccepted:
hub_slug = str(registration.descriptor["hub_slug"])
value = registration.model_dump(mode="json")
async with self._lock:
duplicate = self._registrations.get(hub_slug) == value
self._registrations[hub_slug] = deepcopy(value)
return PortAccepted(
id=hub_slug,
status="duplicate" if duplicate else "accepted",
correlation_id=correlation_id,
)
async def send_message(self, command: MessageCommand) -> PortAccepted:
message_id = uuid4()
value = {
"id": str(message_id),
"created_at": _now().isoformat(),
**command.model_dump(mode="json"),
}
async with self._lock:
self._messages.append(value)
return PortAccepted(
id=str(message_id),
status="accepted",
correlation_id=command.correlation_id,
)
async def list_messages(self, address: str, conversation_id: UUID | None) -> PortCollection:
async with self._lock:
values = [
deepcopy(message)
for message in self._messages
if address in message["to_addresses"]
and (
conversation_id is None
or message.get("conversation_id") == str(conversation_id)
)
]
return PortCollection(items=[self._record("message", value) for value in values])
async def append_progress(self, command: EventCommand) -> PortAccepted:
return await self._append_event(command, self._progress_events, "progress")
async def append_interaction(self, command: EventCommand) -> PortAccepted:
return await self._append_event(command, self._interaction_events, "interaction")
async def query_projection(self, projection_id: str) -> PortRecord | None:
async with self._lock:
sources: Mapping[str, Any] = {
"hub_registry": list(self._registrations.values()),
"messages": self._messages,
"progress_events": self._progress_events,
"interaction_events": self._interaction_events,
}
if projection_id not in sources:
return None
items = deepcopy(sources[projection_id])
return self._record(
projection_id,
{
"projection_id": projection_id,
"items": items,
"rebuild_from": _rebuild_sources(projection_id),
},
)
async def _append_event(
self,
command: EventCommand,
target: list[dict[str, Any]],
family: str,
) -> PortAccepted:
event_id = uuid4()
value = {
"id": str(event_id),
"family": family,
"recorded_at": _now().isoformat(),
**command.model_dump(mode="json"),
}
async with self._lock:
target.append(value)
return PortAccepted(
id=str(event_id),
status="accepted",
correlation_id=command.correlation_id,
)
def _record(self, kind: str, value: dict[str, Any]) -> PortRecord:
encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
record_id = str(value.get("id") or kind)
return PortRecord(
id=record_id,
data=deepcopy(value),
provenance=Provenance(
source_system="hub-core-memory",
source_ref=f"memory://{kind}/{record_id}",
schema_version=CONTRACT_VERSION,
content_hash=hashlib.sha256(encoded).hexdigest(),
indexed_at=_now(),
),
)
def _now() -> datetime:
return datetime.now(timezone.utc)
def _rebuild_sources(projection_id: str) -> list[str]:
return {
"hub_registry": ["hub_descriptors", "hub_manifests"],
"messages": ["messages"],
"progress_events": ["progress_events"],
"interaction_events": ["interaction_events"],
}[projection_id]

View file

@ -0,0 +1,48 @@
from __future__ import annotations
import json
from typing import Any
from jsonschema import Draft202012Validator, FormatChecker
from hub_core.contracts import extension_contract_root
from hub_core.runtime.models import RegistryRegistration
class ContractValidator:
"""Validate runtime registration input against the packaged contract."""
def __init__(self) -> None:
contract_root = extension_contract_root()
schema_root = contract_root.joinpath("schemas")
self._descriptor = _validator(schema_root.joinpath("hub-descriptor.schema.json"))
self._manifest = _validator(schema_root.joinpath("hub-manifest.schema.json"))
catalog = json.loads(
contract_root.joinpath("catalogs", "event-types.json").read_text(encoding="utf-8")
)
self._event_families = {
entry["type"]: entry["family"] for entry in catalog["event_types"]
}
def validate_registration(self, registration: RegistryRegistration) -> None:
self._descriptor.validate(registration.descriptor)
self._manifest.validate(registration.manifest)
descriptor_id = registration.descriptor.get("reuse_surface_id")
manifest_id = registration.manifest.get("reuse_surface_id")
if descriptor_id != manifest_id:
raise ValueError("descriptor and manifest reuse_surface_id must match")
def validate_event_family(self, event_type: str, expected_family: str) -> None:
actual_family = self._event_families.get(event_type)
if actual_family is None:
raise ValueError(f"event type '{event_type}' is not cataloged")
if actual_family != expected_family:
raise ValueError(
f"event type '{event_type}' belongs to '{actual_family}', not '{expected_family}'"
)
def _validator(resource: Any) -> Draft202012Validator:
schema = json.loads(resource.read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
return Draft202012Validator(schema, format_checker=FormatChecker())