Implements the four residual conformance checks left open by the T04
minimal vertical:
- C2: GET /ports/registry/registrations/{hub_slug} resolves missing (404),
ambiguous (shared reuse_surface_id across hub_slugs), and stale
(deprecated/retired descriptor) registrations; a new .../audit route
exposes queryable registration history from the existing in-memory
history and the PostgreSQL runtime_audit_ledger.
- C7: harness proof that disabled compatibility groups deny access
(404) with no fixture credentials involved, matching the existing
fail-closed compat router behavior.
- C9: harness proof plus a dedicated test that /readyz degrades only on
an unavailable configured dependency while unrelated disabled
projections stay non-blocking.
- C10: ContractValidator now negotiates contract_version_min/max against
the runtime's contract version and rejects incompatible or inverted
ranges with an explicit 422 instead of silently accepting them.
HUB-WP-0009 is now finished. HUB-WP-0006 is marked blocked: its only open
task (T06) has no remaining hub-core code path and waits on an external
Forgejo identity/production deployment gate. HUB-WP-0011 is marked
blocked: T02/T03 already waited on external credential/deployment
review, and T01 needs a source/destination ownership and retention
decision against live message data before it can be implemented safely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 310936@bnt-lap001
Assistant-Session: 00cd9abe-09a0-416b-88e0-f907b9101629
157 lines
5.3 KiB
Python
157 lines
5.3 KiB
Python
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(
|
|
"/registry/registrations/{hub_slug}",
|
|
response_model=PortRecord,
|
|
tags=["registry"],
|
|
openapi_extra={"x-port-id": "port.registry", "x-direction": "out"},
|
|
)
|
|
async def resolve_registration(
|
|
hub_slug: str,
|
|
store: PortStore = Depends(get_port_store),
|
|
) -> PortRecord:
|
|
resolved = await store.resolve_registration(hub_slug)
|
|
if resolved is None:
|
|
raise HTTPException(status_code=404, detail=f"Registration '{hub_slug}' not found")
|
|
return resolved
|
|
|
|
@router.get(
|
|
"/registry/registrations/{hub_slug}/audit",
|
|
response_model=PortCollection,
|
|
tags=["registry"],
|
|
openapi_extra={"x-port-id": "port.registry", "x-direction": "out"},
|
|
)
|
|
async def registration_audit(
|
|
hub_slug: str,
|
|
store: PortStore = Depends(get_port_store),
|
|
) -> PortCollection:
|
|
audit = await store.list_registration_audit(hub_slug)
|
|
if not audit.items:
|
|
raise HTTPException(status_code=404, detail=f"No audit history for '{hub_slug}'")
|
|
return audit
|
|
|
|
@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
|