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

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

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
.git
.pytest_cache
.ruff_cache
.venv
__pycache__
dist
docs
tests
workplans

View file

@ -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

47
Containerfile Normal file
View file

@ -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"]

View file

@ -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.
> Centralize the hub contract; keep every authority with the component or
> domain that owns it.

View file

@ -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)
bash $(ECOSYSTEM_REGRESSION)

View file

@ -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 <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.

View file

@ -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)
- Federation registry: `registry/README.md` (reuse-surface contract)

View file

@ -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 |

View file

@ -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.

44
docs/conformance.md Normal file
View file

@ -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.

View file

@ -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 S1S5 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.

88
docs/runtime.md Normal file
View file

@ -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 <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`.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

View file

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

View file

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

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

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

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

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

View file

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

View file

@ -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"

68
tests/test_conformance.py Normal file
View file

@ -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

152
tests/test_contracts.py Normal file
View file

@ -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"]
)

195
tests/test_runtime.py Normal file
View file

@ -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"

304
uv.lock generated
View file

@ -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"

View file

@ -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).
composition, regression handoff).

View file

@ -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 T01T02 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.
Extraction boundary updated.

View file

@ -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.
Done when core-hub CI pins the new hub-core version and both test suites are green.

View file

@ -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