canned-prompts/workplans/CANP-WP-0006-hosted-registry-service.md
tegwick ae52931be5 CANP-WP-0006 T01-T02: service skeleton and tenant-keyed schema
The foundation of the hosted registry, in canned-prompts so rapp.yaml gets
ownership_repo: canned-prompts — the sbom-nexus shape, where product ownership
stays out of the operations repo.

Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic,
PostgreSQL, in service/ with its own environment. reference/ is deliberately
untouched: it is the format's conformance witness and stays dependency-light,
and the service is a separate consumer of the same package semantics.

CANNED_PROMPTS_DATABASE_URL has no default. A service that silently falls back
to a local database when its real one is misconfigured is worse than one that
refuses to start.

Health surface per RailianceAppDeploymentGuide.md: unauthenticated /healthz and
/readyz, plus /state/health for fleet consistency. /healthz deliberately checks
nothing beyond the process being up, so a database blip does not restart pods;
/readyz asks the database something it can fail to answer.

Migration 0001 creates package_versions, package_files and index_entries, every
one carrying a tenant key per business-app-service-contract section 1.3 — the
service is single-tenant today, and the key is present so a later consolidation
is a data copy rather than a rewrite. A test asserts every table in the metadata
is tenant-keyed, so adding an unkeyed table fails the suite rather than being
discovered at consolidation time. Uniqueness is (tenant, registry, package_id,
version): registry-scoped because identity is, tenant-scoped so two tenants may
hold the same id.

The schema keeps the format's three things distinct — an immutable package
version, its files as content rather than parsed rows, and an index entry
recording how a version arrived here.

Fixes a bug its own test caught: check_readiness first caught every failure in
one except and reported "database unreachable", so an unmigrated but perfectly
reachable database sent an operator to credentials and networking when the fix
was alembic upgrade. Connectivity and schema are now checked separately.

Service tests 11 passing; reference tests unaffected at 99.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:57:47 +02:00

6.9 KiB

id type title domain repo status owner topic_slug created updated state_hub_workstream_id
CANP-WP-0006 workplan Hosted registry and index service agents canned-prompts active codex practice 2026-09-06 2026-09-06 70c069e9-7abe-569a-aee2-ffcd15e8970d

Hosted registry and index service

Toward a production deployment on railiance01 as rapp-canned-prompts.

Sequencing finding. A rapp-* repo packages and operates a deployable its ownership_repo already produces — declarations/rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI module with no HTTP surface, no image, no database. A rapp whose upstream_components points at nothing is a stub that tells the fleet's tooling a deployment exists when none does. The service comes first.

Decisions (operator, 2026-09-06):

  • Hosted registry plus index is the v1 surface. § 20 already permits an HTTP registry and § 20.3's index is the query surface, so this validates INTENT principle 10's "registry-ready" claim rather than inventing a new concept.
  • The service lives in canned-prompts, so rapp.yaml gets ownership_repo: canned-prompts — the sbom-nexus shape, where product ownership stays out of the operations repo.
  • Platform service, tenant-keyed regardless. Deployed as a platform service like sbom-nexus, but all owned data is keyed by tenant from the first migration. business-app-service-contract_v0.1 § 1.3 makes that mandatory so a later consolidation is a data-copy rather than a rewrite; a collaborative prompting platform will need it, and retrofitting is the expensive path.
  • Service first, rapp when there is a digest to pin.

Stack, matching state-hub and sbom-nexus: FastAPI, uvicorn, SQLAlchemy, Alembic, Pydantic, PostgreSQL. RailianceAppDeploymentGuide.md additionally requires unauthenticated /healthz and /readyz.

Boundary. The service hosts packages; it does not become an agent runtime. INTENT.md's deliberate boundary still holds — no model execution, no orchestration. Rendering stays deterministic, and derive stays a declaration the service does not satisfy.

Service skeleton and health surface

id: CANP-WP-0006-T01
status: done
priority: high
state_hub_task_id: "6e4178f0-a431-59de-8bd1-fb5b2de95302"

A service/ package inside this repo: FastAPI application factory, settings via pydantic-settings, and the health surface the deployment guide requires — unauthenticated /healthz and /readyz, plus /state/health for consistency with state-hub and sbom-nexus.

/readyz must actually check the database, not return 200 unconditionally. A readiness probe that cannot fail is a liveness probe with a misleading name.

Keep reference/ untouched. It is the format's conformance witness and must stay dependency-light; the service is a separate consumer of the same package semantics.

Done. service/ with a FastAPI factory, pydantic-settings config, and the three endpoints. CANNED_PROMPTS_DATABASE_URL has no default, so a misconfigured service refuses to start rather than quietly using a local database.

Bug found by its own test. check_readiness first caught every failure in one except and reported "database unreachable". An unmigrated but perfectly reachable database therefore reported a connection problem — sending an operator to credentials and networking when the fix was alembic upgrade. Connectivity and schema are now checked separately.

Tenant-keyed schema and first migration

id: CANP-WP-0006-T02
status: done
priority: high
state_hub_task_id: "bc0e4a49-63b1-5fa1-9ec9-5ed0f505baa1"

Alembic migration 0001 creating the store. Every table holding owned data carries a tenant key from this migration onward — packages, versions, and index entries — per the service contract § 1.3. No business logic may assume it is the only tenant (§ 1.4).

Model the three things the format already distinguishes, and do not conflate them:

  • package version — immutable content addressed by <id>@<version> (§ 17), scoped to a registry because identity is registry-scoped (§ 3.2);
  • index entry — how a version arrived in this store (§ 20.3): source, method, included_at which is never overwritten, last_seen_at;
  • manifest fields worth indexing for discovery — name, summary, tags, license, type.

Store package files as content, not as rows per file.

Done. Migration 0001 creates package_versions, package_files and index_entries; a test asserts every table in the metadata carries tenant, so adding an unkeyed table fails the suite rather than being noticed at consolidation time. Uniqueness is (tenant, registry, package_id, version).

Read API

id: CANP-WP-0006-T03
status: todo
priority: high
state_hub_task_id: "0ae11e65-68b0-543c-b8fa-f3188a4d239a"

GET /packages (search over id, name, summary, tags), GET /packages/{id} (versions), GET /packages/{id}/{version} (manifest), GET /packages/{id}/{version}/archive (the package files), and GET /index (§ 20.3 entries).

Ambiguity is reported, never guessed (§ 3.2): a bare id matching packages from more than one registry returns the candidates, not a choice.

Publish API

id: CANP-WP-0006-T04
status: todo
priority: high
state_hub_task_id: "929932b6-931b-5409-a648-b6814f07f0b4"

POST /packages accepting a package, validating it with the same rules as the CLI, and refusing a re-publish of an existing <id>@<version> with different content (§ 17). Strict packaging applies: reserved paths and manifest-referenced files only (§ 2).

Namespace policy (§ 20.1) is enforced here rather than advised, because unlike a filesystem registry a service can authenticate a publisher. Note what identity mechanism it uses; if there is none yet, say so and refuse writes rather than accepting anonymous publishes into a closed namespace.

HTTP registry in the reference CLI

id: CANP-WP-0006-T05
status: todo
priority: medium
state_hub_task_id: "8dacbb68-aaa2-5f91-942d-a0667f1f1fc7"

Teach --registry to accept an https:// URL, so publish and install work against the service. § 20 already allows an HTTP registry; this is the first implementation of one, and the first real test of whether package semantics survive a transport change unmodified — which is what INTENT principle 10 claims.

If they do not survive it, that is a finding about the format, not a bug to paper over in the client.

Image and smoke contract

id: CANP-WP-0006-T06
status: todo
priority: medium
state_hub_task_id: "3c008ebe-bf07-5cee-8678-7b1ed27283ef"

Dockerfile and a published image, plus the checks a rapp smoke contract will assert: health ok, migration at head, private service only, image digest match.

Completing this produces the digest that rapp-canned-prompts needs to pin, at which point that repo can be created against something real.