feat: add durable Core Hub absorption runtime
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 2s

This commit is contained in:
tegwick 2026-08-21 16:16:42 +02:00
parent 7e1ec03f0c
commit 8ab1d0c09a
20 changed files with 2423 additions and 15 deletions

View file

@ -1,9 +1,12 @@
from __future__ import annotations
from contextlib import asynccontextmanager
from fastapi import FastAPI, Response, status
from hub_core import __version__
from hub_core.runtime.config import RuntimeSettings
from hub_core.runtime.compat import SQLCompatibilityStore, create_compatibility_router
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
@ -17,37 +20,66 @@ def create_app(
) -> FastAPI:
resolved_settings = settings or RuntimeSettings.from_env()
resolved_store = port_store or _create_store(resolved_settings)
owns_store = port_store is None
@asynccontextmanager
async def lifespan(_: FastAPI):
yield
if owns_store and (closer := getattr(resolved_store, "aclose", None)):
await closer()
app = FastAPI(
title="Hub Core Runtime",
version=__version__,
description="HelixForge hub framework and named-port runtime.",
lifespan=lifespan,
)
app.state.settings = resolved_settings
app.state.port_store = resolved_store
app.state.compat_store = (
SQLCompatibilityStore(resolved_store)
if resolved_store.backend_name == "postgresql"
else None
)
app.state.contract_validator = ContractValidator()
@app.get("/healthz", response_model=HealthResponse, tags=["system"])
async def healthz() -> HealthResponse:
return HealthResponse(version=__version__)
return HealthResponse(
service="core-hub" if resolved_settings.legacy_health else "hub-core",
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)
dependency_checks = await resolved_store.readiness_checks()
ready = resolved_settings.is_ready(resolved_store.backend_name) and all(
value in {"ok", "not_applicable"} for value in dependency_checks.values()
)
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),
checks={
**resolved_settings.readiness_checks(resolved_store.backend_name),
**dependency_checks,
},
)
app.include_router(create_ports_router())
app.include_router(create_compatibility_router())
return app
def _create_store(settings: RuntimeSettings) -> PortStore:
if settings.backend == "memory":
return InMemoryPortStore()
if settings.backend == "postgresql":
if not settings.database_url:
raise RuntimeError("HUB_CORE_DATABASE_URL is required for PostgreSQL backend")
from hub_core.runtime.postgres_store import PostgresPortStore
return PostgresPortStore.from_url(settings.database_url)
raise RuntimeError(f"Unsupported HUB_CORE_BACKEND '{settings.backend}'")

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import argparse
import json
from importlib.resources import files
from pathlib import Path
from typing import Sequence
from hub_core.mcp import HubCoreMCPServer
@ -28,6 +29,21 @@ def build_parser(settings: RuntimeSettings | None = None) -> argparse.ArgumentPa
migrate.add_argument("revision", nargs="?", default="head")
migrate.add_argument("--database-url", default=resolved.database_url)
migration = commands.add_parser("migration", help="Export, validate, or import migration bundles")
migration_commands = migration.add_subparsers(dest="migration_command", required=True)
migration_validate = migration_commands.add_parser("validate")
migration_validate.add_argument("bundle", type=Path)
migration_validate.add_argument("--output", type=Path)
migration_import = migration_commands.add_parser("import")
migration_import.add_argument("bundle", type=Path)
migration_import.add_argument("--database-url", default=resolved.database_url)
migration_import.add_argument("--dry-run", action="store_true")
migration_import.add_argument("--output", type=Path)
migration_export = migration_commands.add_parser("export")
migration_export.add_argument("--database-url", default=resolved.database_url)
migration_export.add_argument("--source-revision")
migration_export.add_argument("--output", type=Path, required=True)
conformance = commands.add_parser(
"conformance",
help="Run the implemented Tier 2/3 profile against an HTTP runtime",
@ -53,6 +69,8 @@ def main(argv: Sequence[str] | None = None) -> int:
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 == "migration":
return _run_migration(args)
if args.command == "conformance":
return _run_conformance(args.base_url, args.timeout, args.as_json)
raise AssertionError(f"Unhandled command {args.command}")
@ -96,5 +114,38 @@ def _run_conformance(base_url: str, timeout: float, as_json: bool) -> int:
return 0 if report.passed else 1
def _run_migration(args: argparse.Namespace) -> int:
import asyncio
from hub_core.runtime.migration import (
export_bundle,
import_bundle,
load_bundle,
validate_bundle,
)
from hub_core.runtime.postgres_store import PostgresPortStore
if args.migration_command == "validate":
report = validate_bundle(load_bundle(args.bundle))
else:
if not args.database_url:
raise SystemExit("migration command requires --database-url or HUB_CORE_DATABASE_URL")
store = PostgresPortStore.from_url(args.database_url)
async def run() -> dict:
try:
if args.migration_command == "import":
return await import_bundle(store, load_bundle(args.bundle), dry_run=args.dry_run)
return await export_bundle(store, source_revision=args.source_revision)
finally:
await store.aclose()
report = asyncio.run(run())
rendered = json.dumps(report, indent=2, sort_keys=True)
if args.output:
args.output.write_text(rendered + "\n")
else:
print(rendered)
return 0 if report.get("ok", True) else 1
def _sync_database_url(database_url: str) -> str:
return database_url.replace("postgresql+asyncpg://", "postgresql+psycopg2://")

605
hub_core/runtime/compat.py Normal file
View file

@ -0,0 +1,605 @@
from __future__ import annotations
import hashlib
import hmac
import secrets
from datetime import datetime, timezone
from typing import Annotated, Any
from uuid import uuid4
import sqlalchemy as sa
from fastapi import APIRouter, Header, HTTPException, Request, status
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
from hub_core.runtime.compat_catalogs import (
ANNOTATION_CATEGORIES,
EVENT_TYPES,
POLICY_SCOPES,
WIDGET_TYPES,
)
from hub_core.runtime.models import EventCommand
from hub_core.runtime.postgres_store import PostgresPortStore
from hub_core.runtime.tables import (
compat_api_consumers,
compat_api_keys,
compat_hubs,
compat_manifests,
compat_widgets,
runtime_interaction_events,
)
from hub_core.utils.slugs import slugify_or_default
AuthorizationHeader = Annotated[str | None, Header()]
class SQLCompatibilityStore:
def __init__(self, ports: PostgresPortStore) -> None:
self.ports = ports
self.sessions = ports.sessions
async def validate_token(self, token: str) -> bool:
token_hash = _hash_token(token)
async with self.sessions() as session:
result = await session.execute(
sa.select(compat_api_keys.c.id).where(
compat_api_keys.c.key_hash == token_hash,
compat_api_keys.c.status == "active",
)
)
return result.scalar_one_or_none() is not None
async def list_hubs(self) -> list[dict[str, Any]]:
async with self.sessions() as session:
rows = (
await session.execute(sa.select(compat_hubs).order_by(compat_hubs.c.slug))
).mappings()
return [_hub(row) for row in rows]
async def create_hub(self, body: dict[str, Any]) -> dict[str, Any]:
now = _now()
row = {
"id": str(uuid4()),
"slug": body["slug"],
"name": body["name"],
"domain": body.get("domain"),
"hub_kind": body.get("hubKind"),
"hub_family": body.get("hubFamily"),
"vsm_function": body.get("vsmFunction"),
"vsm_system": body.get("vsmSystem"),
"status": body.get("status", "active"),
"description": body.get("description"),
"body": body,
"created_at": now,
"updated_at": now,
}
async with self.sessions.begin() as session:
await session.execute(compat_hubs.insert().values(**row))
await self.ports._audit(
session,
action="compat.hub.created",
subject_type="hub",
subject_id=row["id"],
correlation_id=None,
value=body,
)
return _hub(row)
async def list_manifests(self, hub_id: str | None = None) -> list[dict[str, Any]]:
statement = sa.select(compat_manifests).order_by(compat_manifests.c.created_at)
if hub_id:
statement = statement.where(compat_manifests.c.hub_id == hub_id)
async with self.sessions() as session:
rows = (await session.execute(statement)).mappings()
return [_manifest(row) for row in rows]
async def create_manifest(self, body: dict[str, Any]) -> dict[str, Any]:
now = _now()
hub_id = body.get("hubId")
hub_slug = None
if hub_id:
async with self.sessions() as session:
hub_slug = (
await session.execute(
sa.select(compat_hubs.c.slug).where(compat_hubs.c.id == hub_id)
)
).scalar_one_or_none()
row = {
"id": str(uuid4()),
"hub_id": hub_id,
"hub_slug": hub_slug,
"manifest_version": body.get("manifestVersion", "0.1.0"),
"status": body.get("status", "draft"),
"body": body,
"created_at": now,
"updated_at": now,
}
async with self.sessions.begin() as session:
await session.execute(compat_manifests.insert().values(**row))
await self.ports._audit(
session,
action="compat.manifest.created",
subject_type="manifest",
subject_id=row["id"],
correlation_id=None,
value=body,
)
return _manifest(row)
async def update_manifest(
self, manifest_id: str, body: dict[str, Any], *, activate: bool = False
) -> dict[str, Any]:
async with self.sessions.begin() as session:
current = (
await session.execute(
sa.select(compat_manifests).where(compat_manifests.c.id == manifest_id)
)
).mappings().one_or_none()
if current is None:
return {"id": manifest_id, "status": "missing"}
payload = dict(current["body"] or {})
payload.update(body)
values = {
"body": payload,
"manifest_version": payload.get(
"manifestVersion", current["manifest_version"]
),
"status": "active" if activate else current["status"],
"updated_at": _now(),
}
await session.execute(
compat_manifests.update()
.where(compat_manifests.c.id == manifest_id)
.values(**values)
)
await self.ports._audit(
session,
action="compat.manifest.activated" if activate else "compat.manifest.updated",
subject_type="manifest",
subject_id=manifest_id,
correlation_id=None,
value=payload,
)
return _manifest({**dict(current), **values})
async def list_consumers(self) -> list[dict[str, Any]]:
async with self.sessions() as session:
rows = (
await session.execute(
sa.select(compat_api_consumers).order_by(compat_api_consumers.c.name)
)
).mappings()
return [_consumer(row) for row in rows]
async def create_consumer(self, body: dict[str, Any]) -> dict[str, Any]:
row = {
"id": str(uuid4()),
"slug": body.get("slug") or slugify_or_default(body["name"]),
"name": body["name"],
"description": body.get("description"),
"hub_capability_manifest_id": body.get("hubCapabilityManifestId"),
"rate_limit_per_minute": body.get("rateLimitPerMinute"),
"quota_per_day": body.get("quotaPerDay"),
"key_prefix": None,
"status": body.get("status", "active"),
"body": body,
"created_at": _now(),
}
async with self.sessions.begin() as session:
await session.execute(compat_api_consumers.insert().values(**row))
await self.ports._audit(
session,
action="compat.consumer.created",
subject_type="api_consumer",
subject_id=row["id"],
correlation_id=None,
value=body,
)
return _consumer(row)
async def create_key(self, consumer_id: str, scopes: str | None) -> dict[str, Any]:
full_key = f"ch_{secrets.token_urlsafe(32)}"
prefix = full_key[:12]
row = {
"id": str(uuid4()),
"api_consumer_id": consumer_id,
"key_prefix": prefix,
"key_hash": _hash_token(full_key),
"scopes": scopes,
"status": "active",
"created_at": _now(),
}
async with self.sessions.begin() as session:
await session.execute(compat_api_keys.insert().values(**row))
await session.execute(
compat_api_consumers.update()
.where(compat_api_consumers.c.id == consumer_id)
.values(key_prefix=prefix)
)
await self.ports._audit(
session,
action="compat.api_key.created",
subject_type="api_key",
subject_id=row["id"],
correlation_id=None,
value={"apiConsumerId": consumer_id, "keyPrefix": prefix, "scopes": scopes},
)
return {
"fullKey": full_key,
"apiKey": {
"id": row["id"],
"apiConsumerId": consumer_id,
"keyPrefix": prefix,
"scopes": scopes,
"status": "active",
},
}
async def list_widgets(self) -> list[dict[str, Any]]:
async with self.sessions() as session:
rows = (
await session.execute(sa.select(compat_widgets).order_by(compat_widgets.c.name))
).mappings()
return [_widget(row) for row in rows]
async def create_widget(self, body: dict[str, Any]) -> dict[str, Any]:
row = {
"id": str(uuid4()),
"hub_id": body["hubId"],
"name": body["name"],
"widget_type": body.get("widgetType"),
"capability_ref": body.get("capabilityRef"),
"view_context": body.get("viewContext"),
"policy_scope": body.get("policyScope"),
"status": body.get("status", "active"),
"body": body,
"created_at": _now(),
}
async with self.sessions.begin() as session:
await session.execute(compat_widgets.insert().values(**row))
await self.ports._audit(
session,
action="compat.widget.created",
subject_type="widget",
subject_id=row["id"],
correlation_id=None,
value=body,
)
return _widget(row)
async def list_interactions(self) -> list[dict[str, Any]]:
async with self.sessions() as session:
rows = (
await session.execute(
sa.select(runtime_interaction_events).order_by(
runtime_interaction_events.c.recorded_at,
runtime_interaction_events.c.id,
)
)
).mappings()
values: list[dict[str, Any]] = []
for row in rows:
legacy = dict((row["payload"] or {}).get("legacy") or {})
if legacy:
legacy["id"] = row["id"]
values.append(legacy)
return values
async def create_interaction(self, body: dict[str, Any]) -> dict[str, Any]:
correlation_id = uuid4()
command = EventCommand(
schema_version="0.1.0",
correlation_id=correlation_id,
event_type=body["eventType"],
occurred_at=_now(),
subject_refs={"widget": body["widgetId"]},
payload={"legacy": body},
)
accepted = await self.ports.append_interaction(command)
return {"id": accepted.id, **body}
def create_compatibility_router() -> APIRouter:
router = APIRouter()
@router.get("/api/v2/widget-types")
async def widget_types(request: Request) -> list[dict[str, str]]:
_enabled(request, "system")
return WIDGET_TYPES
@router.get("/api/v2/event-types")
async def event_types(request: Request) -> list[dict[str, str]]:
_enabled(request, "system")
return EVENT_TYPES
@router.get("/api/v2/annotation-categories")
async def annotation_categories(request: Request) -> list[dict[str, str]]:
_enabled(request, "system")
return ANNOTATION_CATEGORIES
@router.get("/api/v2/policy-scopes")
async def policy_scopes(request: Request) -> list[dict[str, str]]:
_enabled(request, "system")
return POLICY_SCOPES
@router.get("/api/v2/hubs")
async def list_hubs(request: Request, authorization: AuthorizationHeader = None) -> dict:
await _protected(request, "registry", authorization)
return _page(await _store(request).list_hubs())
@router.post("/api/v2/hubs", status_code=status.HTTP_201_CREATED)
async def create_hub(
body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "registry", authorization, write=True)
return await _store(request).create_hub(body)
@router.get("/api/v2/hub-capability-manifests")
async def list_manifests(
request: Request,
authorization: AuthorizationHeader = None,
hubId: str | None = None,
) -> dict:
await _protected(request, "registry", authorization)
return _page(await _store(request).list_manifests(hubId))
@router.post("/api/v2/hub-capability-manifests", status_code=status.HTTP_201_CREATED)
async def create_manifest(
body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "registry", authorization, write=True)
return await _store(request).create_manifest(body)
@router.patch("/api/v2/hub-capability-manifests/{manifest_id}")
async def patch_manifest(
manifest_id: str,
body: dict[str, Any],
request: Request,
authorization: AuthorizationHeader = None,
) -> dict:
await _protected(request, "registry", authorization, write=True)
return await _store(request).update_manifest(manifest_id, body)
@router.post("/api/v2/hub-capability-manifests/{manifest_id}/activate")
async def activate_manifest(
manifest_id: str, request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "registry", authorization, write=True)
return await _store(request).update_manifest(manifest_id, {}, activate=True)
@router.get("/api/v2/hub-registry")
async def hub_registry(
request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "registry", authorization)
store = _store(request)
return {
"data": {
"hubs": await store.list_hubs(),
"hubCapabilityManifests": await store.list_manifests(),
}
}
@router.get("/api/v2/api-consumers")
async def list_consumers(
request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "credentials", authorization)
return _page(await _store(request).list_consumers())
@router.post("/api/v2/api-consumers", status_code=status.HTTP_201_CREATED)
async def create_consumer(
body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "credentials", authorization, write=True)
return await _store(request).create_consumer(body)
@router.post(
"/api/v2/api-consumers/{consumer_id}/api-keys",
status_code=status.HTTP_201_CREATED,
)
async def create_key(
consumer_id: str,
body: dict[str, Any],
request: Request,
authorization: AuthorizationHeader = None,
) -> dict:
await _protected(request, "credentials", authorization, write=True)
return await _store(request).create_key(consumer_id, body.get("scopes"))
@router.post("/api/v2/token")
async def token(request: Request, authorization: AuthorizationHeader = None) -> dict:
await _protected(request, "credentials", authorization)
return {"access_token": "already-authenticated", "token_type": "bearer"}
@router.get("/api/v2/widgets")
async def list_widgets(
request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "interaction", authorization)
return _page(await _store(request).list_widgets())
@router.post("/api/v2/widgets", status_code=status.HTTP_201_CREATED)
async def create_widget(
body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "interaction", authorization, write=True)
return await _store(request).create_widget(body)
@router.get("/api/v2/interaction-events")
async def list_interactions(
request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "interaction", authorization)
return _page(await _store(request).list_interactions())
@router.post("/api/v2/interaction-events", status_code=status.HTTP_201_CREATED)
async def create_interaction(
body: dict[str, Any], request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "interaction", authorization, write=True)
return await _store(request).create_interaction(body)
for index, path in enumerate((
"annotations",
"requirement-candidates",
"decision-records",
"deployment-records",
"outcome-signals",
)):
async def empty_collection(
request: Request, authorization: AuthorizationHeader = None
) -> dict:
await _protected(request, "deferred", authorization)
return {"data": []}
stem = path.replace("-", "_")
router.add_api_route(
f"/api/v2/{path}",
empty_collection,
methods=["GET"],
operation_id=f"compat_list_{stem}_{index}",
)
router.add_api_route(
f"/api/v2/{path}",
empty_collection,
methods=["POST"],
status_code=201,
operation_id=f"compat_create_{stem}_{index}",
)
@router.get("/api/v2/openapi.json", include_in_schema=False)
async def openapi_json(request: Request) -> dict[str, Any]:
_enabled(request, "system")
document = request.app.openapi()
paths = document.setdefault("paths", {})
for path, operations in list(paths.items()):
if path.startswith("/api/v2/"):
paths.setdefault(path.removeprefix("/api/v2"), operations)
return document
@router.get("/api/v2/openapi.yaml", include_in_schema=False)
async def openapi_yaml(request: Request) -> PlainTextResponse:
document = await openapi_json(request)
info = document.get("info", {})
return PlainTextResponse(
"openapi: 3.1.0\ninfo:\n"
f" title: {info.get('title', 'Hub Core Runtime')}\n"
f" version: {info.get('version', '0.2.0')}\n",
media_type="application/yaml",
)
@router.get("/api/v2/docs", include_in_schema=False)
async def docs(request: Request) -> RedirectResponse:
_enabled(request, "system")
return RedirectResponse(url="/docs")
@router.get("/console", response_class=HTMLResponse)
async def console(
request: Request, authorization: AuthorizationHeader = None
) -> HTMLResponse:
await _protected(request, "operator", authorization)
return HTMLResponse(
"<!doctype html><title>Hub Core Operator Console</title>"
"<h1>Hub Core Operator Console</h1>"
"<p>Compatibility runtime candidate; projections are authoritative.</p>"
)
return router
def _enabled(request: Request, group: str) -> None:
if group not in request.app.state.settings.v2_groups:
raise HTTPException(status_code=404, detail="compatibility group is disabled")
async def _protected(
request: Request,
group: str,
authorization: str | None,
*,
write: bool = False,
) -> None:
_enabled(request, group)
if write and group not in request.app.state.settings.v2_write_groups:
raise HTTPException(status_code=503, detail="compatibility group is read-only")
if not authorization or not authorization.startswith("Bearer "):
raise _unauthorized("Missing bearer token")
token = authorization.removeprefix("Bearer ").strip()
configured = request.app.state.settings.api_token
if configured and hmac.compare_digest(token, configured):
return
try:
valid = await _store(request).validate_token(token)
except Exception as exc:
raise HTTPException(status_code=503, detail="authorization dependency unavailable") from exc
if not valid:
raise _unauthorized("Invalid bearer token")
def _store(request: Request) -> SQLCompatibilityStore:
store = request.app.state.compat_store
if store is None:
raise HTTPException(status_code=503, detail="compatibility store unavailable")
return store
def _unauthorized(message: str) -> HTTPException:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"code": "unauthorized", "message": message},
)
def _page(values: list[dict[str, Any]]) -> dict[str, Any]:
return {"data": values, "count": len(values)}
def _hub(row: Any) -> dict[str, Any]:
value = dict(row["body"] or {})
value.update(
id=row["id"], slug=row["slug"], name=row["name"], domain=row["domain"],
hubKind=row["hub_kind"], hubFamily=row["hub_family"],
vsmFunction=row["vsm_function"], vsmSystem=row["vsm_system"],
status=row["status"], description=row["description"],
)
return {key: item for key, item in value.items() if item is not None}
def _manifest(row: Any) -> dict[str, Any]:
value = dict(row["body"] or {})
value.update(
id=row["id"], hubId=row["hub_id"], hubSlug=row["hub_slug"],
manifestVersion=row["manifest_version"], status=row["status"],
)
return {key: item for key, item in value.items() if item is not None}
def _consumer(row: Any) -> dict[str, Any]:
value = dict(row["body"] or {})
value.update(
id=row["id"], slug=row["slug"], name=row["name"],
description=row["description"],
hubCapabilityManifestId=row["hub_capability_manifest_id"],
rateLimitPerMinute=row["rate_limit_per_minute"], quotaPerDay=row["quota_per_day"],
keyPrefix=row["key_prefix"], status=row["status"],
)
return {key: item for key, item in value.items() if item is not None}
def _widget(row: Any) -> dict[str, Any]:
value = dict(row["body"] or {})
value.update(
id=row["id"], hubId=row["hub_id"], name=row["name"],
widgetType=row["widget_type"], capabilityRef=row["capability_ref"],
viewContext=row["view_context"], policyScope=row["policy_scope"],
status=row["status"],
)
return {key: item for key, item in value.items() if item is not None}
def _hash_token(token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()
def _now() -> datetime:
return datetime.now(timezone.utc)

View file

@ -0,0 +1,66 @@
from __future__ import annotations
def item(slug: str, name: str, description: str | None = None) -> dict[str, str]:
value = {"slug": slug, "name": name}
if description is not None:
value["description"] = description
return value
WIDGET_TYPES = [
item("status-summary", "Status Summary"),
item("workplan-board", "Workplan Board"),
item("event-stream", "Event Stream"),
item("action", "Action Control", "Button, link, or trigger widget"),
item("chart", "Chart", "Data visualisation chart widget"),
item("chat", "Chat Region", "Conversational interaction region"),
item("diff", "Diff / Review", "Code diff or change review element"),
item("form", "Form", "Data entry form widget"),
*[item(f"ops-{name}", f"ops-{name}") for name in (
"backup-set", "cluster", "endpoint", "environment", "host", "incident",
"migration-wave", "readiness-gate", "release", "risk", "runbook",
"secret-set", "service", "service-catalog",
)],
item("panel", "Status Panel", "Summary or status information panel"),
item("recommendation", "Recommendation", "AI or system recommendation block"),
item("table", "Table", "Tabular data display widget"),
item("workflow-step", "Workflow Step", "Single step in a multi-step workflow"),
]
EVENT_TYPES = [
item("interaction.event", "Interaction Event"),
item("workplan.progress", "Workplan Progress"),
item("evidence.recorded", "Evidence Recorded"),
*[
item(slug, slug.replace("_", " ").title())
for slug in (
"abandoned", "accepted_recommendation", "blocked_by_policy", "clicked",
"commented", "escalated", "failed", "flagged_confusing", "flagged_helpful",
"focused", "rejected_recommendation", "retracted", "retried", "submitted",
"viewed",
)
],
*[
item(f"ops-{name}", f"ops-{name}")
for name in (
"backup-verified", "drift-detected", "endpoint-verified", "health-checked",
"inventory-registered", "inventory-updated", "migration-gate-failed",
"migration-gate-passed", "readiness-gate-updated", "release-observed",
"restore-tested", "risk-accepted", "risk-raised", "runbook-executed",
"service-discovered",
)
],
]
ANNOTATION_CATEGORIES = [
item("operator-note", "Operator Note"),
item("compatibility-delta", "Compatibility Delta"),
item("migration-evidence", "Migration Evidence"),
]
POLICY_SCOPES = [
item("public-read", "Public Read"),
item("hub-write", "Hub Write"),
item("operator-admin", "Operator Admin"),
]

View file

@ -11,6 +11,10 @@ def _env_bool(name: str, default: bool) -> bool:
return raw.strip().lower() in {"1", "true", "yes", "on"}
def _env_set(name: str) -> frozenset[str]:
return frozenset(value.strip() for value in os.getenv(name, "").split(",") if value.strip())
@dataclass(frozen=True, slots=True)
class RuntimeSettings:
environment: str = "development"
@ -23,6 +27,21 @@ class RuntimeSettings:
mcp_port: int = 8011
mcp_transport: str = "http"
database_url: str | None = None
api_token: str | None = None
v2_groups: frozenset[str] = frozenset()
v2_write_groups: frozenset[str] = frozenset()
legacy_write_groups: frozenset[str] = frozenset()
legacy_health: bool = False
def __post_init__(self) -> None:
overlap = self.v2_write_groups & self.legacy_write_groups
if overlap:
joined = ", ".join(sorted(overlap))
raise ValueError(f"hub-core and Core Hub write groups overlap: {joined}")
unknown = self.v2_write_groups - self.v2_groups
if unknown:
joined = ", ".join(sorted(unknown))
raise ValueError(f"hub-core write groups are not enabled: {joined}")
@classmethod
def from_env(cls) -> RuntimeSettings:
@ -41,6 +60,11 @@ class RuntimeSettings:
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"),
api_token=os.getenv("HUB_CORE_API_TOKEN") or os.getenv("CORE_HUB_API_TOKEN"),
v2_groups=_env_set("HUB_CORE_V2_GROUPS"),
v2_write_groups=_env_set("HUB_CORE_V2_WRITE_GROUPS"),
legacy_write_groups=_env_set("CORE_HUB_V2_WRITE_GROUPS"),
legacy_health=_env_bool("HUB_CORE_LEGACY_HEALTH", False),
)
def readiness_checks(self, store_backend: str) -> dict[str, str]:
@ -51,6 +75,9 @@ class RuntimeSettings:
"active_backend": store_backend,
"ephemeral_backend": "allowed" if ephemeral_allowed else "not_allowed",
"contract": "helixforge.hub-extension/0.1.0",
"v2_groups": ",".join(sorted(self.v2_groups)) or "none",
"v2_write_groups": ",".join(sorted(self.v2_write_groups)) or "none",
"legacy_write_groups": ",".join(sorted(self.legacy_write_groups)) or "none",
}
def is_ready(self, store_backend: str) -> bool:

View file

@ -0,0 +1,438 @@
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from uuid import NAMESPACE_URL, uuid4, uuid5
import sqlalchemy as sa
from hub_core.runtime.postgres_store import PostgresPortStore
from hub_core.runtime.tables import (
compat_api_consumers,
compat_api_keys,
compat_hubs,
compat_manifests,
compat_migration_runs,
compat_widgets,
runtime_audit_ledger,
runtime_import_runs,
runtime_interaction_events,
)
SCHEMA_VERSION = "core-hub.migration.v1"
COLLECTIONS = (
"hubs",
"hubCapabilityManifests",
"apiConsumers",
"apiKeys",
"widgets",
"interactionEvents",
"migrationRuns",
)
SECRET_FIELDS = {"fullKey", "rawKey", "apiKey", "secret", "token", "authorization"}
def _now() -> datetime:
return datetime.now(timezone.utc)
def _json(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), default=_json_default)
def _json_default(value: Any) -> str:
if isinstance(value, datetime):
return _timestamp(value)
raise TypeError(f"cannot encode {type(value).__name__}")
def _timestamp(value: datetime) -> str:
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def _datetime(value: Any, default: datetime | None = None) -> datetime:
if isinstance(value, datetime):
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
if isinstance(value, str) and value:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
return default or _now()
def _records(bundle: dict[str, Any], collection: str) -> list[dict[str, Any]]:
records = bundle.get("records", {})
values = records.get(collection, []) if isinstance(records, dict) else []
return [dict(value) for value in values] if isinstance(values, list) else []
def _contains_secret(value: Any) -> bool:
if isinstance(value, dict):
return any(key in SECRET_FIELDS or _contains_secret(child) for key, child in value.items())
if isinstance(value, list):
return any(_contains_secret(child) for child in value)
return False
def _canonical_record(record: dict[str, Any]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in record.items():
if key in {"createdAt", "updatedAt", "occurredAt", "recordedAt"} and value:
result[key] = _timestamp(_datetime(value))
elif isinstance(value, dict):
result[key] = _canonical_record(value)
elif isinstance(value, list):
result[key] = [
_canonical_record(item) if isinstance(item, dict) else item for item in value
]
else:
result[key] = value
return result
def _identity(record: dict[str, Any]) -> str:
return str(record.get("id") or record.get("slug") or record.get("keyHash") or "")
def content_hashes(bundle: dict[str, Any]) -> dict[str, str]:
hashes: dict[str, str] = {}
for collection in COLLECTIONS:
values = [_canonical_record(value) for value in _records(bundle, collection)]
values.sort(key=_identity)
hashes[collection] = hashlib.sha256(_json(values).encode()).hexdigest()
return hashes
def identity_sets(bundle: dict[str, Any]) -> dict[str, list[str]]:
return {
collection: sorted(_identity(value) for value in _records(bundle, collection))
for collection in COLLECTIONS
}
def bundle_digest(bundle: dict[str, Any]) -> str:
value = {key: child for key, child in bundle.items() if key != "bundleSha256"}
return hashlib.sha256(_json(value).encode()).hexdigest()
def validate_bundle(bundle: dict[str, Any]) -> dict[str, Any]:
errors: list[str] = []
warnings: list[str] = []
unknown = set(bundle) - {
"schemaVersion",
"source",
"sourceRevision",
"exportedAt",
"highWaterMark",
"bundleSha256",
"records",
}
if unknown:
errors.append(f"unknown bundle fields: {', '.join(sorted(unknown))}")
if bundle.get("schemaVersion") != SCHEMA_VERSION:
errors.append(f"schemaVersion must be {SCHEMA_VERSION}")
if not isinstance(bundle.get("records"), dict):
errors.append("records must be an object")
else:
unknown_collections = set(bundle["records"]) - set(COLLECTIONS)
if unknown_collections:
errors.append(f"unknown collections: {', '.join(sorted(unknown_collections))}")
claimed = bundle.get("bundleSha256")
calculated = bundle_digest(bundle)
if claimed and claimed != calculated:
errors.append("bundleSha256 does not match canonical bundle content")
required = {
"hubs": ("slug", "name"),
"hubCapabilityManifests": ("id",),
"apiConsumers": ("name",),
"apiKeys": ("id", "keyPrefix", "keyHash"),
"widgets": ("id", "name"),
"interactionEvents": ("id", "widgetId", "eventType"),
"migrationRuns": ("id", "source", "schemaVersion", "bundleSha256"),
}
for collection in COLLECTIONS:
seen: set[str] = set()
for index, record in enumerate(_records(bundle, collection)):
path = f"records.{collection}[{index}]"
for field in required[collection]:
if record.get(field) in (None, ""):
errors.append(f"{path}.{field} is required")
identity = _identity(record)
if identity and identity in seen:
errors.append(f"duplicate {collection} identity: {identity}")
seen.add(identity)
if _contains_secret(record):
errors.append(f"{path} contains secret-shaped material")
for record in _records(bundle, "hubCapabilityManifests"):
if not record.get("hubId") and not record.get("hubSlug"):
errors.append("manifest must include hubId or hubSlug")
for record in _records(bundle, "apiConsumers"):
if not record.get("id") and not record.get("slug"):
errors.append("API consumer must include id or slug")
for record in _records(bundle, "apiKeys"):
if not record.get("apiConsumerId") and not record.get("apiConsumerSlug"):
errors.append("API key must include apiConsumerId or apiConsumerSlug")
for record in _records(bundle, "widgets"):
if not record.get("hubId") and not record.get("hubSlug"):
errors.append("widget must include hubId or hubSlug")
return {
"ok": not errors,
"schemaVersion": bundle.get("schemaVersion"),
"source": bundle.get("source", "unknown"),
"bundleSha256": calculated,
"highWaterMark": bundle.get("highWaterMark"),
"counts": {name: {"input": len(_records(bundle, name))} for name in COLLECTIONS},
"identitySets": identity_sets(bundle),
"contentHashes": content_hashes(bundle),
"errors": errors,
"warnings": warnings,
}
async def import_bundle(
store: PostgresPortStore, bundle: dict[str, Any], *, dry_run: bool = False
) -> dict[str, Any]:
report = validate_bundle(bundle)
report["dryRun"] = dry_run
report["idempotent"] = False
report["counts"] = {
name: {"input": len(_records(bundle, name)), "created": 0, "updated": 0, "skipped": 0}
for name in COLLECTIONS
}
if not report["ok"]:
return report
async with store.sessions() as session:
prior = (
await session.execute(
sa.select(runtime_import_runs).where(
runtime_import_runs.c.bundle_sha256 == report["bundleSha256"]
)
)
).mappings().first()
if prior and not dry_run:
report["idempotent"] = True
report["migrationRunId"] = prior["id"]
for name in COLLECTIONS:
report["counts"][name]["skipped"] = report["counts"][name]["input"]
return report
async with store.sessions.begin() as session:
hubs = await _maps(session, compat_hubs, "id", "slug")
await _import_hubs(session, bundle, report, hubs, dry_run)
hubs = await _maps(session, compat_hubs, "id", "slug") if not dry_run else hubs
await _import_manifests(session, bundle, report, hubs, dry_run)
consumers = await _maps(session, compat_api_consumers, "id", "slug")
await _import_consumers(session, bundle, report, consumers, dry_run)
consumers = (
await _maps(session, compat_api_consumers, "id", "slug") if not dry_run else consumers
)
await _import_keys(session, bundle, report, consumers, dry_run)
await _import_widgets(session, bundle, report, hubs, dry_run)
await _import_events(session, bundle, report, dry_run)
await _import_source_runs(session, bundle, report, dry_run)
if dry_run:
await session.rollback()
return report
run_id = str(uuid4())
await session.execute(
runtime_import_runs.insert().values(
id=run_id,
source=report["source"],
schema_version=SCHEMA_VERSION,
bundle_sha256=report["bundleSha256"],
high_water_mark=report["highWaterMark"],
counts=report["counts"],
content_hashes=report["contentHashes"],
status="imported",
created_at=_now(),
)
)
await session.execute(
runtime_audit_ledger.insert().values(
id=str(uuid4()),
action="migration.imported",
subject_type="migration_bundle",
subject_id=report["bundleSha256"],
correlation_id=None,
payload_hash=hashlib.sha256(_json(report["contentHashes"]).encode()).hexdigest(),
detail={"source": report["source"], "counts": report["counts"]},
recorded_at=_now(),
)
)
report["migrationRunId"] = run_id
return report
async def _maps(session: Any, table: sa.Table, *keys: str) -> dict[str, dict[str, Any]]:
rows = (await session.execute(sa.select(table))).mappings().all()
return {str(row[key]): dict(row) for row in rows for key in keys if row[key]}
def _mark(report: dict[str, Any], name: str, existing: Any) -> None:
report["counts"][name]["updated" if existing else "created"] += 1
async def _upsert(session: Any, table: sa.Table, row: dict[str, Any], existing: Any) -> None:
if existing:
await session.execute(table.update().where(table.c.id == existing["id"]).values(**row))
else:
await session.execute(table.insert().values(**row))
def _parent_id(record: dict[str, Any], rows: dict[str, dict[str, Any]], kind: str) -> str:
parent = record.get(f"{kind}Id") or record.get(f"{kind}Slug")
if parent in rows:
return str(rows[str(parent)]["id"])
raise ValueError(f"unresolved {kind} reference: {parent}")
async def _import_hubs(session: Any, bundle: dict[str, Any], report: dict, rows: dict, dry: bool) -> None:
for record in _records(bundle, "hubs"):
existing = rows.get(str(record.get("id"))) or rows.get(str(record.get("slug")))
_mark(report, "hubs", existing)
if dry:
continue
now = _now()
row = {
"id": existing["id"] if existing else str(record.get("id") or uuid4()),
"slug": record["slug"], "name": record["name"], "domain": record.get("domain"),
"hub_kind": record.get("hubKind"), "hub_family": record.get("hubFamily"),
"vsm_function": record.get("vsmFunction"), "vsm_system": record.get("vsmSystem"),
"status": record.get("status", "active"), "description": record.get("description"),
"body": record, "created_at": _datetime(record.get("createdAt"), now),
"updated_at": _datetime(record.get("updatedAt"), now),
}
await _upsert(session, compat_hubs, row, existing)
async def _import_manifests(session: Any, bundle: dict, report: dict, hubs: dict, dry: bool) -> None:
current = await _maps(session, compat_manifests, "id")
for record in _records(bundle, "hubCapabilityManifests"):
existing = current.get(str(record["id"])); _mark(report, "hubCapabilityManifests", existing)
if dry: continue
now = _now(); hub_id = _parent_id(record, hubs, "hub")
row = {"id": record["id"], "hub_id": hub_id, "hub_slug": hubs[hub_id]["slug"],
"manifest_version": record.get("manifestVersion", "0.1.0"),
"status": record.get("status", "draft"), "body": record,
"created_at": _datetime(record.get("createdAt"), now),
"updated_at": _datetime(record.get("updatedAt"), now)}
await _upsert(session, compat_manifests, row, existing)
async def _import_consumers(session: Any, bundle: dict, report: dict, rows: dict, dry: bool) -> None:
for record in _records(bundle, "apiConsumers"):
existing = rows.get(str(record.get("id"))) or rows.get(str(record.get("slug")))
_mark(report, "apiConsumers", existing)
if dry: continue
row = {"id": existing["id"] if existing else str(record.get("id") or uuid4()),
"slug": record.get("slug"), "name": record["name"],
"description": record.get("description"),
"hub_capability_manifest_id": record.get("hubCapabilityManifestId"),
"rate_limit_per_minute": record.get("rateLimitPerMinute"),
"quota_per_day": record.get("quotaPerDay"), "key_prefix": record.get("keyPrefix"),
"status": record.get("status", "active"), "body": record,
"created_at": _datetime(record.get("createdAt"))}
await _upsert(session, compat_api_consumers, row, existing)
async def _import_keys(session: Any, bundle: dict, report: dict, consumers: dict, dry: bool) -> None:
current = await _maps(session, compat_api_keys, "id", "key_hash")
for record in _records(bundle, "apiKeys"):
existing = current.get(str(record["id"])) or current.get(str(record["keyHash"]))
_mark(report, "apiKeys", existing)
if dry: continue
consumer_id = _parent_id(record, consumers, "apiConsumer")
row = {"id": record["id"], "api_consumer_id": consumer_id,
"key_prefix": record["keyPrefix"], "key_hash": record["keyHash"],
"scopes": record.get("scopes"), "status": record.get("status", "active"),
"created_at": _datetime(record.get("createdAt"))}
await _upsert(session, compat_api_keys, row, existing)
async def _import_widgets(session: Any, bundle: dict, report: dict, hubs: dict, dry: bool) -> None:
current = await _maps(session, compat_widgets, "id")
for record in _records(bundle, "widgets"):
existing = current.get(str(record["id"])); _mark(report, "widgets", existing)
if dry: continue
row = {"id": record["id"], "hub_id": _parent_id(record, hubs, "hub"),
"name": record["name"], "widget_type": record.get("widgetType"),
"capability_ref": record.get("capabilityRef"),
"view_context": record.get("viewContext"), "policy_scope": record.get("policyScope"),
"status": record.get("status", "active"), "body": record,
"created_at": _datetime(record.get("createdAt"))}
await _upsert(session, compat_widgets, row, existing)
async def _import_events(session: Any, bundle: dict, report: dict, dry: bool) -> None:
current = await _maps(session, runtime_interaction_events, "id")
for record in _records(bundle, "interactionEvents"):
existing = current.get(str(record["id"])); _mark(report, "interactionEvents", existing)
if dry: continue
created = _datetime(record.get("createdAt") or record.get("occurredAt"))
row = {"id": record["id"], "schema_version": "0.1.0",
"correlation_id": str(record.get("correlationId") or uuid5(NAMESPACE_URL, record["id"])),
"event_type": record["eventType"], "occurred_at": created,
"subject_refs": {"widget": record["widgetId"]}, "payload": {"legacy": record},
"recorded_at": _datetime(record.get("recordedAt"), created)}
await _upsert(session, runtime_interaction_events, row, existing)
async def _import_source_runs(session: Any, bundle: dict, report: dict, dry: bool) -> None:
current = await _maps(session, compat_migration_runs, "id")
for record in _records(bundle, "migrationRuns"):
existing = current.get(str(record["id"])); _mark(report, "migrationRuns", existing)
if dry: continue
row = {"id": record["id"], "source": record["source"],
"schema_version": record["schemaVersion"], "bundle_sha256": record["bundleSha256"],
"dry_run": bool(record.get("dryRun", False)), "status": record.get("status", "imported"),
"counts": record.get("counts", {}), "diagnostics": record.get("diagnostics", {}),
"created_at": _datetime(record.get("createdAt"))}
await _upsert(session, compat_migration_runs, row, existing)
async def export_bundle(store: PostgresPortStore, *, source_revision: str | None = None) -> dict[str, Any]:
async with store.sessions() as session:
hubs = (await session.execute(sa.select(compat_hubs).order_by(compat_hubs.c.id))).mappings().all()
manifests = (await session.execute(sa.select(compat_manifests).order_by(compat_manifests.c.id))).mappings().all()
consumers = (await session.execute(sa.select(compat_api_consumers).order_by(compat_api_consumers.c.id))).mappings().all()
keys = (await session.execute(sa.select(compat_api_keys).order_by(compat_api_keys.c.id))).mappings().all()
widgets = (await session.execute(sa.select(compat_widgets).order_by(compat_widgets.c.id))).mappings().all()
events = (await session.execute(sa.select(runtime_interaction_events).order_by(runtime_interaction_events.c.id))).mappings().all()
runs = (await session.execute(sa.select(compat_migration_runs).order_by(compat_migration_runs.c.id))).mappings().all()
records = {
"hubs": [_body(row, created=True, updated=True) for row in hubs],
"hubCapabilityManifests": [_body(row, created=True, updated=True) for row in manifests],
"apiConsumers": [_body(row, created=True) for row in consumers],
"apiKeys": [{"id": row["id"], "apiConsumerId": row["api_consumer_id"], "keyPrefix": row["key_prefix"], "keyHash": row["key_hash"], "scopes": row["scopes"], "status": row["status"], "createdAt": _timestamp(row["created_at"])} for row in keys],
"widgets": [_body(row, created=True) for row in widgets],
"interactionEvents": [{**dict((row["payload"] or {}).get("legacy") or {}), "id": row["id"], "widgetId": (row["subject_refs"] or {}).get("widget"), "eventType": row["event_type"], "occurredAt": _timestamp(row["occurred_at"]), "recordedAt": _timestamp(row["recorded_at"]), "correlationId": row["correlation_id"]} for row in events],
"migrationRuns": [{"id": row["id"], "source": row["source"], "schemaVersion": row["schema_version"], "bundleSha256": row["bundle_sha256"], "dryRun": row["dry_run"], "status": row["status"], "counts": row["counts"], "diagnostics": row["diagnostics"], "createdAt": _timestamp(row["created_at"])} for row in runs],
}
high_water = max((_timestamp(row["recorded_at"]) for row in events), default=None)
bundle: dict[str, Any] = {"schemaVersion": SCHEMA_VERSION, "source": "hub-core", "exportedAt": _timestamp(_now()), "highWaterMark": high_water, "records": records}
if source_revision:
bundle["sourceRevision"] = source_revision
bundle["bundleSha256"] = bundle_digest(bundle)
return bundle
def _body(row: Any, *, created: bool = False, updated: bool = False) -> dict[str, Any]:
value = dict(row["body"] or {})
if created:
value["createdAt"] = _timestamp(row["created_at"])
if updated:
value["updatedAt"] = _timestamp(row["updated_at"])
return value
def load_bundle(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text())
if not isinstance(value, dict):
raise ValueError("migration bundle must be a JSON object")
return value

View file

@ -0,0 +1,321 @@
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping
from copy import deepcopy
from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
from hub_core.contracts import CONTRACT_VERSION
from hub_core.runtime.models import (
EventCommand,
MessageCommand,
PortAccepted,
PortCollection,
PortRecord,
Provenance,
RegistryRegistration,
)
from hub_core.runtime.tables import (
runtime_audit_ledger,
runtime_interaction_events,
runtime_messages,
runtime_progress_events,
runtime_registrations,
)
class PostgresPortStore:
"""Durable named-port store for PostgreSQL-compatible async engines."""
backend_name = "postgresql"
def __init__(self, engine: AsyncEngine) -> None:
self.engine = engine
self.sessions = async_sessionmaker(engine, expire_on_commit=False)
@classmethod
def from_url(cls, database_url: str) -> PostgresPortStore:
return cls(create_async_engine(database_url, pool_pre_ping=True))
async def readiness_checks(self) -> dict[str, str]:
try:
async with self.engine.connect() as connection:
await connection.execute(sa.text("SELECT 1"))
except Exception:
return {"database": "unavailable"}
return {"database": "ok"}
async def aclose(self) -> None:
await self.engine.dispose()
async def register_extension(
self,
registration: RegistryRegistration,
correlation_id: UUID,
) -> PortAccepted:
hub_slug = str(registration.descriptor["hub_slug"])
value = registration.model_dump(mode="json")
now = _now()
async with self.sessions.begin() as session:
current = (
await session.execute(
sa.select(runtime_registrations.c.package).where(
runtime_registrations.c.hub_slug == hub_slug
)
)
).scalar_one_or_none()
duplicate = current == value
if current is None:
await session.execute(
runtime_registrations.insert().values(
hub_slug=hub_slug,
package=value,
created_at=now,
updated_at=now,
)
)
elif not duplicate:
await session.execute(
runtime_registrations.update()
.where(runtime_registrations.c.hub_slug == hub_slug)
.values(package=value, updated_at=now)
)
await self._audit(
session,
action="registry.duplicate" if duplicate else "registry.accepted",
subject_type="registration",
subject_id=hub_slug,
correlation_id=correlation_id,
value=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()
created_at = _now()
value = {
"id": str(message_id),
"created_at": created_at.isoformat(),
**command.model_dump(mode="json"),
}
stored = {
**value,
"created_at": created_at,
}
async with self.sessions.begin() as session:
await session.execute(runtime_messages.insert().values(**stored))
await self._audit(
session,
action="message.accepted",
subject_type="message",
subject_id=str(message_id),
correlation_id=command.correlation_id,
value=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:
statement = sa.select(runtime_messages).order_by(
runtime_messages.c.created_at, runtime_messages.c.id
)
if conversation_id is not None:
statement = statement.where(
runtime_messages.c.conversation_id == str(conversation_id)
)
async with self.sessions() as session:
rows = (await session.execute(statement)).mappings().all()
values = [
_message_value(row)
for row in rows
if address in list(row["to_addresses"] or [])
]
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, runtime_progress_events, "progress")
async def append_interaction(self, command: EventCommand) -> PortAccepted:
return await self._append_event(command, runtime_interaction_events, "interaction")
async def query_projection(self, projection_id: str) -> PortRecord | None:
async with self.sessions() as session:
if projection_id == "hub_registry":
rows = (
await session.execute(
sa.select(runtime_registrations.c.package).order_by(
runtime_registrations.c.hub_slug
)
)
).scalars()
items = [deepcopy(value) for value in rows]
elif projection_id == "messages":
rows = (
await session.execute(
sa.select(runtime_messages).order_by(
runtime_messages.c.created_at, runtime_messages.c.id
)
)
).mappings()
items = [_message_value(row) for row in rows]
elif projection_id in {"progress_events", "interaction_events"}:
table = (
runtime_progress_events
if projection_id == "progress_events"
else runtime_interaction_events
)
family = "progress" if projection_id == "progress_events" else "interaction"
rows = (
await session.execute(
sa.select(table).order_by(table.c.recorded_at, table.c.id)
)
).mappings()
items = [_event_value(row, family) for row in rows]
else:
return None
return self._record(
projection_id,
{
"projection_id": projection_id,
"items": items,
"rebuild_from": _rebuild_sources(projection_id),
},
)
async def _append_event(
self,
command: EventCommand,
table: sa.Table,
family: str,
) -> PortAccepted:
event_id = uuid4()
recorded_at = _now()
value = {
"id": str(event_id),
"family": family,
"recorded_at": recorded_at.isoformat(),
**command.model_dump(mode="json"),
}
stored = {
"id": str(event_id),
"schema_version": command.schema_version,
"correlation_id": str(command.correlation_id),
"event_type": command.event_type,
"occurred_at": command.occurred_at,
"subject_refs": command.subject_refs,
"payload": command.payload,
"recorded_at": recorded_at,
}
async with self.sessions.begin() as session:
await session.execute(table.insert().values(**stored))
await self._audit(
session,
action=f"event.{family}.accepted",
subject_type=f"{family}_event",
subject_id=str(event_id),
correlation_id=command.correlation_id,
value=value,
)
return PortAccepted(
id=str(event_id), status="accepted", correlation_id=command.correlation_id
)
async def _audit(
self,
session: Any,
*,
action: str,
subject_type: str,
subject_id: str,
correlation_id: UUID | None,
value: Mapping[str, Any],
) -> None:
await session.execute(
runtime_audit_ledger.insert().values(
id=str(uuid4()),
action=action,
subject_type=subject_type,
subject_id=subject_id,
correlation_id=str(correlation_id) if correlation_id else None,
payload_hash=_hash(value),
detail={"schema_version": CONTRACT_VERSION},
recorded_at=_now(),
)
)
def _record(self, kind: str, value: dict[str, Any]) -> PortRecord:
record_id = str(value.get("id") or kind)
return PortRecord(
id=record_id,
data=deepcopy(value),
provenance=Provenance(
source_system="hub-core-postgresql",
source_ref=f"postgresql://hub_runtime/{kind}/{record_id}",
schema_version=CONTRACT_VERSION,
content_hash=_hash(value),
indexed_at=_now(),
),
)
def _message_value(row: Mapping[str, Any]) -> dict[str, Any]:
return {
"id": str(row["id"]),
"created_at": _iso(row["created_at"]),
"schema_version": row["schema_version"],
"correlation_id": str(row["correlation_id"]),
"conversation_id": str(row["conversation_id"]) if row["conversation_id"] else None,
"from_address": row["from_address"],
"to_addresses": list(row["to_addresses"] or []),
"body": row["body"],
"subject_refs": dict(row["subject_refs"] or {}),
}
def _event_value(row: Mapping[str, Any], family: str) -> dict[str, Any]:
return {
"id": str(row["id"]),
"family": family,
"recorded_at": _iso(row["recorded_at"]),
"schema_version": row["schema_version"],
"correlation_id": str(row["correlation_id"]),
"event_type": row["event_type"],
"occurred_at": _iso(row["occurred_at"]),
"subject_refs": dict(row["subject_refs"] or {}),
"payload": dict(row["payload"] or {}),
}
def _hash(value: Mapping[str, Any]) -> str:
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode()
return hashlib.sha256(encoded).hexdigest()
def _iso(value: Any) -> str:
return value.isoformat() if isinstance(value, datetime) else str(value)
def _now() -> datetime:
return datetime.now(timezone.utc)
def _rebuild_sources(projection_id: str) -> list[str]:
return {
"hub_registry": ["runtime_registrations"],
"messages": ["runtime_messages"],
"progress_events": ["runtime_progress_events"],
"interaction_events": ["runtime_interaction_events"],
}[projection_id]

View file

@ -26,6 +26,8 @@ class PortStore(Protocol):
backend_name: str
async def readiness_checks(self) -> dict[str, str]: ...
async def register_extension(
self,
registration: RegistryRegistration,
@ -59,6 +61,9 @@ class InMemoryPortStore:
self._progress_events: list[dict[str, Any]] = []
self._interaction_events: list[dict[str, Any]] = []
async def readiness_checks(self) -> dict[str, str]:
return {"database": "not_applicable"}
async def register_extension(
self,
registration: RegistryRegistration,

169
hub_core/runtime/tables.py Normal file
View file

@ -0,0 +1,169 @@
from __future__ import annotations
import sqlalchemy as sa
runtime_metadata = sa.MetaData()
runtime_registrations = sa.Table(
"runtime_registrations",
runtime_metadata,
sa.Column("hub_slug", sa.String(120), primary_key=True),
sa.Column("package", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
runtime_messages = sa.Table(
"runtime_messages",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("schema_version", sa.String(40), nullable=False),
sa.Column("correlation_id", sa.String(36), nullable=False, index=True),
sa.Column("conversation_id", sa.String(36), nullable=True, index=True),
sa.Column("from_address", sa.String(240), nullable=False),
sa.Column("to_addresses", sa.JSON(), nullable=False),
sa.Column("body", sa.Text(), nullable=False),
sa.Column("subject_refs", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, index=True),
)
def _event_table(name: str) -> sa.Table:
return sa.Table(
name,
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("schema_version", sa.String(40), nullable=False),
sa.Column("correlation_id", sa.String(36), nullable=False, index=True),
sa.Column("event_type", sa.String(160), nullable=False, index=True),
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("subject_refs", sa.JSON(), nullable=False),
sa.Column("payload", sa.JSON(), nullable=False),
sa.Column("recorded_at", sa.DateTime(timezone=True), nullable=False, index=True),
)
runtime_progress_events = _event_table("runtime_progress_events")
runtime_interaction_events = _event_table("runtime_interaction_events")
runtime_audit_ledger = sa.Table(
"runtime_audit_ledger",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("action", sa.String(120), nullable=False, index=True),
sa.Column("subject_type", sa.String(80), nullable=False),
sa.Column("subject_id", sa.String(240), nullable=False, index=True),
sa.Column("correlation_id", sa.String(36), nullable=True, index=True),
sa.Column("payload_hash", sa.String(64), nullable=False),
sa.Column("detail", sa.JSON(), nullable=False),
sa.Column("recorded_at", sa.DateTime(timezone=True), nullable=False, index=True),
)
compat_hubs = sa.Table(
"compat_hubs",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("slug", sa.String(120), nullable=False, unique=True, index=True),
sa.Column("name", sa.String(240), nullable=False),
sa.Column("domain", sa.String(240), nullable=True),
sa.Column("hub_kind", sa.String(80), nullable=True),
sa.Column("hub_family", sa.String(80), nullable=True),
sa.Column("vsm_function", sa.String(80), nullable=True),
sa.Column("vsm_system", sa.String(80), nullable=True),
sa.Column("status", sa.String(40), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("body", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
compat_manifests = sa.Table(
"compat_manifests",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("hub_id", sa.String(36), sa.ForeignKey("compat_hubs.id"), nullable=True, index=True),
sa.Column("hub_slug", sa.String(120), nullable=True, index=True),
sa.Column("manifest_version", sa.String(40), nullable=False),
sa.Column("status", sa.String(40), nullable=False),
sa.Column("body", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
compat_api_consumers = sa.Table(
"compat_api_consumers",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("slug", sa.String(120), nullable=True, unique=True, index=True),
sa.Column("name", sa.String(240), nullable=False, index=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("hub_capability_manifest_id", sa.String(36), nullable=True, index=True),
sa.Column("rate_limit_per_minute", sa.Integer(), nullable=True),
sa.Column("quota_per_day", sa.Integer(), nullable=True),
sa.Column("key_prefix", sa.String(32), nullable=True),
sa.Column("status", sa.String(40), nullable=False),
sa.Column("body", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)
compat_api_keys = sa.Table(
"compat_api_keys",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column(
"api_consumer_id",
sa.String(36),
sa.ForeignKey("compat_api_consumers.id"),
nullable=False,
index=True,
),
sa.Column("key_prefix", sa.String(32), nullable=False, index=True),
sa.Column("key_hash", sa.String(64), nullable=False, unique=True),
sa.Column("scopes", sa.Text(), nullable=True),
sa.Column("status", sa.String(40), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)
compat_widgets = sa.Table(
"compat_widgets",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("hub_id", sa.String(36), sa.ForeignKey("compat_hubs.id"), nullable=False, index=True),
sa.Column("name", sa.String(240), nullable=False),
sa.Column("widget_type", sa.String(120), nullable=True, index=True),
sa.Column("capability_ref", sa.String(240), nullable=True, index=True),
sa.Column("view_context", sa.String(500), nullable=True),
sa.Column("policy_scope", sa.String(120), nullable=True),
sa.Column("status", sa.String(40), nullable=False),
sa.Column("body", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)
compat_migration_runs = sa.Table(
"compat_migration_runs",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("source", sa.String(120), nullable=False, index=True),
sa.Column("schema_version", sa.String(80), nullable=False),
sa.Column("bundle_sha256", sa.String(64), nullable=False, index=True),
sa.Column("dry_run", sa.Boolean(), nullable=False),
sa.Column("status", sa.String(40), nullable=False),
sa.Column("counts", sa.JSON(), nullable=False),
sa.Column("diagnostics", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)
runtime_import_runs = sa.Table(
"runtime_import_runs",
runtime_metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("source", sa.String(120), nullable=False, index=True),
sa.Column("schema_version", sa.String(80), nullable=False),
sa.Column("bundle_sha256", sa.String(64), nullable=False, unique=True, index=True),
sa.Column("high_water_mark", sa.String(200), nullable=True),
sa.Column("counts", sa.JSON(), nullable=False),
sa.Column("content_hashes", sa.JSON(), nullable=False),
sa.Column("status", sa.String(40), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)