diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5e848dd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.pytest_cache +.ruff_cache +.venv +__pycache__ +dist +docs +tests +workplans diff --git a/AGENTS.md b/AGENTS.md index db19410..8c5bf5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ **Domain:** infotech **Repo slug:** hub-core -**Topic ID:** `1f2e4d10-c967-4803-ae6c-7f4b4e806409` +**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a` **Workplan prefix:** `HUB-WP-` --- @@ -33,8 +33,8 @@ statehub outbox status/replay after connectivity returns. # Offline brief — works without hub connection cat .custodian-brief.md -# Active workplans for this domain -curl -s "http://127.0.0.1:8000/workplans/?topic_id=1f2e4d10-c967-4803-ae6c-7f4b4e806409&status=active" \ +# Active workplans for this topic +curl -s "http://127.0.0.1:8000/workplans/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \ | python3 -m json.tool # Check inbox diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..fdc3426 --- /dev/null +++ b/Containerfile @@ -0,0 +1,47 @@ +FROM python:3.12-slim AS builder + +COPY --from=ghcr.io/astral-sh/uv:0.5.9 /uv /usr/local/bin/uv + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PROJECT_ENVIRONMENT=/opt/hub-core + +WORKDIR /src + +COPY pyproject.toml uv.lock README.md LICENSE ./ +COPY hub_core/ ./hub_core/ + +RUN uv sync --frozen --no-dev --extra runtime --no-editable + +FROM python:3.12-slim AS runtime + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PATH="/opt/hub-core/bin:${PATH}" \ + HUB_CORE_ENV=production \ + HUB_CORE_BACKEND=memory \ + HUB_CORE_ALLOW_EPHEMERAL=0 \ + HUB_CORE_API_HOST=0.0.0.0 \ + HUB_CORE_API_PORT=8010 + +ARG VERSION=0.2.0 +ARG VCS_REF=unknown + +LABEL org.opencontainers.image.title="hub-core" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.source="https://forgejo.coulomb.social/coulomb/hub-core" + +RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin hubcore + +COPY --from=builder /opt/hub-core /opt/hub-core + +USER 10001:10001 +WORKDIR /home/hubcore + +EXPOSE 8010 8011 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8010/healthz', timeout=3).read()" + +CMD ["hub-core", "api", "--host", "0.0.0.0", "--port", "8010"] diff --git a/INTENT.md b/INTENT.md index a4230c1..e74504f 100644 --- a/INTENT.md +++ b/INTENT.md @@ -2,32 +2,35 @@ **Project:** `hub-core` **Domain:** `infotech` -**Status:** Active — library anchor for hub ecosystem (`CUST-WP-0057`) -**Updated:** 2026-07-09 +**Status:** Active — surviving hub framework and runtime (`HUB-WP-0004`) +**Updated:** 2026-08-21 --- ## One-line intent -`hub-core` provides reusable FastAPI, SQLAlchemy, and MCP primitives so multiple -FOS hub services can share a common foundation without importing each other's -domain-specific coordination models. +`hub-core` provides the contracts, reusable primitives, and surviving runtime +for HelixForge hubs without absorbing domain authorities or repository-owned +work records. --- ## Ecosystem position -`hub-core` is the **library layer** in the three-repo hub stack: +`hub-core` is the **surviving framework and runtime** in the consolidating +three-repo hub stack: | Repo | Role | | --- | --- | -| `hub-core` | Shared Python package — this repo | -| `state-hub` | Dev coordination host (primary consumer) | -| `core-hub` | Production framework (`/api/v2`; adopts hub-core utils) | +| `hub-core` | Shared Python package plus target framework/runtime — this repo | +| `state-hub` | Legacy coordination host; capabilities move or retire incrementally | +| `core-hub` | Current `/api/v2` runtime; routes are absorbed, then the repo is archived | Canon: `/home/worsch/the-custodian/docs/hub-ecosystem-architecture.md` -**Naming:** `hub-core` = core *primitives* (library). `core-hub` = core *framework* (service). Do not conflate them. +**Transition naming:** `hub-core` is the surviving product name. `core-hub` +names the current service being absorbed; keep the distinction only while the +compatibility and cutover work remains. --- @@ -52,12 +55,15 @@ package own only its domain-specific tables, workflows, and policies. ## Governing principle -> **Hub-core is a library, not a hub.** +> **Hub-core is the framework and runtime substrate, not a domain hub or a +> repository authority.** -It ships models, schemas, router factories, migration scaffolds, utilities, and -an optional FastMCP base server. A consuming repository (for example -`state-hub`) wires database sessions, auth, host-specific callbacks, and -domain-only routes into those factories. +Today it ships models, schemas, router factories, migration scaffolds, +utilities, and an optional FastMCP base server. Under +`SHR-ARCH-HUB-0001`, those importable surfaces remain while hub-core grows into +the surviving runtime. `ADR-0001` selects a primary OCI image built from this +repository while keeping the wheel importable; that packaging must not move +domain authority into hub-core. `hub-core` should answer: @@ -77,7 +83,8 @@ remain in `state-hub` and other host implementations. ## What it is -`hub-core` is the **shared Python package** for FOS hub services. +`hub-core` is the **shared Python package and target runtime** for HelixForge +hub composition. Current package surface (`hub_core/`): @@ -87,6 +94,7 @@ Current package surface (`hub_core/`): | `schemas/` | Pydantic contracts matching core models plus DoI report shapes | | `routers/` | Factory functions: domains, repos, messages, progress, capabilities, TPSC, policy | | `mcp/` | `HubCoreMCPServer` — generic orientation, messaging, capability, repo, DoI, TPSC/GDPR, risk/alert, progress tools | +| `contracts/` | Packaged `helixforge.hub-extension` schemas, named-port OpenAPI, catalogs, fixtures, compatibility | | `migrations/` | Alembic scaffold and `0001_core_schema` for adopters | | `utils/` | Slugs, pagination, repo path resolution, trailing-slash routing | | `events.py` | Canonical FOS §10 risk and alert event types | @@ -100,8 +108,8 @@ models where extended, and workflow callbacks. | Concern | Owner | |---|---| -| Running production hub deployment | `state-hub`, `ops-hub`, future hubs | -| Topics, workplans, tasks, decisions, SBOM, token accounting | `state-hub` (dev-hub layer) | +| Repository-owned workplans, tasks, decisions, and consistency | `repo-manager` over Git authority | +| Domain-specific business data and APIs | Domain hubs and specialized services | | Custodian canon, constitution, domain charters | `the-custodian` | | Event-triggered maintenance task creation | `activity-core` | | General issue/task lifecycle outside Custodian workplans | `issue-core` | @@ -118,9 +126,9 @@ acceptance flows) stay in the host hub. | Consumer | Relationship | |---|---| -| `state-hub` | First adopter; incremental import of schemas, routers, MCP (CUST-WP-0025 T08+) | -| `ops-hub` | Planned consumer of shared primitives without dev-hub tables | -| Future FOS hubs | Fin-hub and domain hubs mount subsets of hub-core factories | +| `state-hub` | Compatibility source during incremental capability cutover | +| `core-hub` | `/api/v2` route/runtime source to absorb before archival | +| `ops-hub`, `fin-hub`, future hubs | Domain/aspect extensions using versioned hub-core ports and manifests | Extraction boundary and migration status: `/home/worsch/the-custodian/docs/hub-core-extraction-boundary.md` @@ -134,8 +142,9 @@ Extraction boundary and migration status: - a new hub can register domains and repos using hub-core routers without copying SQLAlchemy models - State Hub pytest suite passes with hub-core as an editable dependency - MCP tools for orientation, messages, progress, and TPSC behave consistently across hosts that opt in +- the surviving runtime exposes versioned `helixforge.hub-extension` ports and projections - schema changes to shared primitives are versioned through hub-core migrations, not ad hoc forks -- dev-hub-specific foreign keys never appear in hub-core models (extension via host callbacks or JSON context fields) +- repository work authority and domain-specific business models never migrate into hub-core --- @@ -145,9 +154,11 @@ Extraction boundary and migration status: - Owning PostgreSQL instance provisioning for any environment - Becoming a general application framework unrelated to hub-shaped services - Absorbing reuse-surface capability maturity registry semantics +- Owning Git mutation, work-record reconciliation, authorization decisions, secrets, or scheduling --- ## Working mantra -> Extract once what every hub needs; keep domain drama in the hub that owns it. \ No newline at end of file +> Centralize the hub contract; keep every authority with the component or +> domain that owns it. diff --git a/Makefile b/Makefile index 78caecb..29ba8af 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ -.PHONY: install test ecosystem-regression +.PHONY: install test runtime-run conformance container-build ecosystem-regression UV ?= uv ECOSYSTEM_REGRESSION ?= /home/worsch/the-custodian/scripts/hub-ecosystem-regression.sh +IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/hub-core +IMAGE_TAG ?= dev install: $(UV) sync @@ -9,5 +11,14 @@ install: test: $(UV) run python -m pytest -q +runtime-run: + $(UV) run --extra runtime hub-core api + +conformance: + $(UV) run hub-core conformance --base-url $${HUB_CORE_API_BASE:-http://127.0.0.1:8010} + +container-build: + docker build -f Containerfile -t $(IMAGE_REPOSITORY):$(IMAGE_TAG) . + ecosystem-regression: - bash $(ECOSYSTEM_REGRESSION) \ No newline at end of file + bash $(ECOSYSTEM_REGRESSION) diff --git a/README.md b/README.md index c5b6470..543b1da 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,23 @@ # Hub Core -Reusable FastAPI, SQLAlchemy, and MCP primitives for FOS hubs. +Contracts, reusable Python primitives, and the surviving runtime for +HelixForge hubs. ## Hub stack glossary | Name | Role | | --- | --- | -| **hub-core** | This repo — shared Python library (`hub_core`) | -| **state-hub** | Dev coordination host (workplans, MCP) | -| **core-hub** | Production framework (`/api/v2`, operator console) | +| **hub-core** | This repo — importable package plus target primary runtime image | +| **state-hub** | Legacy coordination host being replaced capability by capability | +| **core-hub** | Current `/api/v2` runtime retained temporarily for absorption rollback | Ecosystem architecture: `/home/worsch/the-custodian/docs/hub-ecosystem-architecture.md` +Runtime packaging is fixed by `docs/adr/ADR-0001-runtime-packaging.md`: the +wheel remains importable, while this repository will also own the primary OCI +image. API, MCP, and migration workloads may run separately from that same +image. Core-hub is not a permanent thin host. + `hub-core` is being extracted from the standalone State Hub repository as part of `CUST-WP-0025`. The initial package slice contains only the generic database models and schemas that can move without importing dev-hub concepts such as @@ -23,6 +29,45 @@ Source boundary notes live in: /home/worsch/the-custodian/docs/hub-core-extraction-boundary.md ``` +## Extension contract + +The wheel includes `helixforge.hub-extension` 0.1.0 under +`hub_core.contracts`. Use `extension_contract_root()` to locate the packaged +descriptor and manifest schemas, event catalog schema and seed, named-port +OpenAPI fragments, ops-hub fixture, and compatibility matrix. + +```python +from hub_core.contracts import CONTRACT_VERSION, extension_contract_root + +contract_root = extension_contract_root() +``` + +## Runtime scaffold + +Install the runtime extra and start the API, MCP, or migration process through +the shared console entrypoint: + +```bash +uv sync --extra runtime +hub-core api +hub-core mcp --api-base http://127.0.0.1:8010 +hub-core migrate head --database-url postgresql+asyncpg://... +``` + +The initial runtime exposes registry, messaging, progress-event, +interaction-event, and projection-query ports. Its included memory backend is +for local/conformance use and fails production readiness unless explicitly +enabled. See `docs/runtime.md`. + +The reusable Tier 2/3 scaffold is documented in `docs/conformance.md` and runs +against any compatible HTTP target with +`hub-core conformance --base-url `. + +The staged Core Hub transition is defined in +`docs/core-hub-absorption-plan.md`; it keeps one writer per capability and +retains Core Hub as rollback until data, consumer, and stabilization gates +close. + ## First Slice - SQLAlchemy base metadata and timestamp helpers. @@ -42,5 +87,9 @@ Source boundary notes live in: - Alembic templates plus an initial core-schema migration for hub adopters. - FastMCP base-server wrapper for generic orientation, messaging, capability, repo, DoI, TPSC/GDPR, risk/alert, and progress tools. +- Packaged `helixforge.hub-extension` 0.1.0 Tier 1 schemas, OpenAPI port + fragments, event catalog, compatibility matrix, and ops-hub fixture. +- Injectable primary runtime scaffold with five named ports, health/readiness, + API/MCP/migration commands, and a locked non-root OCI image. Domain-specific MCP tools follow in each hub package. diff --git a/SCOPE.md b/SCOPE.md index d4424cb..e7b41ea 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -1,22 +1,25 @@ # SCOPE — hub-core -**Updated:** 2026-06-16 +**Updated:** 2026-08-21 --- ## One-liner -Reusable Python package of FastAPI router factories, SQLAlchemy models, Pydantic -schemas, MCP tooling, and migration scaffolds for FOS hub services. +Importable Python package and target runtime for versioned HelixForge hub +contracts, ports, projections, MCP tooling, and compatibility surfaces. --- ## Core idea -`hub-core` is a **library boundary** between shared hub infrastructure and -host-specific hub implementations. Host repositories depend on `hub-core` as an -editable or published package; they run the actual HTTP/MCP service, own -deployment, and add domain tables and workflows on top. +`hub-core` preserves its **library boundary** while growing into the surviving +hub framework and runtime defined by `SHR-ARCH-HUB-0001`. Domain hubs depend on +its versioned contracts and ports while retaining their own data and APIs. +Repository work remains authoritative in Git and is reached through +repo-manager ports. `ADR-0001` selects a primary OCI image from this repository, +with API, MCP, and migration processes sharing the image and contract version; +the Python package remains independently importable. --- @@ -31,6 +34,10 @@ deployment, and add domain tables and workflows on top. - **Package metadata** — `pyproject.toml`, hatchling wheel build - **Capability registry scaffold** — `registry/` per helix_forge federation contract (entries added when reusable behaviors are registered) +- **Hub-extension contracts and conformance** — descriptors, manifests, event + catalogs, port APIs, fixtures, and compatibility matrices +- **Surviving runtime surfaces** — registry, messaging, progress and + interaction events, projections, telemetry, `/api/v2` compatibility, and MCP - **Documentation** — `README.md`, `INTENT.md`, `SCOPE.md`, pointer to extraction boundary in `the-custodian` @@ -38,13 +45,13 @@ deployment, and add domain tables and workflows on top. ## Out of scope -- Long-running hub service, Docker image, or production URL for hub-core itself -- Dev-hub tables: topics, workplans, tasks, decisions, dependencies, SBOM, - token accounting, kaizen agents +- Repository-owned work records, Git write authority, and consistency logic +- Domain-specific business tables and APIs belonging to domain hubs - State Hub dashboard UI, consistency sync scripts, and workplan file authority - Custodian canon content and constitution maintenance - Plaintext secrets, environment-specific connection strings committed to git - Replacing or wrapping non-hub application domains (feature-control, reuse-surface, etc.) +- Authorization decisions, credential custody, and schedule execution --- @@ -58,6 +65,10 @@ After the CUST-WP-0025 first slice (2026-06-06 — 2026-06-07): | Import core models and schemas | `hub_core.models`, `hub_core.schemas` | | Mount generic routers in a host FastAPI app | `hub_core.routers.create_*_router` | | Run generic MCP tools via `HubCoreMCPServer` | `hub_core.mcp` | +| Load versioned hub-extension contracts from the wheel | `hub_core.contracts.extension_contract_root()` | +| Run the minimal named-port HTTP runtime | `hub-core api` / `hub_core.runtime.create_app` | +| Run API, MCP, and packaged migration processes | `hub-core api`, `hub-core mcp`, `hub-core migrate` | +| Build the primary locked non-root OCI image | `make container-build` | | Apply core-schema migration template | `hub_core/migrations/versions/0001_core_schema.py` | | Adopt shared slug/pagination/path utilities | `hub_core.utils` | | Expose risk/alert progress read views | `/progress/risks`, `/progress/alerts` patterns | @@ -74,11 +85,10 @@ python3 -m venv .venv && .venv/bin/pip install -e . ## What is not possible yet - **Published PyPI package** — consumed via editable path or private index only -- **Standalone `hub-core serve`** — no CLI entrypoint; hosts own `uvicorn` +- **Production durable port store** — T04 ships only the fail-closed ephemeral conformance backend - **Complete State Hub decoupling** — dev-hub routes and models still live in `state-hub` - **ops-hub / fin-hub adoption** — planned; not verified in this repo - **Capability registry entries** — scaffold only (`capabilities: []`); no registered reusable behaviors yet -- **Gitea federation publish** — repo not yet on Gitea; blocks T01 in reuse-surface WP-0015 --- @@ -86,13 +96,15 @@ python3 -m venv .venv && .venv/bin/pip install -e . | Item | Value | |---|---| -| Package version | `0.1.0` (`hub_core.__version__`) | +| Package version | `0.2.0` (`hub_core.__version__`) | | Python | `>=3.12` | -| Dependencies | FastAPI, FastMCP, SQLAlchemy, Pydantic, httpx | +| Dependencies | FastAPI, FastMCP, SQLAlchemy, Pydantic, httpx, JSON Schema; runtime extra adds ASGI/PostgreSQL/Alembic | | Tests | pytest under `tests/` | | Registry | Empty capability index; federation scaffold present | | Primary consumer | `state-hub` (editable dependency, router/schema import in progress) | | Extraction workplan | `CUST-WP-0025` (custodian domain) | +| Target architecture | `SHR-ARCH-IA-0001` + `SHR-ARCH-HUB-0001` | +| Runtime workplan | `HUB-WP-0004` | --- @@ -109,6 +121,8 @@ hub-core/ │ ├── schemas/ │ ├── routers/ │ ├── mcp/ +│ ├── runtime/ +│ ├── contracts/ │ ├── migrations/ │ ├── utils/ │ ├── database.py @@ -125,8 +139,9 @@ hub-core/ | Repo | Boundary | |---|---| -| `state-hub` | Primary host — mounts router factories and MCP composition; owns workplans/tasks | -| `core-hub` | Secondary consumer — imports utils/schemas; owns `/api/v2` framework tables locally | +| `state-hub` | Temporary compatibility source; no new permanent authorities | +| `core-hub` | `/api/v2` runtime and route source to absorb, then archive | +| `repo-manager` | Owns repository representation, work index, consistency, and governed Git mutations | | `the-custodian` | Owns ecosystem architecture (`hub-ecosystem-architecture.md`) and extraction boundary | | `reuse-surface` | Federation hub for capability indexes; not a runtime dependency of hub-core | | `ops-hub` | Consumer of core-hub `/api/v2`; operations tables stay local | @@ -135,19 +150,18 @@ hub-core/ ## Workplan convention -Hub-core extraction and package work is tracked under **custodian** workplans -(for example `CUST-WP-0025`). Host adoption milestones are tracked in -`state-hub` workplans (for example `CUST-WP-0048`). - -When hub-core gains repo-local workplans, prefer a stable prefix agreed with -custodian operators (for example `HUBCORE-WP-####`). +Repo-local work uses the `HUB-WP-####` prefix. Cross-repository consolidation +is coordinated by `prj-state-hub-retirement`; owner repositories keep their +own workplan files authoritative. --- ## Getting oriented - Product intent: `INTENT.md` +- Target architecture: `/home/worsch/prj-state-hub-retirement/architecture/hub-extension-architecture_v0.1.md` +- Information model: `/home/worsch/prj-state-hub-retirement/architecture/information-model_v0.1.md` - Extraction boundary: `/home/worsch/the-custodian/docs/hub-core-extraction-boundary.md` - Package entry: `hub_core/__init__.py`, `hub_core/routers/__init__.py` - Consumer example: `/home/worsch/state-hub` (editable `hub-core` dependency) -- Federation registry: `registry/README.md` (reuse-surface contract) \ No newline at end of file +- Federation registry: `registry/README.md` (reuse-surface contract) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index f736157..df8e16a 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -11,7 +11,7 @@ | workplan | HUB-WP-0001 | finished | — | workplans/HUB-WP-0001-statehub-bootstrap.md | | workplan | HUB-WP-0002 | finished | — | workplans/HUB-WP-0002-import-refactor-adapter-seams.md | | workplan | HUB-WP-0003 | finished | — | workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md | -| workplan | HUB-WP-0004 | proposed | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | +| workplan | HUB-WP-0004 | finished | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | | task | HUB-WP-0001-T01 | done | — | workplans/HUB-WP-0001-statehub-bootstrap.md | | task | HUB-WP-0001-T02 | done | — | workplans/HUB-WP-0001-statehub-bootstrap.md | | task | HUB-WP-0001-T03 | done | — | workplans/HUB-WP-0001-statehub-bootstrap.md | @@ -24,9 +24,9 @@ | task | HUB-WP-0003-T04 | done | — | workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md | | task | HUB-WP-0003-T05 | done | — | workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md | | task | HUB-WP-0003-T06 | done | — | workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md | -| task | HUB-WP-0004-T01 | todo | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | -| task | HUB-WP-0004-T02 | todo | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | -| task | HUB-WP-0004-T03 | todo | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | -| task | HUB-WP-0004-T04 | todo | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | -| task | HUB-WP-0004-T05 | todo | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | -| task | HUB-WP-0004-T06 | todo | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | +| task | HUB-WP-0004-T01 | done | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | +| task | HUB-WP-0004-T02 | done | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | +| task | HUB-WP-0004-T03 | done | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | +| task | HUB-WP-0004-T04 | done | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | +| task | HUB-WP-0004-T05 | done | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | +| task | HUB-WP-0004-T06 | done | — | workplans/HUB-WP-0004-runtime-and-extension-contract.md | diff --git a/docs/adr/ADR-0001-runtime-packaging.md b/docs/adr/ADR-0001-runtime-packaging.md new file mode 100644 index 0000000..dc6aef5 --- /dev/null +++ b/docs/adr/ADR-0001-runtime-packaging.md @@ -0,0 +1,103 @@ +# ADR-0001: Ship the primary runtime image from hub-core + +**Status:** Accepted +**Date:** 2026-08-21 +**Workplan:** `HUB-WP-0004-T03` +**Architecture:** `SHR-ARCH-HUB-0001` + +## Context + +Hub-core currently publishes only the importable `hub_core` Python package. +Core-hub supplies the deployable FastAPI application, `/api/v2` compatibility, +PostgreSQL drivers and migrations, OCI build, health endpoints, and operator +surfaces. Its image vendors a hub-core source checkout through a Docker build +context, so a production release spans two repositories and two version +boundaries. + +The State Hub retirement architecture makes hub-core the surviving framework +and runtime and archives core-hub after compatibility cutover. We therefore +need a durable packaging boundary before implementing ports or designing the +dual-run. + +## Decision + +Hub-core will ship the **primary HelixForge hub runtime OCI image** from this +repository. The `hub_core` Python package remains a supported, independently +importable SDK/library surface. Both artifacts share one source revision and +release version. + +The packaging contract is: + +1. The base wheel keeps reusable models, schemas, router factories, contracts, + utilities, and MCP composition importable without starting a service. +2. Runtime-only dependencies such as the ASGI server, PostgreSQL driver, and + migration tooling belong to a declared runtime dependency group/extra and + are installed in the image. +3. Hub-core owns the application factory, health/readiness endpoints, runtime + configuration contract, packaged migrations, image definition, and + compatibility route composition. +4. One image may expose separate commands for API serving, MCP serving, and + migration jobs. Kubernetes may run those as separate processes or workloads + for scaling and privilege isolation; “one primary image” does not require a + monolithic process. +5. Deployment declarations, secret references, rollout policy, and live smoke + evidence remain in the rapp/platform repositories. They consume the + hub-core image rather than defining a permanent Python host. +6. Core-hub remains a temporary rollback runtime during dual-run. Its routes, + contracts, migrations, and fixtures move in governed slices; hub-core must + not retain a steady-state dependency on the `core_hub` package or repository. +7. The runtime keeps domain data, Git/work-record authority, authorization + decisions, credential custody, and schedule execution behind the ports + assigned by `SHR-ARCH-HUB-0001`. + +The initial command names and module layout are implementation details for +`HUB-WP-0004-T04`, but they must provide equivalent surfaces for: + +- starting the HTTP API; +- starting or composing the MCP surface; +- running schema migrations without application auto-create in production; +- probing liveness and readiness. + +## Migration constraints + +- `/api/v2` compatibility moves by route/data slice with core-hub retained as + rollback until consumer smokes and row/provenance comparisons pass. +- Hub-core and core-hub SQLAlchemy metadata remain isolated during dual-run; + model/migration absorption is explicit rather than cross-imported. +- The wheel and image report the same semantic version and source revision so + evidence can identify the exact contract/runtime pair. +- Contract fixtures and OpenAPI snapshots move with the implementing slice and + remain usable without a live deployment. +- Production image coordinates and deployment ownership change only through + the joint `HUB-WP-0004-T06` / `CORE-WP-0010` cutover plan. + +## Alternatives considered + +### Keep a permanent thin host repository + +Rejected. It preserves two release units, prevents core-hub archival, and makes +contract/runtime compatibility a cross-repository pinning problem. A temporary +compatibility host during migration is allowed but has an explicit retirement +gate. + +### Publish a second Python runtime distribution + +Rejected for the initial consolidation. One distribution with a runtime extra +keeps contracts, application composition, and migrations version-aligned. This +can be revisited only if dependency or release evidence shows a material need. + +### Run API and MCP in one mandatory process + +Rejected as a packaging requirement. They share the image and contract version, +but deployments may separate processes to preserve scaling, failure, and access +boundaries. + +## Consequences + +- Hub-core gains operational artifact ownership and must test both wheel and + image entry surfaces. +- Library-only consumers keep an import path without runtime startup. +- Core-hub absorption can converge on one release rather than another + permanent wrapper. +- T04 owns the minimal application/command/image scaffold alongside the core + ports; T06 owns dual-run and production transition details. diff --git a/docs/conformance.md b/docs/conformance.md new file mode 100644 index 0000000..498e21a --- /dev/null +++ b/docs/conformance.md @@ -0,0 +1,44 @@ +# Hub-extension conformance + +`hub_core.conformance` is the reusable Tier 2/3 harness scaffold for contract +version 0.1.0. It drives only public HTTP ports, so a FastAPI `TestClient`, an +`httpx.Client`, or another compatible target can be used without importing the +runtime implementation. + +The harness mutates its target. Run it against a disposable instance or a +dedicated test namespace: + +```bash +hub-core api --host 127.0.0.1 --port 8010 +hub-core conformance --base-url http://127.0.0.1:8010 +hub-core conformance --base-url http://127.0.0.1:8010 --json +``` + +## Implemented profile + +| ID | Tier | Automated evidence | +| --- | --- | --- | +| C1 | 2 | Packaged descriptor, manifest, and catalog validate against Draft 2020-12 schemas | +| C3 | 2 | Runtime health probe returns healthy | +| C4 | 2 | Repeated manifest registration is reported as a duplicate | +| C5 | 2 | Cataloged progress/interaction events are accepted; wrong-family and unknown events are rejected | +| C6 | 2 | Contract and scenario fixtures reject secret-shaped keys, credentialed database URLs, and private keys | +| C8 | 2 | Registry response propagates the request correlation identifier | +| F2 | 3 | Progress and interaction fixture events appear only in their respective projections | +| F3 | 3 | Authority fixtures appear in projections with declared rebuild sources and provenance hashes | + +The projection scenario is shipped in the wheel as +`fixtures/projection-rebuild.json`. Correlation and time fields are generated +per run, allowing the harness to identify its own evidence without relying on +global row counts. + +## Deliberately open checks + +C2 registry resolution, C7 raw-port configuration policy, C9 dependency-aware +readiness, C10 version negotiation, F1 registry audit history, F4 `/api/v2` +consumer smokes, F5 MCP projection binding, F6 policy fail-closed behavior, F7 +telemetry rejection, and F8 migration metadata isolation require ports or +absorption slices that are not part of the T04 minimal vertical. Tenant +isolation also remains open because the 0.1 runtime has no tenant identity or +authorization context yet. These gaps must not be interpreted as passing; the +harness reports only the implemented profile above. diff --git a/docs/core-hub-absorption-plan.md b/docs/core-hub-absorption-plan.md new file mode 100644 index 0000000..87f0528 --- /dev/null +++ b/docs/core-hub-absorption-plan.md @@ -0,0 +1,307 @@ +# Core Hub absorption and `/api/v2` cutover plan + +**Status:** accepted joint input for `HUB-WP-0004-T06` and +`CORE-WP-0010-T02` +**Date:** 2026-08-21 +**Runtime source:** `hub-core` +**Rollback runtime:** `core-hub` on railiance01 + +## Decision + +Absorb Core Hub into hub-core by capability and data boundary. There is no +big-bang route move and no period with two production writers for the same +boundary. + +Hub-core owns the surviving wheel, runtime image, extension contracts, +compatibility routes, durable framework store, and migrations. Core Hub stays +deployable as the rollback runtime until every `/api/v2` route group, durable +record family, consumer gate, and rollback exercise has passed. Its Python +package is never imported by hub-core. + +The current production baseline is Core Hub chart `0.3.2` on railiance01, +backed by the `core_hub` PostgreSQL schema. Its source database on CoulombCore +is already frozen and is historical rollback evidence, not a participant in +this absorption. + +## Non-negotiable invariants + +1. A route group has exactly one production writer at any moment. +2. A write freeze, final export, count/content comparison, and explicit + operator approval precede every production writer change. +3. Shadow traffic consists of reads and synthetic fixture writes only. Do not + mirror normal writes to both runtimes. +4. Hub-core and Core Hub keep separate SQLAlchemy metadata and migration + lineages. Hub-core may copy legacy rows but must not import `core_hub` + models or migrations. +5. IDs, timestamps, relationships, source revision, source table, and import + bundle hash survive migration as provenance. +6. Only API-key hashes and prefixes move. Raw keys, static operator tokens, + database credentials, and secret values never enter bundles or evidence. +7. Authorization fails closed when its dependency is unavailable. The + compatibility API-key validator is temporary; authorization decisions + remain owned through the identity/policy boundary. +8. Core Hub remains a tested rollback target until the stabilization and + rollback-retirement gates close. + +## Runtime and database shape + +The hub-core image selected by ADR-0001 is the candidate runtime. Its current +in-memory `PortStore` is not a migration target. The first absorption slice +must add a PostgreSQL implementation, production readiness checks, an audit +ledger, and migrations owned entirely by hub-core. + +Use the existing platform PostgreSQL allocation currently assigned to Core +Hub; do not consume a fifth application slot. Add a distinct target schema +(provisionally `hub_runtime`) beside retained legacy schema `core_hub`, then +transfer the consumer declaration and dynamic credential identity as part of +the final package transition. Separate schemas make source/target counts and +rollback observable without mixing ORM metadata. + +The deployment package currently named `rapp-core-hub` owns the verified +railiance01 chart, policies, secret projections, live verifier, and rollback +surface. During dual-run it must deploy both immutable image revisions or a +candidate release beside the legacy release. Before Core Hub is archived, the +package must either be renamed/replaced by a hub-core-owned rapp package or +recorded explicitly as the surviving package with updated ownership and +intent. No application source moves into the deployment package. + +## Traffic controls + +Implement route-group controls before moving data: + +- `HUB_CORE_V2_GROUPS`: compatibility groups served by hub-core; +- `HUB_CORE_V2_WRITE_GROUPS`: subset allowed to mutate hub-core authority; +- `CORE_HUB_V2_WRITE_GROUPS`: legacy groups still allowed to mutate Core Hub; +- an ingress/gateway route map selecting `core-hub` or `hub-core` per group; +- a shadow comparator that calls both read targets, normalizes ordering and + volatile fields, and records hashes without response bodies containing + sensitive data. + +The exact configuration carrier may be Helm values rather than environment +variables, but the three states are mandatory and independently observable: + +| State | Public owner | Candidate behavior | Write rule | +| --- | --- | --- | --- | +| legacy | Core Hub | health plus synthetic fixtures | Core Hub only | +| shadow | Core Hub | normalized read comparison | Core Hub only | +| candidate | hub-core | public read/write for the group | hub-core only | + +Direct service access is network-policy restricted. The route map is the +normal traffic authority; application write-group checks are defense in +depth. A configuration assertion must reject overlap between legacy and +candidate write sets. + +## Route and module move order + +Each slice is independently buildable, reversible, and evidenced. Later +slices do not excuse a failed earlier gate. + +### S0 — Durable runtime, compatibility shell, and migration tooling + +No public route moves. + +- Add hub-core PostgreSQL `PortStore`, session lifecycle, readiness, audit + ledger, import/export commands, and a migration bundle schema. +- Add `/api/v2` router composition, legacy response serializers, bearer-token + dependency, per-group write controls, and normalized shadow comparison. +- Import the static operator token through the existing OpenBao/ExternalSecret + lane and copy API-key hashes into the isolated compatibility auth store. +- Preserve current error/status semantics and unprefixed OpenAPI aliases. +- Extend the conformance suite with durable restart/rebuild, authorization + allow/deny/unavailable, metadata isolation, and migration idempotence. + +Gate: frozen source fixture imports twice without duplicates; all seven source +table counts can be represented; production readiness fails when PostgreSQL or +authorization is unavailable; no traffic has moved. + +### S1 — System and read-only discovery + +Routes: + +- `/healthz`, `/readyz`; +- `/api/v2/widget-types`, `/api/v2/event-types`; +- `/api/v2/annotation-categories`, `/api/v2/policy-scopes`; +- `/api/v2/openapi.json`, `/api/v2/openapi.yaml`, `/api/v2/docs`; +- compatibility aliases published by the current OpenAPI surface. + +Move the catalogs, schemas, OpenAPI snapshot/export behavior, and public +consumer fixtures. Static catalog responses are compared as canonical JSON. +Readiness must reflect the dependencies required by enabled groups rather than +reporting healthy from process liveness alone. + +Gate: public catalog and documentation smoke passes against both targets; +normalized payload hashes match; ops-hub public gate and activity-core +resolver pass; rollback route-map change is rehearsed. + +### S2 — Registry and manifest authority + +Routes: + +- `GET/POST /api/v2/hubs`; +- `GET/POST /api/v2/hub-capability-manifests`; +- `PATCH /api/v2/hub-capability-manifests/{manifest_id}`; +- `POST /api/v2/hub-capability-manifests/{manifest_id}/activate`; +- `GET /api/v2/hub-registry`. + +Modules/data: + +- copy `hubs` and `hub_capability_manifests` into hub-core registry authority; +- preserve legacy UUIDs and camel-case compatibility serialization; +- validate new writes against `helixforge.hub-extension`, while a named legacy + adapter reads migrated 0.x records without silently rewriting them; +- add activation audit records and expose registry projections with + provenance. + +Gate: table counts and order-independent row hashes match; manifest activation +is idempotent and audited; protected allow/deny checks pass; ops-hub bootstrap +can resolve and update its manifest; rollback delta replay is proven. + +### S3 — API consumers and compatibility credentials + +Routes: + +- `GET/POST /api/v2/api-consumers`; +- `POST /api/v2/api-consumers/{consumer_id}/api-keys`; +- `POST /api/v2/token`. + +Modules/data: + +- copy `api_consumers` and `api_keys`, including status, scopes, quotas, hash, + and prefix but never raw key material; +- preserve one-time key issuance semantics and ensure generated secrets are + absent from logs, projections, migration bundles, and evidence; +- keep the static operator credential and dynamic consumer keys in distinct + custody lanes; +- record a residual owner and follow-on task for replacing compatibility keys + with the platform identity/authorization ports. The shim may outlive Core + Hub briefly, but it is not a permanent hub-core authority. + +Gate: existing ops-hub and activity-core credentials authenticate against the +candidate; invalid, revoked, missing, and dependency-unavailable cases deny; +rate/quota behavior is either preserved or explicitly residual-owned; reverse +delta replay for newly issued key hashes is rehearsed without exposing a key. + +### S4 — Widgets and interaction evidence + +Routes: + +- `GET/POST /api/v2/widgets`; +- `GET/POST /api/v2/interaction-events`. + +Modules/data: + +- copy `widgets` into the operator-surface registry/projection boundary; +- copy `interaction_events` into the physically separate interaction-event + authority while retaining legacy `widgetId`, event type, context, metadata, + IDs, and timestamps in the compatibility view; +- route new writes through `port.events.interaction` and build `/api/v2` reads + from the same authority rather than a second compatibility-only store. + +Gate: counts and hashes match; projection rebuild fixture and migrated sample +produce provenance-equivalent views; progress events never appear in the +interaction family; ops-hub and activity-core each post and read back unique +correlated evidence; rollback delta replay is proven. + +### S5 — Empty compatibility collections and operator console + +Routes: + +- `GET/POST /api/v2/annotations`; +- `GET/POST /api/v2/requirement-candidates`; +- `GET/POST /api/v2/decision-records`; +- `GET/POST /api/v2/deployment-records`; +- `GET/POST /api/v2/outcome-signals`; +- `/console`. + +The five collections have no Core Hub durable models and currently return +empty data. Preserve that behavior only as a compatibility adapter; do not +claim or create authority. Each endpoint needs an explicit retirement or +future-owner record before the adapter is removed. Rebuild the console from +hub-core projections and public APIs; it must not query the retained +`core_hub` schema. + +Gate: exact empty/error compatibility passes, the console authorization +boundary and visual smoke pass, and every deferred collection has a residual +disposition. + +### S6 — Whole-host cutover and Core Hub retirement + +After S1–S5 are in candidate state: + +1. Freeze all remaining Core Hub writes and capture a final encrypted backup, + table counts, content hashes, schema revision, image digest, and correlation + ID. +2. Import any remaining deltas and require zero unexplained count/hash drift. +3. Run hub-core conformance, the Core Hub deployed smoke, ops-hub bootstrap, + and activity-core resolver/write-readback against the candidate image. +4. With explicit operator approval, point `hub.coulomb.social` entirely at + hub-core and set every Core Hub write group empty. +5. Observe at least seven consecutive days with no unexplained 5xx, no failed + consumer gate, no data drift, and no normal request to Core Hub. Record any + exception with an owner and expiry. +6. Exercise rollback once during rehearsal. After the stabilization window and + explicit retirement approval, scale Core Hub to zero while retaining its + immutable image, database backup, deployment revision, and route-map + rollback instructions until the agreed expiry. +7. Move the final OpenAPI/contracts/smoke fixtures into hub-core history, + record residual owners, archive the `core-hub` repository read-only, and + update the deployment package identity. + +## Evidence required for every durable slice + +| Evidence | Pass condition | +| --- | --- | +| Source identity | Core Hub image/source revision and schema revision recorded | +| Target identity | hub-core wheel/image version, revision, and contract version recorded | +| Backup | Encrypted location and checksum recorded; no secret value logged | +| Counts | Per-table source/import/target counts with zero unexplained delta | +| Content | Order-independent canonical row hashes match after documented transforms | +| Provenance | Source table/ID/revision, import bundle hash, and target record IDs queryable | +| Contract | Static OpenAPI diff has no unexplained removed route/status/schema behavior | +| Authorization | Missing/invalid/revoked deny; allowed roles pass; dependency outage denies | +| Failure mode | Database, policy/auth, and candidate unavailability behavior recorded | +| Consumers | Owned ops-hub and activity-core gates pass against the candidate | +| Rollback | Reverse delta export/import and route-map reversal exercised | + +An unexplained row, response, authorization, or provenance mismatch fails the +slice. A waiver requires a human progress note and a live residual task. + +## Smoke and change ownership + +| Surface | Owner during absorption | +| --- | --- | +| Hub-core unit, contract, conformance, migration, and image tests | `hub-core` | +| Legacy OpenAPI and deployed-smoke oracle | `core-hub` until archive, then historical fixture in `hub-core` | +| ops-hub public/bootstrap gate | `ops-hub` | +| activity-core resolver and interaction write/readback | `activity-core` | +| Helm render, policy, secret projection, live verification, traffic map, rollback | rapp package (currently `rapp-core-hub`) + platform owners | +| Production writer flip, rollback, scale-to-zero, archive | operator approval required | + +## Rollback procedure + +For a failed candidate slice: + +1. Stop candidate writes for the affected group; do not enable legacy writes + yet. +2. Export the candidate-only delta with IDs and hashes, validate it against the + reverse adapter, and import it into the retained legacy schema. +3. Compare counts/content and run the legacy group smoke. +4. Route the group to Core Hub, enable only its legacy writer, and confirm the + candidate writer remains disabled. +5. Record the failure correlation ID, data interval, revisions, evidence, and + next owner. Do not continue to the next slice. + +If candidate data cannot be replayed safely, keep both writers disabled and +invoke the operator recovery path. Availability pressure does not authorize +discarding acknowledged writes or enabling concurrent writers. + +## Completion criteria + +`HUB-WP-0004-T06` is complete when this plan is linked to CORE-WP-0010 and its +handoff is recorded. HUB-WP-0004 may then finish; it does not claim production +absorption. + +`CORE-WP-0010` completes only when all slices have evidence, production is on +hub-core, the stabilization and rollback gates close, residuals have live +owners, the deployment package names the surviving runtime, and Core Hub is +archived read-only. diff --git a/docs/runtime.md b/docs/runtime.md new file mode 100644 index 0000000..f55e099 --- /dev/null +++ b/docs/runtime.md @@ -0,0 +1,88 @@ +# Hub Core runtime + +The initial runtime scaffold implements the packaging decision in ADR-0001 and +the minimal vertical in `HUB-WP-0004-T04`. It is a conformance and absorption +base, not yet the production replacement for Core Hub. + +## Processes + +The `runtime` dependency extra installs one `hub-core` command with three +process modes: + +```bash +uv sync --extra runtime +hub-core api --host 127.0.0.1 --port 8010 +hub-core mcp --host 127.0.0.1 --port 8011 --api-base http://127.0.0.1:8010 +hub-core migrate head --database-url postgresql+asyncpg://... +``` + +The migration command converts the async PostgreSQL URL for the packaged +synchronous Alembic environment. Production must run migrations explicitly; +the API does not auto-create tables. + +## Runtime ports + +| Port | Initial path | Behavior | +| --- | --- | --- | +| `port.registry` | `POST /ports/registry/registrations` | Validates and idempotently records a descriptor/manifest package | +| `port.messaging` | `GET/POST /ports/messaging/messages` | Addressed messages with optional conversation identity | +| `port.events.progress` | `POST /ports/events/progress` | Accepts only cataloged progress-family events | +| `port.events.interaction` | `POST /ports/events/interaction` | Accepts only cataloged interaction-family events | +| `port.projection.query` | `GET /ports/projections/{id}` | Rebuildable registry/message/event projections with provenance | + +Available projection ids are `hub_registry`, `messages`, `progress_events`, and +`interaction_events`. The two event families use distinct stores and cannot be +submitted through each other's endpoint. + +## Backend boundary and readiness + +The app is created with an injected `PortStore`. T04 ships +`InMemoryPortStore` for deterministic tests and local contract smokes. It is +ephemeral and is not a production authority. + +`GET /healthz` proves the process is alive. `GET /readyz` fails with HTTP 503 +when the active backend does not match `HUB_CORE_BACKEND`, or when the memory +backend is used without explicit permission. Development and test permit it by +default; the OCI image sets production-safe defaults: + +```text +HUB_CORE_ENV=production +HUB_CORE_BACKEND=memory +HUB_CORE_ALLOW_EPHEMERAL=0 +``` + +Therefore the image is deliberately not production-ready until a durable +backend lands in an absorption slice. A local image smoke may opt in with +`HUB_CORE_ALLOW_EPHEMERAL=1`. + +## OCI image + +The `Containerfile` uses `uv.lock` with `uv sync --frozen`, installs the runtime +extra, runs as UID/GID 10001, includes OCI version/revision labels, and exposes +API port 8010 plus MCP port 8011. + +```bash +docker build -f Containerfile \ + --build-arg VERSION=0.2.0 \ + --build-arg VCS_REF="$(git rev-parse HEAD)" \ + -t hub-core:dev . + +docker run --rm -p 8010:8010 \ + -e HUB_CORE_ALLOW_EPHEMERAL=1 \ + hub-core:dev +``` + +Deployment charts, secrets, rollout policy, and live evidence remain owned by +the rapp/platform repositories. `/api/v2` data and compatibility routes remain +on Core Hub until the T06/CORE-WP-0010 dual-run slices move them. + +## Conformance + +The public-port Tier 2/3 scaffold is documented in `docs/conformance.md`. Run +it against an isolated runtime with `hub-core conformance --base-url `. + +## Core Hub absorption + +`docs/core-hub-absorption-plan.md` defines the capability-sized `/api/v2` +route and data move order, single-writer dual-run controls, evidence gates, +rollback, and final cutover criteria shared with `CORE-WP-0010`. diff --git a/hub_core/__init__.py b/hub_core/__init__.py index 087db60..b43cc6e 100644 --- a/hub_core/__init__.py +++ b/hub_core/__init__.py @@ -1,4 +1,4 @@ -"""Reusable primitives for FOS hub services.""" +"""Contracts, reusable primitives, and runtime surfaces for HelixForge hubs.""" __all__ = ["__version__"] diff --git a/hub_core/conformance/__init__.py b/hub_core/conformance/__init__.py new file mode 100644 index 0000000..71f871f --- /dev/null +++ b/hub_core/conformance/__init__.py @@ -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", +] diff --git a/hub_core/conformance/harness.py b/hub_core/conformance/harness.py new file mode 100644 index 0000000..32e04db --- /dev/null +++ b/hub_core/conformance/harness.py @@ -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")) diff --git a/hub_core/contracts/__init__.py b/hub_core/contracts/__init__.py new file mode 100644 index 0000000..b2008c5 --- /dev/null +++ b/hub_core/contracts/__init__.py @@ -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"] diff --git a/hub_core/contracts/helixforge_hub_extension/__init__.py b/hub_core/contracts/helixforge_hub_extension/__init__.py new file mode 100644 index 0000000..f208187 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/__init__.py @@ -0,0 +1 @@ +"""Versioned ``helixforge.hub-extension`` contract packages.""" diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/README.md b/hub_core/contracts/helixforge_hub_extension/v0_1_0/README.md new file mode 100644 index 0000000..6b0d498 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/README.md @@ -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. diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/__init__.py b/hub_core/contracts/helixforge_hub_extension/v0_1_0/__init__.py new file mode 100644 index 0000000..26bec9e --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/__init__.py @@ -0,0 +1 @@ +"""Contract artifacts for ``helixforge.hub-extension`` version 0.1.0.""" diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/catalogs/event-types.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/catalogs/event-types.json new file mode 100644 index 0000000..13aedd7 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/catalogs/event-types.json @@ -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" + } + ] +} diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/compatibility-matrix.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/compatibility-matrix.json new file mode 100644 index 0000000..d4af370 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/compatibility-matrix.json @@ -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." + } +} diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/fixtures/ops-hub.extension.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/fixtures/ops-hub.extension.json new file mode 100644 index 0000000..1cf99d5 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/fixtures/ops-hub.extension.json @@ -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." + } + ] + } +} diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/fixtures/projection-rebuild.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/fixtures/projection-rebuild.json new file mode 100644 index 0000000..a7b1f07 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/fixtures/projection-rebuild.json @@ -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"]} + } +} diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/openapi/ports.openapi.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/openapi/ports.openapi.json new file mode 100644 index 0000000..2388928 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/openapi/ports.openapi.json @@ -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" + } + } + } + } + } + } +} diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/event-type-catalog.schema.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/event-type-catalog.schema.json new file mode 100644 index 0000000..c6240d7 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/event-type-catalog.schema.json @@ -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"] + } + } + } + } +} diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/hub-descriptor.schema.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/hub-descriptor.schema.json new file mode 100644 index 0000000..7972398 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/hub-descriptor.schema.json @@ -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 + } + } +} diff --git a/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/hub-manifest.schema.json b/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/hub-manifest.schema.json new file mode 100644 index 0000000..1e90ff2 --- /dev/null +++ b/hub_core/contracts/helixforge_hub_extension/v0_1_0/schemas/hub-manifest.schema.json @@ -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 + } + } + } + } +} diff --git a/hub_core/runtime/__init__.py b/hub_core/runtime/__init__.py new file mode 100644 index 0000000..402ab4e --- /dev/null +++ b/hub_core/runtime/__init__.py @@ -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) diff --git a/hub_core/runtime/app.py b/hub_core/runtime/app.py new file mode 100644 index 0000000..abd9831 --- /dev/null +++ b/hub_core/runtime/app.py @@ -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() diff --git a/hub_core/runtime/cli.py b/hub_core/runtime/cli.py new file mode 100644 index 0000000..5e75208 --- /dev/null +++ b/hub_core/runtime/cli.py @@ -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://") diff --git a/hub_core/runtime/config.py b/hub_core/runtime/config.py new file mode 100644 index 0000000..af919d7 --- /dev/null +++ b/hub_core/runtime/config.py @@ -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 + ) diff --git a/hub_core/runtime/models.py b/hub_core/runtime/models.py new file mode 100644 index 0000000..f1c1293 --- /dev/null +++ b/hub_core/runtime/models.py @@ -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] diff --git a/hub_core/runtime/ports.py b/hub_core/runtime/ports.py new file mode 100644 index 0000000..f22e174 --- /dev/null +++ b/hub_core/runtime/ports.py @@ -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 diff --git a/hub_core/runtime/store.py b/hub_core/runtime/store.py new file mode 100644 index 0000000..25dfe75 --- /dev/null +++ b/hub_core/runtime/store.py @@ -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] diff --git a/hub_core/runtime/validation.py b/hub_core/runtime/validation.py new file mode 100644 index 0000000..2df1c2b --- /dev/null +++ b/hub_core/runtime/validation.py @@ -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()) diff --git a/pyproject.toml b/pyproject.toml index fac88be..7783ece 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,27 @@ dependencies = [ "fastapi>=0.115.0", "fastmcp>=2.0.0", "httpx>=0.28.0", + "jsonschema>=4.23.0", "sqlalchemy[asyncio]>=2.0.0", "pydantic>=2.10.0", ] +[project.optional-dependencies] +runtime = [ + "alembic>=1.13.0", + "asyncpg>=0.29.0", + "psycopg2-binary>=2.9.0", + "uvicorn[standard]>=0.30.0", +] + +[project.scripts] +hub-core = "hub_core.runtime.cli:main" + +[dependency-groups] +dev = [ + "pytest>=8.0.0", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/tests/test_conformance.py b/tests/test_conformance.py new file mode 100644 index 0000000..66f29ac --- /dev/null +++ b/tests/test_conformance.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from fastapi.testclient import TestClient + +from hub_core.conformance import ConformanceHarness, find_secret_violations +from hub_core.runtime.app import create_app +from hub_core.runtime.cli import build_parser +from hub_core.runtime.config import RuntimeSettings +from hub_core.runtime.store import InMemoryPortStore + + +def isolated_target() -> TestClient: + settings = RuntimeSettings(environment="test", backend="memory", allow_ephemeral=True) + return TestClient(create_app(settings=settings, port_store=InMemoryPortStore())) + + +def test_implemented_tier_2_and_3_profile_passes_reference_runtime() -> None: + report = ConformanceHarness(isolated_target()).run() + + assert report.passed + assert report.passed_count == 8 + assert {check.check_id for check in report.checks} == { + "C1", + "C3", + "C4", + "C5", + "C6", + "C8", + "F2", + "F3", + } + assert all(check.status == "pass" for check in report.checks) + assert report.to_dict()["summary"] == {"passed": 8, "total": 8} + + +def test_projection_rebuild_scenario_leaves_separate_provenance_bearing_views() -> None: + target = isolated_target() + assert ConformanceHarness(target).run().passed + + progress = target.get("/ports/projections/progress_events").json() + interaction = target.get("/ports/projections/interaction_events").json() + + assert progress["data"]["rebuild_from"] == ["progress_events"] + assert interaction["data"]["rebuild_from"] == ["interaction_events"] + assert {item["family"] for item in progress["data"]["items"]} == {"progress"} + assert {item["family"] for item in interaction["data"]["items"]} == {"interaction"} + assert progress["provenance"]["content_hash"] + assert interaction["provenance"]["content_hash"] + + +def test_secret_heuristic_reports_paths_without_echoing_values() -> None: + value = { + "nested": {"api_token": "do-not-echo"}, + "database": "postgresql://runtime:do-not-echo@example.invalid/hub", + "safe": "https://ops-hub.example.invalid/docs", + } + + assert find_secret_violations(value) == ["$.nested.api_token", "$.database"] + + +def test_cli_exposes_remote_conformance_runner() -> None: + args = build_parser(RuntimeSettings()).parse_args( + ["conformance", "--base-url", "http://runtime.invalid", "--json"] + ) + + assert args.command == "conformance" + assert args.base_url == "http://runtime.invalid" + assert args.as_json is True diff --git a/tests/test_contracts.py b/tests/test_contracts.py new file mode 100644 index 0000000..dcb92a5 --- /dev/null +++ b/tests/test_contracts.py @@ -0,0 +1,152 @@ +import json +from collections.abc import Mapping, Sequence +from typing import Any + +from jsonschema import Draft202012Validator, FormatChecker + +from hub_core.contracts import CONTRACT_ID, CONTRACT_VERSION, extension_contract_root + + +ROOT = extension_contract_root() +SCHEMAS = ROOT.joinpath("schemas") +FIXTURE = ROOT.joinpath("fixtures", "ops-hub.extension.json") +REBUILD_FIXTURE = ROOT.joinpath("fixtures", "projection-rebuild.json") +CATALOG = ROOT.joinpath("catalogs", "event-types.json") +OPENAPI = ROOT.joinpath("openapi", "ports.openapi.json") +COMPATIBILITY = ROOT.joinpath("compatibility-matrix.json") + +PORT_IDS = { + "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", +} + +SECRET_KEYS = { + "api_key", + "credential", + "password", + "passwd", + "private_key", + "secret", + "token", +} + + +def load_json(resource: Any) -> Any: + return json.loads(resource.read_text(encoding="utf-8")) + + +def validate(instance: Any, schema_name: str) -> None: + schema = load_json(SCHEMAS.joinpath(schema_name)) + Draft202012Validator.check_schema(schema) + Draft202012Validator(schema, format_checker=FormatChecker()).validate(instance) + + +def iter_keys(value: Any) -> set[str]: + if isinstance(value, Mapping): + keys = {str(key).lower() for key in value} + for child in value.values(): + keys.update(iter_keys(child)) + return keys + if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): + keys: set[str] = set() + for child in value: + keys.update(iter_keys(child)) + return keys + return set() + + +def test_packaged_contract_identity_and_artifacts() -> None: + assert CONTRACT_ID == "helixforge.hub-extension" + assert CONTRACT_VERSION == "0.1.0" + assert ROOT.joinpath("README.md").is_file() + assert FIXTURE.is_file() + assert REBUILD_FIXTURE.is_file() + assert CATALOG.is_file() + assert OPENAPI.is_file() + assert COMPATIBILITY.is_file() + + +def test_ops_hub_fixture_validates_against_descriptor_and_manifest_schemas() -> None: + package = load_json(FIXTURE) + + validate(package["descriptor"], "hub-descriptor.schema.json") + validate(package["manifest"], "hub-manifest.schema.json") + + assert package["descriptor"]["reuse_surface_id"] == package["manifest"]["reuse_surface_id"] + + +def test_event_catalog_validates_and_keeps_event_families_distinct() -> None: + catalog = load_json(CATALOG) + validate(catalog, "event-type-catalog.schema.json") + + event_types = catalog["event_types"] + names = [entry["type"] for entry in event_types] + families = {entry["family"] for entry in event_types} + assert len(names) == len(set(names)) + assert {"progress", "interaction"} <= families + + +def test_fixture_events_resolve_in_catalog() -> None: + package = load_json(FIXTURE) + catalog = load_json(CATALOG) + known = {entry["type"] for entry in catalog["event_types"]} + declared = set(package["manifest"]["events_emitted"]) + declared.update(package["manifest"]["events_consumed"]) + assert declared <= known + + +def test_contract_examples_contain_no_secret_fields() -> None: + assert not (iter_keys(load_json(FIXTURE)) & SECRET_KEYS) + assert not (iter_keys(load_json(REBUILD_FIXTURE)) & SECRET_KEYS) + assert not (iter_keys(load_json(CATALOG)) & SECRET_KEYS) + + +def test_openapi_declares_every_named_port() -> None: + document = load_json(OPENAPI) + assert document["openapi"] == "3.1.0" + assert document["info"]["version"] == CONTRACT_VERSION + + operations = [ + operation + for path_item in document["paths"].values() + for method, operation in path_item.items() + if method in {"get", "post", "put", "patch", "delete"} + ] + assert {operation["x-port-id"] for operation in operations} == PORT_IDS + assert all(operation["x-direction"] in {"in", "out"} for operation in operations) + assert all(operation.get("operationId") for operation in operations) + assert all(operation.get("responses") for operation in operations) + + +def test_manifest_port_enum_matches_openapi_ports() -> None: + manifest_schema = load_json(SCHEMAS.joinpath("hub-manifest.schema.json")) + declared = set(manifest_schema["$defs"]["portId"]["enum"]) + openapi = load_json(OPENAPI) + implemented = { + operation["x-port-id"] + for path_item in openapi["paths"].values() + for method, operation in path_item.items() + if method in {"get", "post", "put", "patch", "delete"} + } + assert declared == implemented == PORT_IDS + + +def test_compatibility_matrix_covers_current_and_core_hub_adapter() -> None: + matrix = load_json(COMPATIBILITY) + assert matrix["contract_id"] == CONTRACT_ID + assert matrix["current_version"] == CONTRACT_VERSION + assert any(entry["version"] == CONTRACT_VERSION for entry in matrix["versions"]) + assert any( + adapter["source"] == "core-hub.hub-manifest" + and adapter["target_version"] == CONTRACT_VERSION + for adapter in matrix["migration_adapters"] + ) diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 0000000..09acee6 --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone +from uuid import uuid4 + +from fastapi.testclient import TestClient + +from hub_core.contracts import extension_contract_root +from hub_core.runtime.app import create_app +from hub_core.runtime.cli import _sync_database_url, build_parser +from hub_core.runtime.config import RuntimeSettings +from hub_core.runtime.store import InMemoryPortStore + + +def client(*, allow_ephemeral: bool = True, environment: str = "test") -> TestClient: + settings = RuntimeSettings( + environment=environment, + backend="memory", + allow_ephemeral=allow_ephemeral, + ) + return TestClient(create_app(settings=settings, port_store=InMemoryPortStore())) + + +def ops_hub_package() -> dict: + fixture = extension_contract_root().joinpath("fixtures", "ops-hub.extension.json") + return json.loads(fixture.read_text(encoding="utf-8")) + + +def event_body(event_type: str) -> dict: + return { + "schema_version": "0.1.0", + "correlation_id": str(uuid4()), + "event_type": event_type, + "occurred_at": datetime.now(timezone.utc).isoformat(), + "subject_refs": {"hub": "ops-hub"}, + "payload": {"result": "ok"}, + } + + +def test_health_and_ephemeral_readiness() -> None: + runtime = client() + health = runtime.get("/healthz") + ready = runtime.get("/readyz") + + assert health.status_code == 200 + assert health.json()["service"] == "hub-core" + assert ready.status_code == 200 + assert ready.json()["status"] == "ok" + assert ready.json()["checks"]["active_backend"] == "memory" + + +def test_production_readiness_fails_closed_for_ephemeral_backend() -> None: + response = client(allow_ephemeral=False, environment="production").get("/readyz") + + assert response.status_code == 503 + assert response.json()["status"] == "degraded" + assert response.json()["checks"]["ephemeral_backend"] == "not_allowed" + + +def test_registry_validates_and_registers_idempotently() -> None: + runtime = client() + package = ops_hub_package() + correlation_id = str(uuid4()) + + first = runtime.post( + "/ports/registry/registrations", + headers={"X-Correlation-ID": correlation_id}, + json=package, + ) + second = runtime.post( + "/ports/registry/registrations", + headers={"X-Correlation-ID": correlation_id}, + json=package, + ) + projection = runtime.get("/ports/projections/hub_registry") + + assert first.status_code == 202 + assert first.json()["status"] == "accepted" + assert second.status_code == 202 + assert second.json()["status"] == "duplicate" + assert projection.status_code == 200 + assert projection.json()["data"]["items"][0]["descriptor"]["hub_slug"] == "ops-hub" + + +def test_registry_rejects_contract_mismatch() -> None: + runtime = client() + package = ops_hub_package() + package["manifest"]["reuse_surface_id"] = "capability.operations.other-hub" + + response = runtime.post( + "/ports/registry/registrations", + headers={"X-Correlation-ID": str(uuid4())}, + json=package, + ) + + assert response.status_code == 422 + assert "reuse_surface_id must match" in response.json()["detail"] + + +def test_messaging_port_writes_and_reads_conversation() -> None: + runtime = client() + conversation_id = uuid4() + body = { + "schema_version": "0.1.0", + "correlation_id": str(uuid4()), + "conversation_id": str(conversation_id), + "from_address": "agent:codex", + "to_addresses": ["hub:ops-hub", "agent:operator"], + "body": "Non-secret runtime smoke.", + "subject_refs": {"hub": "ops-hub"}, + } + + sent = runtime.post("/ports/messaging/messages", json=body) + listed = runtime.get( + "/ports/messaging/messages", + params={"address": "hub:ops-hub", "conversation_id": str(conversation_id)}, + ) + + assert sent.status_code == 202 + assert listed.status_code == 200 + assert len(listed.json()["items"]) == 1 + assert listed.json()["items"][0]["data"]["body"] == body["body"] + + +def test_progress_and_interaction_events_stay_separate() -> None: + runtime = client() + + progress = runtime.post("/ports/events/progress", json=event_body("hub.progress.recorded")) + interaction = runtime.post( + "/ports/events/interaction", + json=event_body("hub.interaction.recorded"), + ) + progress_projection = runtime.get("/ports/projections/progress_events").json() + interaction_projection = runtime.get("/ports/projections/interaction_events").json() + + assert progress.status_code == 202 + assert interaction.status_code == 202 + assert [item["family"] for item in progress_projection["data"]["items"]] == ["progress"] + assert [item["family"] for item in interaction_projection["data"]["items"]] == [ + "interaction" + ] + + +def test_event_ports_reject_wrong_or_uncataloged_families() -> None: + runtime = client() + + wrong_family = runtime.post( + "/ports/events/progress", + json=event_body("hub.interaction.recorded"), + ) + unknown = runtime.post( + "/ports/events/interaction", + json=event_body("hub.interaction.unknown"), + ) + + assert wrong_family.status_code == 422 + assert "belongs to 'interaction'" in wrong_family.json()["detail"] + assert unknown.status_code == 422 + assert "is not cataloged" in unknown.json()["detail"] + + +def test_unknown_projection_is_not_found() -> None: + response = client().get("/ports/projections/not-a-projection") + + assert response.status_code == 404 + + +def test_runtime_openapi_marks_the_five_minimal_ports() -> None: + document = client().get("/openapi.json").json() + port_ids = { + operation["x-port-id"] + for path_item in document["paths"].values() + for method, operation in path_item.items() + if method in {"get", "post", "put", "patch", "delete"} and "x-port-id" in operation + } + + assert port_ids == { + "port.registry", + "port.messaging", + "port.events.progress", + "port.events.interaction", + "port.projection.query", + } + + +def test_cli_exposes_api_mcp_and_migration_processes() -> None: + parser = build_parser(RuntimeSettings()) + + assert parser.parse_args(["api"]).command == "api" + assert parser.parse_args(["mcp"]).command == "mcp" + assert parser.parse_args(["migrate", "head", "--database-url", "postgresql://db"]).command == ( + "migrate" + ) + assert _sync_database_url("postgresql+asyncpg://db") == "postgresql+psycopg2://db" diff --git a/uv.lock b/uv.lock index dec916b..8b5baac 100644 --- a/uv.lock +++ b/uv.lock @@ -19,6 +19,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446 }, ] +[[package]] +name = "alembic" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648", size = 2070816 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be", size = 265946 }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -50,6 +64,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875 }, ] +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042 }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504 }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321 }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685 }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858 }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852 }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175 }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111 }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928 }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067 }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156 }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636 }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079 }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606 }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569 }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867 }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349 }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428 }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678 }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505 }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744 }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251 }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901 }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280 }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931 }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608 }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738 }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026 }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426 }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495 }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062 }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -511,6 +565,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247 }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064 }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851 }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842 }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238 }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567 }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918 }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148 }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368 }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447 }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448 }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460 }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312 }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117 }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183 }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079 }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596 }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865 }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189 }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610 }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705 }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023 }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405 }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497 }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585 }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297 }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535 }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209 }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -543,19 +633,41 @@ dependencies = [ { name = "fastapi" }, { name = "fastmcp" }, { name = "httpx" }, + { name = "jsonschema" }, { name = "pydantic" }, { name = "sqlalchemy", extra = ["asyncio"] }, ] +[package.optional-dependencies] +runtime = [ + { name = "alembic" }, + { name = "asyncpg" }, + { name = "psycopg2-binary" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ + { name = "alembic", marker = "extra == 'runtime'", specifier = ">=1.13.0" }, + { name = "asyncpg", marker = "extra == 'runtime'", specifier = ">=0.29.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "fastmcp", specifier = ">=2.0.0" }, { name = "httpx", specifier = ">=0.28.0" }, + { name = "jsonschema", specifier = ">=4.23.0" }, + { name = "psycopg2-binary", marker = "extra == 'runtime'", specifier = ">=2.9.0" }, { name = "pydantic", specifier = ">=2.10.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'runtime'", specifier = ">=0.30.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0.0" }] + [[package]] name = "idna" version = "3.18" @@ -565,6 +677,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -687,6 +808,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160 }, ] +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010 }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -699,6 +832,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687 }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +] + [[package]] name = "mcp" version = "1.28.1" @@ -793,6 +989,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743 }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459 }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936 }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676 }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917 }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843 }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556 }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714 }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154 }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882 }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298 }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230 }, + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419 }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990 }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696 }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982 }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867 }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578 }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816 }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307 }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968 }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369 }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347 }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428 }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184 }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157 }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970 }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175 }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658 }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251 }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810 }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977 }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466 }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762 }, +] + [[package]] name = "py-key-value-aio" version = "0.4.5" @@ -972,6 +1218,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063 }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1328,6 +1590,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219 }, ] +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936 }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769 }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413 }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307 }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970 }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343 }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 }, +] + [[package]] name = "watchfiles" version = "1.2.0" diff --git a/workplans/HUB-WP-0001-statehub-bootstrap.md b/workplans/HUB-WP-0001-statehub-bootstrap.md index a58e263..13987e8 100644 --- a/workplans/HUB-WP-0001-statehub-bootstrap.md +++ b/workplans/HUB-WP-0001-statehub-bootstrap.md @@ -6,7 +6,7 @@ domain: inter_hub repo: hub-core status: finished owner: codex -topic_slug: inter_hub +topic_slug: custodian created: "2026-06-16" updated: "2026-06-22" state_hub_workstream_id: "4a31e8cd-1a06-40bf-abc9-76ca9ac173f2" @@ -68,4 +68,4 @@ make fix-consistency REPO=hub-core Completed 2026-06-22: seeded `workplans/HUB-WP-0002-import-refactor-adapter-seams.md` to close the remaining CUST-WP-0048 adapter seams (capability request writes, MCP -composition, regression handoff). \ No newline at end of file +composition, regression handoff). diff --git a/workplans/HUB-WP-0002-import-refactor-adapter-seams.md b/workplans/HUB-WP-0002-import-refactor-adapter-seams.md index 01e9cb2..abd0df0 100644 --- a/workplans/HUB-WP-0002-import-refactor-adapter-seams.md +++ b/workplans/HUB-WP-0002-import-refactor-adapter-seams.md @@ -6,7 +6,7 @@ domain: inter_hub repo: hub-core status: finished owner: codex -topic_slug: inter_hub +topic_slug: custodian created: "2026-06-22" updated: "2026-06-22" state_hub_workstream_id: "439b559b-fcb7-4d21-b831-cfc9c6bbc1a0" @@ -82,4 +82,4 @@ After T01–T02 land (or are explicitly deferred with documented seams): Completed 2026-06-22: hub-core 27 tests pass; state-hub capability tests pass (20/20) and full suite 352/353 (one pre-existing health-route flake). -Extraction boundary updated. \ No newline at end of file +Extraction boundary updated. diff --git a/workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md b/workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md index 6c43946..827b5c3 100644 --- a/workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md +++ b/workplans/HUB-WP-0003-ecosystem-consolidation-library-lane.md @@ -6,7 +6,7 @@ domain: infotech repo: hub-core status: finished owner: codex -topic_slug: infotech +topic_slug: custodian created: "2026-07-09" updated: "2026-07-11" state_hub_workstream_id: "ee4f1eef-3a26-4af3-b735-f0c1c638656f" @@ -148,4 +148,4 @@ When `core-hub` merges hub-core utils import (CORE-WP-0009 T02): - add CHANGELOG entry listing utils adoption and ecosystem consolidation; - verify `uv run pytest -q` still passes. -Done when core-hub CI pins the new hub-core version and both test suites are green. \ No newline at end of file +Done when core-hub CI pins the new hub-core version and both test suites are green. diff --git a/workplans/HUB-WP-0004-runtime-and-extension-contract.md b/workplans/HUB-WP-0004-runtime-and-extension-contract.md index 1fa154d..01fd18d 100644 --- a/workplans/HUB-WP-0004-runtime-and-extension-contract.md +++ b/workplans/HUB-WP-0004-runtime-and-extension-contract.md @@ -4,11 +4,11 @@ type: workplan title: "Runtime consolidation and extension contract" domain: infotech repo: hub-core -status: proposed +status: finished owner: codex -topic_slug: infotech +topic_slug: custodian created: "2026-08-09" -updated: "2026-08-20" +updated: "2026-08-21" parent_project: prj-state-hub-retirement parent_workplan: SHR-WP-0001 related: @@ -35,7 +35,7 @@ Coordinated by `prj-state-hub-retirement` (SHR-WP-0001). Architecture: ```task id: HUB-WP-0004-T01 -status: todo +status: done priority: high state_hub_task_id: "47b879ac-bfb4-48c2-8fd8-c29045df6900" ``` @@ -43,11 +43,35 @@ state_hub_task_id: "47b879ac-bfb4-48c2-8fd8-c29045df6900" Review and accept SHR-ARCH-IA-0001 and SHR-ARCH-HUB-0001 as the vocabulary for ports, kinds, and extension packages. Record gaps as tasks, not silent drift. +Completed 2026-08-21. Hub-core accepts the two project artefacts as the +normative v0.1 vocabulary for this workplan: + +- authority and projection remain distinct; repository work stays authoritative + in Git and is reached through repo-manager; +- messages/conversations, progress events, interaction events, telemetry, and + domain events remain distinct information families joined by correlation; +- hub-core is the surviving framework/runtime and owns the named `port.*` + contracts, while domain hubs, functional components, and authoritative + services retain the boundaries defined by `SHR-ARCH-HUB-0001`; +- `helixforge.hub-extension` descriptors, manifests, event catalogs, fixtures, + and compatibility rules are the Tier 1 contract target. + +The accepted freeze is reflected in `INTENT.md` and `SCOPE.md`. Open questions +are assigned rather than silently deferred: + +| Gap / decision | Owning task | +| --- | --- | +| Descriptor, manifest authority, widgets/operator surfaces | T02 | +| Primary image vs permanent thin host; MCP packaging | T03 | +| Named port implementations, conversation support, missing interaction/projection surfaces | T04 | +| Tenant isolation and Tier 2/3 enforcement | T05 | +| Topic/work boundary and `/api/v2` migration order | T06 with CORE-WP-0010 | + ## Ship extension contract Tier 1 artefacts ```task id: HUB-WP-0004-T02 -status: todo +status: done priority: high state_hub_task_id: "eac82a0b-48b9-4097-afa6-a9e730de7789" ``` @@ -56,11 +80,25 @@ Publish versioned hub descriptor schema, manifest schema, event-type catalog schema, and port OpenAPI fragments for `helixforge.hub-extension` 0.1+, plus a sample hub fixture. Align with `hub-extension-contract_v0.1.yaml`. +Completed 2026-08-21. The wheel now packages +`hub_core.contracts.helixforge_hub_extension.v0_1_0` with: + +- Draft 2020-12 hub descriptor, manifest, and event-type catalog schemas; +- OpenAPI 3.1 fragments covering all 11 named `port.*` interfaces; +- an initial four-family event catalog and non-secret ops-hub fixture; +- an explicit 0.1.0 compatibility matrix, including the temporary Core Hub + manifest adapter; and +- automated schema, catalog, fixture, port-coverage, compatibility, and + secret-field checks in `tests/test_contracts.py`. + +Focused validation passed (8 tests), and a built `hub_core-0.2.0` wheel was +inspected to confirm every contract artefact is shipped. + ## Define runtime packaging decision ```task id: HUB-WP-0004-T03 -status: todo +status: done priority: high state_hub_task_id: "f149096d-74f2-4c5a-8907-f81cba8e3edb" ``` @@ -69,11 +107,18 @@ Record whether hub-core ships a primary runtime image (recommended) with the library remaining importable, vs library + permanent thin host. Decision before absorbing core-hub routes. +Completed 2026-08-21. `docs/adr/ADR-0001-runtime-packaging.md` selects one +hub-core release lineage with an importable Python wheel and a primary runtime +OCI image built from this repository. API, MCP, and migration jobs may be +separate processes from that image. A permanent thin-host repository is +rejected; core-hub remains only as the dual-run rollback runtime until its +routes, data, contracts, and fixtures are absorbed. + ## Implement core ports (minimal vertical) ```task id: HUB-WP-0004-T04 -status: todo +status: done priority: high state_hub_task_id: "fa4db2f7-0e86-4a41-8469-a8704d9c2a46" ``` @@ -82,11 +127,36 @@ Expose minimal working surfaces for `port.registry`, `port.messaging`, `port.events.progress`, `port.events.interaction`, and `port.projection.query` sufficient for dual-run and conformance scaffolding. +Implement the minimal application factory, runtime dependency group, process +commands, health/readiness surface, and primary image scaffold selected by +ADR-0001 alongside these ports. + +Completed 2026-08-21. `hub_core.runtime` now provides an injectable FastAPI +application and `PortStore` boundary with a deterministic in-memory conformance +backend. The vertical implements contract-valid, idempotent registry writes; +addressed messages/conversations; catalog-enforced and physically separate +progress and interaction event sinks; and provenance-bearing projections. + +The `hub-core` console entrypoint starts API and MCP processes or packaged +Alembic migrations. The runtime extra is locked in `uv.lock`. `Containerfile` +builds that frozen dependency set, runs as UID/GID 10001, labels version and +revision, and defaults to fail-closed readiness while only the ephemeral store +exists. `docs/runtime.md` records the boundary and commands. + +Validation evidence: + +- focused contract/runtime tests pass; +- local API and MCP process smokes pass; +- packaged migration head resolves as `0001_core_schema`; +- the locked OCI image builds and passes health, opted-in readiness, and + ops-hub contract registration; and +- production-mode memory readiness returns 503 unless explicitly allowed. + ## Conformance harness scaffold ```task id: HUB-WP-0004-T05 -status: todo +status: done priority: medium state_hub_task_id: "6f8fb0e8-8b9b-4c37-ae62-0748dd0e5975" ``` @@ -95,11 +165,26 @@ Scaffold Tier 2/3 automated checks (schema validate, no-secrets, event family separation, projection rebuild fixture). Full green suite may continue under follow-on WPs. +Completed 2026-08-21. `hub_core.conformance` now drives a public HTTP target +through an implementation-neutral client protocol. The initial green profile +automates Tier 2 checks C1, C3, C4, C5, C6, and C8 plus Tier 3 checks F2 and F3. +It validates the packaged schemas and catalog, rejects secret-shaped fixture +material, exercises health and idempotent activation with correlation +propagation, rejects wrong-family/uncataloged events, proves family separation, +and verifies authority-derived projections with rebuild-source declarations and +provenance hashes. + +The wheel ships `fixtures/projection-rebuild.json`; `hub-core conformance` +supports human-readable or JSON results against an isolated remote runtime. +`docs/conformance.md` explicitly records uncovered requirements and tenant +isolation as open rather than treating absent checks as passing. The reference +runtime passes all eight implemented checks over a live HTTP process. + ## Absorption plan with core-hub ```task id: HUB-WP-0004-T06 -status: todo +status: done priority: high state_hub_task_id: "b35fff11-ac5a-4d70-9f54-416331baea87" ``` @@ -107,6 +192,24 @@ state_hub_task_id: "b35fff11-ac5a-4d70-9f54-416331baea87" With CORE-WP-0010, document route/module move order for `/api/v2`, dual-run, and cutover criteria. No big-bang. +Completed 2026-08-21. `docs/core-hub-absorption-plan.md` reconciles Core Hub's +finished runtime inventory, its current railiance01 production/rollback state, +all 22 checked-in OpenAPI paths, seven durable tables, both production +consumers, and the project retirement gates. + +The accepted plan uses six governed capability slices after a durable-runtime +foundation. It requires isolated legacy/candidate schemas, exactly one writer +per route group, read-only shadow comparison, per-slice count/hash/provenance +and authorization evidence, ops-hub and activity-core smokes, reverse-delta +rollback rehearsal, explicit operator approval for production writer changes, +and a seven-day final stabilization window before Core Hub retirement. The +compatibility credential shim and empty collection adapters receive explicit +residual gates rather than becoming silent permanent authorities. + +State Hub decision `12514947-6cc0-42a0-98ca-9aacc9d875b0` records the +single-writer slice design. Message `819d5af0-959f-4b0d-9f52-87514d99b480` +hands the plan to `core-hub` for the file-backed CORE-WP-0010-T02 transition. + ## Readiness review (2026-08-20) Reviewed against current repo state at the request of `STATE-WP-0079`, which is @@ -154,9 +257,9 @@ the owner to flip.* ## Acceptance -- [ ] Architecture freeze acknowledged in INTENT/SCOPE -- [ ] Tier 1 contract artefacts published and versioned -- [ ] Runtime packaging decision recorded -- [ ] Core ports have a minimal implementable surface -- [ ] Conformance harness scaffold exists -- [ ] Absorption plan linked to CORE-WP-0010 +- [x] Architecture freeze acknowledged in INTENT/SCOPE +- [x] Tier 1 contract artefacts published and versioned +- [x] Runtime packaging decision recorded +- [x] Core ports have a minimal implementable surface +- [x] Conformance harness scaffold exists +- [x] Absorption plan linked to CORE-WP-0010