feat: add hub runtime and extension contract
This commit is contained in:
parent
fce19f193f
commit
7e1ec03f0c
44 changed files with 3875 additions and 84 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.git
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
.venv
|
||||
__pycache__
|
||||
dist
|
||||
docs
|
||||
tests
|
||||
workplans
|
||||
|
|
@ -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
47
Containerfile
Normal 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"]
|
||||
57
INTENT.md
57
INTENT.md
|
|
@ -2,32 +2,35 @@
|
|||
|
||||
**Project:** `hub-core`
|
||||
**Domain:** `infotech`
|
||||
**Status:** Active — library anchor for hub ecosystem (`CUST-WP-0057`)
|
||||
**Updated:** 2026-07-09
|
||||
**Status:** Active — surviving hub framework and runtime (`HUB-WP-0004`)
|
||||
**Updated:** 2026-08-21
|
||||
|
||||
---
|
||||
|
||||
## One-line intent
|
||||
|
||||
`hub-core` provides reusable FastAPI, SQLAlchemy, and MCP primitives so multiple
|
||||
FOS hub services can share a common foundation without importing each other's
|
||||
domain-specific coordination models.
|
||||
`hub-core` provides the contracts, reusable primitives, and surviving runtime
|
||||
for HelixForge hubs without absorbing domain authorities or repository-owned
|
||||
work records.
|
||||
|
||||
---
|
||||
|
||||
## Ecosystem position
|
||||
|
||||
`hub-core` is the **library layer** in the three-repo hub stack:
|
||||
`hub-core` is the **surviving framework and runtime** in the consolidating
|
||||
three-repo hub stack:
|
||||
|
||||
| Repo | Role |
|
||||
| --- | --- |
|
||||
| `hub-core` | Shared Python package — this repo |
|
||||
| `state-hub` | Dev coordination host (primary consumer) |
|
||||
| `core-hub` | Production framework (`/api/v2`; adopts hub-core utils) |
|
||||
| `hub-core` | Shared Python package plus target framework/runtime — this repo |
|
||||
| `state-hub` | Legacy coordination host; capabilities move or retire incrementally |
|
||||
| `core-hub` | Current `/api/v2` runtime; routes are absorbed, then the repo is archived |
|
||||
|
||||
Canon: `/home/worsch/the-custodian/docs/hub-ecosystem-architecture.md`
|
||||
|
||||
**Naming:** `hub-core` = core *primitives* (library). `core-hub` = core *framework* (service). Do not conflate them.
|
||||
**Transition naming:** `hub-core` is the surviving product name. `core-hub`
|
||||
names the current service being absorbed; keep the distinction only while the
|
||||
compatibility and cutover work remains.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -52,12 +55,15 @@ package own only its domain-specific tables, workflows, and policies.
|
|||
|
||||
## Governing principle
|
||||
|
||||
> **Hub-core is a library, not a hub.**
|
||||
> **Hub-core is the framework and runtime substrate, not a domain hub or a
|
||||
> repository authority.**
|
||||
|
||||
It ships models, schemas, router factories, migration scaffolds, utilities, and
|
||||
an optional FastMCP base server. A consuming repository (for example
|
||||
`state-hub`) wires database sessions, auth, host-specific callbacks, and
|
||||
domain-only routes into those factories.
|
||||
Today it ships models, schemas, router factories, migration scaffolds,
|
||||
utilities, and an optional FastMCP base server. Under
|
||||
`SHR-ARCH-HUB-0001`, those importable surfaces remain while hub-core grows into
|
||||
the surviving runtime. `ADR-0001` selects a primary OCI image built from this
|
||||
repository while keeping the wheel importable; that packaging must not move
|
||||
domain authority into hub-core.
|
||||
|
||||
`hub-core` should answer:
|
||||
|
||||
|
|
@ -77,7 +83,8 @@ remain in `state-hub` and other host implementations.
|
|||
|
||||
## What it is
|
||||
|
||||
`hub-core` is the **shared Python package** for FOS hub services.
|
||||
`hub-core` is the **shared Python package and target runtime** for HelixForge
|
||||
hub composition.
|
||||
|
||||
Current package surface (`hub_core/`):
|
||||
|
||||
|
|
@ -87,6 +94,7 @@ Current package surface (`hub_core/`):
|
|||
| `schemas/` | Pydantic contracts matching core models plus DoI report shapes |
|
||||
| `routers/` | Factory functions: domains, repos, messages, progress, capabilities, TPSC, policy |
|
||||
| `mcp/` | `HubCoreMCPServer` — generic orientation, messaging, capability, repo, DoI, TPSC/GDPR, risk/alert, progress tools |
|
||||
| `contracts/` | Packaged `helixforge.hub-extension` schemas, named-port OpenAPI, catalogs, fixtures, compatibility |
|
||||
| `migrations/` | Alembic scaffold and `0001_core_schema` for adopters |
|
||||
| `utils/` | Slugs, pagination, repo path resolution, trailing-slash routing |
|
||||
| `events.py` | Canonical FOS §10 risk and alert event types |
|
||||
|
|
@ -100,8 +108,8 @@ models where extended, and workflow callbacks.
|
|||
|
||||
| Concern | Owner |
|
||||
|---|---|
|
||||
| Running production hub deployment | `state-hub`, `ops-hub`, future hubs |
|
||||
| Topics, workplans, tasks, decisions, SBOM, token accounting | `state-hub` (dev-hub layer) |
|
||||
| Repository-owned workplans, tasks, decisions, and consistency | `repo-manager` over Git authority |
|
||||
| Domain-specific business data and APIs | Domain hubs and specialized services |
|
||||
| Custodian canon, constitution, domain charters | `the-custodian` |
|
||||
| Event-triggered maintenance task creation | `activity-core` |
|
||||
| General issue/task lifecycle outside Custodian workplans | `issue-core` |
|
||||
|
|
@ -118,9 +126,9 @@ acceptance flows) stay in the host hub.
|
|||
|
||||
| Consumer | Relationship |
|
||||
|---|---|
|
||||
| `state-hub` | First adopter; incremental import of schemas, routers, MCP (CUST-WP-0025 T08+) |
|
||||
| `ops-hub` | Planned consumer of shared primitives without dev-hub tables |
|
||||
| Future FOS hubs | Fin-hub and domain hubs mount subsets of hub-core factories |
|
||||
| `state-hub` | Compatibility source during incremental capability cutover |
|
||||
| `core-hub` | `/api/v2` route/runtime source to absorb before archival |
|
||||
| `ops-hub`, `fin-hub`, future hubs | Domain/aspect extensions using versioned hub-core ports and manifests |
|
||||
|
||||
Extraction boundary and migration status:
|
||||
`/home/worsch/the-custodian/docs/hub-core-extraction-boundary.md`
|
||||
|
|
@ -134,8 +142,9 @@ Extraction boundary and migration status:
|
|||
- a new hub can register domains and repos using hub-core routers without copying SQLAlchemy models
|
||||
- State Hub pytest suite passes with hub-core as an editable dependency
|
||||
- MCP tools for orientation, messages, progress, and TPSC behave consistently across hosts that opt in
|
||||
- the surviving runtime exposes versioned `helixforge.hub-extension` ports and projections
|
||||
- schema changes to shared primitives are versioned through hub-core migrations, not ad hoc forks
|
||||
- dev-hub-specific foreign keys never appear in hub-core models (extension via host callbacks or JSON context fields)
|
||||
- repository work authority and domain-specific business models never migrate into hub-core
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -145,9 +154,11 @@ Extraction boundary and migration status:
|
|||
- Owning PostgreSQL instance provisioning for any environment
|
||||
- Becoming a general application framework unrelated to hub-shaped services
|
||||
- Absorbing reuse-surface capability maturity registry semantics
|
||||
- Owning Git mutation, work-record reconciliation, authorization decisions, secrets, or scheduling
|
||||
|
||||
---
|
||||
|
||||
## Working mantra
|
||||
|
||||
> Extract once what every hub needs; keep domain drama in the hub that owns it.
|
||||
> Centralize the hub contract; keep every authority with the component or
|
||||
> domain that owns it.
|
||||
|
|
|
|||
15
Makefile
15
Makefile
|
|
@ -1,7 +1,9 @@
|
|||
.PHONY: install test ecosystem-regression
|
||||
.PHONY: install test runtime-run conformance container-build ecosystem-regression
|
||||
|
||||
UV ?= uv
|
||||
ECOSYSTEM_REGRESSION ?= /home/worsch/the-custodian/scripts/hub-ecosystem-regression.sh
|
||||
IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/hub-core
|
||||
IMAGE_TAG ?= dev
|
||||
|
||||
install:
|
||||
$(UV) sync
|
||||
|
|
@ -9,5 +11,14 @@ install:
|
|||
test:
|
||||
$(UV) run python -m pytest -q
|
||||
|
||||
runtime-run:
|
||||
$(UV) run --extra runtime hub-core api
|
||||
|
||||
conformance:
|
||||
$(UV) run hub-core conformance --base-url $${HUB_CORE_API_BASE:-http://127.0.0.1:8010}
|
||||
|
||||
container-build:
|
||||
docker build -f Containerfile -t $(IMAGE_REPOSITORY):$(IMAGE_TAG) .
|
||||
|
||||
ecosystem-regression:
|
||||
bash $(ECOSYSTEM_REGRESSION)
|
||||
bash $(ECOSYSTEM_REGRESSION)
|
||||
|
|
|
|||
57
README.md
57
README.md
|
|
@ -1,17 +1,23 @@
|
|||
# Hub Core
|
||||
|
||||
Reusable FastAPI, SQLAlchemy, and MCP primitives for FOS hubs.
|
||||
Contracts, reusable Python primitives, and the surviving runtime for
|
||||
HelixForge hubs.
|
||||
|
||||
## Hub stack glossary
|
||||
|
||||
| Name | Role |
|
||||
| --- | --- |
|
||||
| **hub-core** | This repo — shared Python library (`hub_core`) |
|
||||
| **state-hub** | Dev coordination host (workplans, MCP) |
|
||||
| **core-hub** | Production framework (`/api/v2`, operator console) |
|
||||
| **hub-core** | This repo — importable package plus target primary runtime image |
|
||||
| **state-hub** | Legacy coordination host being replaced capability by capability |
|
||||
| **core-hub** | Current `/api/v2` runtime retained temporarily for absorption rollback |
|
||||
|
||||
Ecosystem architecture: `/home/worsch/the-custodian/docs/hub-ecosystem-architecture.md`
|
||||
|
||||
Runtime packaging is fixed by `docs/adr/ADR-0001-runtime-packaging.md`: the
|
||||
wheel remains importable, while this repository will also own the primary OCI
|
||||
image. API, MCP, and migration workloads may run separately from that same
|
||||
image. Core-hub is not a permanent thin host.
|
||||
|
||||
`hub-core` is being extracted from the standalone State Hub repository as part
|
||||
of `CUST-WP-0025`. The initial package slice contains only the generic database
|
||||
models and schemas that can move without importing dev-hub concepts such as
|
||||
|
|
@ -23,6 +29,45 @@ Source boundary notes live in:
|
|||
/home/worsch/the-custodian/docs/hub-core-extraction-boundary.md
|
||||
```
|
||||
|
||||
## Extension contract
|
||||
|
||||
The wheel includes `helixforge.hub-extension` 0.1.0 under
|
||||
`hub_core.contracts`. Use `extension_contract_root()` to locate the packaged
|
||||
descriptor and manifest schemas, event catalog schema and seed, named-port
|
||||
OpenAPI fragments, ops-hub fixture, and compatibility matrix.
|
||||
|
||||
```python
|
||||
from hub_core.contracts import CONTRACT_VERSION, extension_contract_root
|
||||
|
||||
contract_root = extension_contract_root()
|
||||
```
|
||||
|
||||
## Runtime scaffold
|
||||
|
||||
Install the runtime extra and start the API, MCP, or migration process through
|
||||
the shared console entrypoint:
|
||||
|
||||
```bash
|
||||
uv sync --extra runtime
|
||||
hub-core api
|
||||
hub-core mcp --api-base http://127.0.0.1:8010
|
||||
hub-core migrate head --database-url postgresql+asyncpg://...
|
||||
```
|
||||
|
||||
The initial runtime exposes registry, messaging, progress-event,
|
||||
interaction-event, and projection-query ports. Its included memory backend is
|
||||
for local/conformance use and fails production readiness unless explicitly
|
||||
enabled. See `docs/runtime.md`.
|
||||
|
||||
The reusable Tier 2/3 scaffold is documented in `docs/conformance.md` and runs
|
||||
against any compatible HTTP target with
|
||||
`hub-core conformance --base-url <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.
|
||||
|
|
|
|||
60
SCOPE.md
60
SCOPE.md
|
|
@ -1,22 +1,25 @@
|
|||
# SCOPE — hub-core
|
||||
|
||||
**Updated:** 2026-06-16
|
||||
**Updated:** 2026-08-21
|
||||
|
||||
---
|
||||
|
||||
## One-liner
|
||||
|
||||
Reusable Python package of FastAPI router factories, SQLAlchemy models, Pydantic
|
||||
schemas, MCP tooling, and migration scaffolds for FOS hub services.
|
||||
Importable Python package and target runtime for versioned HelixForge hub
|
||||
contracts, ports, projections, MCP tooling, and compatibility surfaces.
|
||||
|
||||
---
|
||||
|
||||
## Core idea
|
||||
|
||||
`hub-core` is a **library boundary** between shared hub infrastructure and
|
||||
host-specific hub implementations. Host repositories depend on `hub-core` as an
|
||||
editable or published package; they run the actual HTTP/MCP service, own
|
||||
deployment, and add domain tables and workflows on top.
|
||||
`hub-core` preserves its **library boundary** while growing into the surviving
|
||||
hub framework and runtime defined by `SHR-ARCH-HUB-0001`. Domain hubs depend on
|
||||
its versioned contracts and ports while retaining their own data and APIs.
|
||||
Repository work remains authoritative in Git and is reached through
|
||||
repo-manager ports. `ADR-0001` selects a primary OCI image from this repository,
|
||||
with API, MCP, and migration processes sharing the image and contract version;
|
||||
the Python package remains independently importable.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -31,6 +34,10 @@ deployment, and add domain tables and workflows on top.
|
|||
- **Package metadata** — `pyproject.toml`, hatchling wheel build
|
||||
- **Capability registry scaffold** — `registry/` per helix_forge federation
|
||||
contract (entries added when reusable behaviors are registered)
|
||||
- **Hub-extension contracts and conformance** — descriptors, manifests, event
|
||||
catalogs, port APIs, fixtures, and compatibility matrices
|
||||
- **Surviving runtime surfaces** — registry, messaging, progress and
|
||||
interaction events, projections, telemetry, `/api/v2` compatibility, and MCP
|
||||
- **Documentation** — `README.md`, `INTENT.md`, `SCOPE.md`, pointer to
|
||||
extraction boundary in `the-custodian`
|
||||
|
||||
|
|
@ -38,13 +45,13 @@ deployment, and add domain tables and workflows on top.
|
|||
|
||||
## Out of scope
|
||||
|
||||
- Long-running hub service, Docker image, or production URL for hub-core itself
|
||||
- Dev-hub tables: topics, workplans, tasks, decisions, dependencies, SBOM,
|
||||
token accounting, kaizen agents
|
||||
- Repository-owned work records, Git write authority, and consistency logic
|
||||
- Domain-specific business tables and APIs belonging to domain hubs
|
||||
- State Hub dashboard UI, consistency sync scripts, and workplan file authority
|
||||
- Custodian canon content and constitution maintenance
|
||||
- Plaintext secrets, environment-specific connection strings committed to git
|
||||
- Replacing or wrapping non-hub application domains (feature-control, reuse-surface, etc.)
|
||||
- Authorization decisions, credential custody, and schedule execution
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -58,6 +65,10 @@ After the CUST-WP-0025 first slice (2026-06-06 — 2026-06-07):
|
|||
| Import core models and schemas | `hub_core.models`, `hub_core.schemas` |
|
||||
| Mount generic routers in a host FastAPI app | `hub_core.routers.create_*_router` |
|
||||
| Run generic MCP tools via `HubCoreMCPServer` | `hub_core.mcp` |
|
||||
| Load versioned hub-extension contracts from the wheel | `hub_core.contracts.extension_contract_root()` |
|
||||
| Run the minimal named-port HTTP runtime | `hub-core api` / `hub_core.runtime.create_app` |
|
||||
| Run API, MCP, and packaged migration processes | `hub-core api`, `hub-core mcp`, `hub-core migrate` |
|
||||
| Build the primary locked non-root OCI image | `make container-build` |
|
||||
| Apply core-schema migration template | `hub_core/migrations/versions/0001_core_schema.py` |
|
||||
| Adopt shared slug/pagination/path utilities | `hub_core.utils` |
|
||||
| Expose risk/alert progress read views | `/progress/risks`, `/progress/alerts` patterns |
|
||||
|
|
@ -74,11 +85,10 @@ python3 -m venv .venv && .venv/bin/pip install -e .
|
|||
## What is not possible yet
|
||||
|
||||
- **Published PyPI package** — consumed via editable path or private index only
|
||||
- **Standalone `hub-core serve`** — no CLI entrypoint; hosts own `uvicorn`
|
||||
- **Production durable port store** — T04 ships only the fail-closed ephemeral conformance backend
|
||||
- **Complete State Hub decoupling** — dev-hub routes and models still live in `state-hub`
|
||||
- **ops-hub / fin-hub adoption** — planned; not verified in this repo
|
||||
- **Capability registry entries** — scaffold only (`capabilities: []`); no registered reusable behaviors yet
|
||||
- **Gitea federation publish** — repo not yet on Gitea; blocks T01 in reuse-surface WP-0015
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -86,13 +96,15 @@ python3 -m venv .venv && .venv/bin/pip install -e .
|
|||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Package version | `0.1.0` (`hub_core.__version__`) |
|
||||
| Package version | `0.2.0` (`hub_core.__version__`) |
|
||||
| Python | `>=3.12` |
|
||||
| Dependencies | FastAPI, FastMCP, SQLAlchemy, Pydantic, httpx |
|
||||
| Dependencies | FastAPI, FastMCP, SQLAlchemy, Pydantic, httpx, JSON Schema; runtime extra adds ASGI/PostgreSQL/Alembic |
|
||||
| Tests | pytest under `tests/` |
|
||||
| Registry | Empty capability index; federation scaffold present |
|
||||
| Primary consumer | `state-hub` (editable dependency, router/schema import in progress) |
|
||||
| Extraction workplan | `CUST-WP-0025` (custodian domain) |
|
||||
| Target architecture | `SHR-ARCH-IA-0001` + `SHR-ARCH-HUB-0001` |
|
||||
| Runtime workplan | `HUB-WP-0004` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -109,6 +121,8 @@ hub-core/
|
|||
│ ├── schemas/
|
||||
│ ├── routers/
|
||||
│ ├── mcp/
|
||||
│ ├── runtime/
|
||||
│ ├── contracts/
|
||||
│ ├── migrations/
|
||||
│ ├── utils/
|
||||
│ ├── database.py
|
||||
|
|
@ -125,8 +139,9 @@ hub-core/
|
|||
|
||||
| Repo | Boundary |
|
||||
|---|---|
|
||||
| `state-hub` | Primary host — mounts router factories and MCP composition; owns workplans/tasks |
|
||||
| `core-hub` | Secondary consumer — imports utils/schemas; owns `/api/v2` framework tables locally |
|
||||
| `state-hub` | Temporary compatibility source; no new permanent authorities |
|
||||
| `core-hub` | `/api/v2` runtime and route source to absorb, then archive |
|
||||
| `repo-manager` | Owns repository representation, work index, consistency, and governed Git mutations |
|
||||
| `the-custodian` | Owns ecosystem architecture (`hub-ecosystem-architecture.md`) and extraction boundary |
|
||||
| `reuse-surface` | Federation hub for capability indexes; not a runtime dependency of hub-core |
|
||||
| `ops-hub` | Consumer of core-hub `/api/v2`; operations tables stay local |
|
||||
|
|
@ -135,19 +150,18 @@ hub-core/
|
|||
|
||||
## Workplan convention
|
||||
|
||||
Hub-core extraction and package work is tracked under **custodian** workplans
|
||||
(for example `CUST-WP-0025`). Host adoption milestones are tracked in
|
||||
`state-hub` workplans (for example `CUST-WP-0048`).
|
||||
|
||||
When hub-core gains repo-local workplans, prefer a stable prefix agreed with
|
||||
custodian operators (for example `HUBCORE-WP-####`).
|
||||
Repo-local work uses the `HUB-WP-####` prefix. Cross-repository consolidation
|
||||
is coordinated by `prj-state-hub-retirement`; owner repositories keep their
|
||||
own workplan files authoritative.
|
||||
|
||||
---
|
||||
|
||||
## Getting oriented
|
||||
|
||||
- Product intent: `INTENT.md`
|
||||
- Target architecture: `/home/worsch/prj-state-hub-retirement/architecture/hub-extension-architecture_v0.1.md`
|
||||
- Information model: `/home/worsch/prj-state-hub-retirement/architecture/information-model_v0.1.md`
|
||||
- Extraction boundary: `/home/worsch/the-custodian/docs/hub-core-extraction-boundary.md`
|
||||
- Package entry: `hub_core/__init__.py`, `hub_core/routers/__init__.py`
|
||||
- Consumer example: `/home/worsch/state-hub` (editable `hub-core` dependency)
|
||||
- Federation registry: `registry/README.md` (reuse-surface contract)
|
||||
- Federation registry: `registry/README.md` (reuse-surface contract)
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
103
docs/adr/ADR-0001-runtime-packaging.md
Normal file
103
docs/adr/ADR-0001-runtime-packaging.md
Normal 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
44
docs/conformance.md
Normal 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.
|
||||
307
docs/core-hub-absorption-plan.md
Normal file
307
docs/core-hub-absorption-plan.md
Normal 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 S1–S5 are in candidate state:
|
||||
|
||||
1. Freeze all remaining Core Hub writes and capture a final encrypted backup,
|
||||
table counts, content hashes, schema revision, image digest, and correlation
|
||||
ID.
|
||||
2. Import any remaining deltas and require zero unexplained count/hash drift.
|
||||
3. Run hub-core conformance, the Core Hub deployed smoke, ops-hub bootstrap,
|
||||
and activity-core resolver/write-readback against the candidate image.
|
||||
4. With explicit operator approval, point `hub.coulomb.social` entirely at
|
||||
hub-core and set every Core Hub write group empty.
|
||||
5. Observe at least seven consecutive days with no unexplained 5xx, no failed
|
||||
consumer gate, no data drift, and no normal request to Core Hub. Record any
|
||||
exception with an owner and expiry.
|
||||
6. Exercise rollback once during rehearsal. After the stabilization window and
|
||||
explicit retirement approval, scale Core Hub to zero while retaining its
|
||||
immutable image, database backup, deployment revision, and route-map
|
||||
rollback instructions until the agreed expiry.
|
||||
7. Move the final OpenAPI/contracts/smoke fixtures into hub-core history,
|
||||
record residual owners, archive the `core-hub` repository read-only, and
|
||||
update the deployment package identity.
|
||||
|
||||
## Evidence required for every durable slice
|
||||
|
||||
| Evidence | Pass condition |
|
||||
| --- | --- |
|
||||
| Source identity | Core Hub image/source revision and schema revision recorded |
|
||||
| Target identity | hub-core wheel/image version, revision, and contract version recorded |
|
||||
| Backup | Encrypted location and checksum recorded; no secret value logged |
|
||||
| Counts | Per-table source/import/target counts with zero unexplained delta |
|
||||
| Content | Order-independent canonical row hashes match after documented transforms |
|
||||
| Provenance | Source table/ID/revision, import bundle hash, and target record IDs queryable |
|
||||
| Contract | Static OpenAPI diff has no unexplained removed route/status/schema behavior |
|
||||
| Authorization | Missing/invalid/revoked deny; allowed roles pass; dependency outage denies |
|
||||
| Failure mode | Database, policy/auth, and candidate unavailability behavior recorded |
|
||||
| Consumers | Owned ops-hub and activity-core gates pass against the candidate |
|
||||
| Rollback | Reverse delta export/import and route-map reversal exercised |
|
||||
|
||||
An unexplained row, response, authorization, or provenance mismatch fails the
|
||||
slice. A waiver requires a human progress note and a live residual task.
|
||||
|
||||
## Smoke and change ownership
|
||||
|
||||
| Surface | Owner during absorption |
|
||||
| --- | --- |
|
||||
| Hub-core unit, contract, conformance, migration, and image tests | `hub-core` |
|
||||
| Legacy OpenAPI and deployed-smoke oracle | `core-hub` until archive, then historical fixture in `hub-core` |
|
||||
| ops-hub public/bootstrap gate | `ops-hub` |
|
||||
| activity-core resolver and interaction write/readback | `activity-core` |
|
||||
| Helm render, policy, secret projection, live verification, traffic map, rollback | rapp package (currently `rapp-core-hub`) + platform owners |
|
||||
| Production writer flip, rollback, scale-to-zero, archive | operator approval required |
|
||||
|
||||
## Rollback procedure
|
||||
|
||||
For a failed candidate slice:
|
||||
|
||||
1. Stop candidate writes for the affected group; do not enable legacy writes
|
||||
yet.
|
||||
2. Export the candidate-only delta with IDs and hashes, validate it against the
|
||||
reverse adapter, and import it into the retained legacy schema.
|
||||
3. Compare counts/content and run the legacy group smoke.
|
||||
4. Route the group to Core Hub, enable only its legacy writer, and confirm the
|
||||
candidate writer remains disabled.
|
||||
5. Record the failure correlation ID, data interval, revisions, evidence, and
|
||||
next owner. Do not continue to the next slice.
|
||||
|
||||
If candidate data cannot be replayed safely, keep both writers disabled and
|
||||
invoke the operator recovery path. Availability pressure does not authorize
|
||||
discarding acknowledged writes or enabling concurrent writers.
|
||||
|
||||
## Completion criteria
|
||||
|
||||
`HUB-WP-0004-T06` is complete when this plan is linked to CORE-WP-0010 and its
|
||||
handoff is recorded. HUB-WP-0004 may then finish; it does not claim production
|
||||
absorption.
|
||||
|
||||
`CORE-WP-0010` completes only when all slices have evidence, production is on
|
||||
hub-core, the stabilization and rollback gates close, residuals have live
|
||||
owners, the deployment package names the surviving runtime, and Core Hub is
|
||||
archived read-only.
|
||||
88
docs/runtime.md
Normal file
88
docs/runtime.md
Normal 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`.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""Reusable primitives for FOS hub services."""
|
||||
"""Contracts, reusable primitives, and runtime surfaces for HelixForge hubs."""
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
|
|
|
|||
15
hub_core/conformance/__init__.py
Normal file
15
hub_core/conformance/__init__.py
Normal 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",
|
||||
]
|
||||
354
hub_core/conformance/harness.py
Normal file
354
hub_core/conformance/harness.py
Normal 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"))
|
||||
16
hub_core/contracts/__init__.py
Normal file
16
hub_core/contracts/__init__.py
Normal 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"]
|
||||
1
hub_core/contracts/helixforge_hub_extension/__init__.py
Normal file
1
hub_core/contracts/helixforge_hub_extension/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Versioned ``helixforge.hub-extension`` contract packages."""
|
||||
26
hub_core/contracts/helixforge_hub_extension/v0_1_0/README.md
Normal file
26
hub_core/contracts/helixforge_hub_extension/v0_1_0/README.md
Normal 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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Contract artifacts for ``helixforge.hub-extension`` version 0.1.0."""
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
hub_core/runtime/__init__.py
Normal file
21
hub_core/runtime/__init__.py
Normal 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
54
hub_core/runtime/app.py
Normal 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
100
hub_core/runtime/cli.py
Normal 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://")
|
||||
59
hub_core/runtime/config.py
Normal file
59
hub_core/runtime/config.py
Normal 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
|
||||
)
|
||||
72
hub_core/runtime/models.py
Normal file
72
hub_core/runtime/models.py
Normal 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
127
hub_core/runtime/ports.py
Normal 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
179
hub_core/runtime/store.py
Normal 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]
|
||||
48
hub_core/runtime/validation.py
Normal file
48
hub_core/runtime/validation.py
Normal 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())
|
||||
|
|
@ -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
68
tests/test_conformance.py
Normal 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
152
tests/test_contracts.py
Normal 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
195
tests/test_runtime.py
Normal 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
304
uv.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ domain: inter_hub
|
|||
repo: hub-core
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: inter_hub
|
||||
topic_slug: custodian
|
||||
created: "2026-06-22"
|
||||
updated: "2026-06-22"
|
||||
state_hub_workstream_id: "439b559b-fcb7-4d21-b831-cfc9c6bbc1a0"
|
||||
|
|
@ -82,4 +82,4 @@ After T01–T02 land (or are explicitly deferred with documented seams):
|
|||
|
||||
Completed 2026-06-22: hub-core 27 tests pass; state-hub capability tests pass
|
||||
(20/20) and full suite 352/353 (one pre-existing health-route flake).
|
||||
Extraction boundary updated.
|
||||
Extraction boundary updated.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue