canned-prompts/workplans/CANP-WP-0006-hosted-registry-service.md

295 lines
13 KiB
Markdown
Raw Normal View History

Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
---
id: CANP-WP-0006
type: workplan
title: "Hosted registry and index service"
domain: agents
repo: canned-prompts
CANP-WP-0006 T06: container image and smoke checks Two-stage python:3.12-slim build with no toolchain in the runtime layer, running non-root (uid 10001) and writing nothing to disk — its state is the database. reference/ is a real build input, because the service delegates validation to it so the two cannot disagree about what a valid package is. Migrations deliberately do not run at start-up. A schema change is a deployment step with its own rollback, not something that races between replicas. tools/smoke.py asserts what can be known about a running service: liveness, readiness, fleet health shape, migration revision, and that the index and registry are queryable. stdlib only, so it runs inside the runtime image; non-zero exit, so a deployment gate can call it directly. Verified by running it, not by inspection: the container starts, all six checks pass against it, the reference CLI installs a package from it over HTTP, and the checks fail correctly against a wrong --expect-migration — so migration-at-head is a real check rather than a decorative one. Without --expect-migration the check can only confirm the schema is stamped at all, and says so rather than implying it verified the head. Cluster-level checks a rapp contract also names — NetworkPolicies present, external secrets ready, private-Service-only, live image digest match — are properties of the deployment and belong to rapp-canned-prompts. The digest rapp.yaml would pin does not exist yet. The image was built locally and verified, but never pushed; that digest exists only once the image is published to the fleet registry, which needs credentials and is outward-facing enough not to do unasked. The follow-on sequence for rapp-canned-prompts is recorded in the workplan. CANP-WP-0006 is finished. Service tests 33, reference 105. 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 21:25:48 +02:00
status: finished
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
owner: codex
topic_slug: practice
created: "2026-09-06"
updated: "2026-09-06"
state_hub_workstream_id: "70c069e9-7abe-569a-aee2-ffcd15e8970d"
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
---
# 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
```task
id: CANP-WP-0006-T01
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
status: done
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
priority: high
state_hub_task_id: "6e4178f0-a431-59de-8bd1-fb5b2de95302"
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
```
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.
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
**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.
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
## Tenant-keyed schema and first migration
```task
id: CANP-WP-0006-T02
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
status: done
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
priority: high
state_hub_task_id: "bc0e4a49-63b1-5fa1-9ec9-5ed0f505baa1"
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
```
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.
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
**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)`.
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
## Read API
```task
id: CANP-WP-0006-T03
CANP-WP-0006 T03: read API Search, versions, manifests, archives and the index, over HTTP. The route shape is the decision worth recording. Package ids contain `/`, so the obvious /packages/{id}/{version} is ambiguous under a greedy path parameter. Rather than invent an HTTP-specific identifier, the routes speak the format's own <registry>:<id>@<version> syntax and parse it — `:` and `@` are both legal in a path segment, and each route keeps a distinct prefix so greediness cannot swallow a neighbouring one. The API therefore exercises section 3.2's reference notation instead of working around it. A bare id present in more than one registry returns 409 with the candidates, never a guess. 409 rather than 300 because the request is answerable once the caller says which registry they meant. Omitting a version applies section 17.1's selector rules, so a prerelease is never chosen implicitly. Validation is delegated to reference/, installed into the service environment rather than reimplemented. One validator means the service and the CLI cannot disagree about what a valid package is; a service accepting something the CLI rejects would be the divergence this project exists to prevent. Importing it is not changing it — reference/ stays the dependency-light conformance witness. Storage keeps the format's distinctions: an immutable package version, its files as content rather than parsed rows, and an index entry recording arrival. Only reserved paths and manifest-referenced files are stored (section 2), and a re-publish of identical content is accepted while different content under the same id@version is a conflict (section 17). Handles a real test-vs-production difference: SQLite autoincrements INTEGER PRIMARY KEY only, never BIGINT, so the SQLite-backed tests could not insert a row. BigInteger().with_variant(Integer, "sqlite") keeps BIGINT on PostgreSQL while letting the tests exercise the same models and migration. Verified live against a seeded store holding this repo's examples and four helix-forge prompt packages. Service tests 11 -> 22. 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 20:23:25 +02:00
status: done
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
priority: high
state_hub_task_id: "0ae11e65-68b0-543c-b8fa-f3188a4d239a"
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
```
`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.
CANP-WP-0006 T03: read API Search, versions, manifests, archives and the index, over HTTP. The route shape is the decision worth recording. Package ids contain `/`, so the obvious /packages/{id}/{version} is ambiguous under a greedy path parameter. Rather than invent an HTTP-specific identifier, the routes speak the format's own <registry>:<id>@<version> syntax and parse it — `:` and `@` are both legal in a path segment, and each route keeps a distinct prefix so greediness cannot swallow a neighbouring one. The API therefore exercises section 3.2's reference notation instead of working around it. A bare id present in more than one registry returns 409 with the candidates, never a guess. 409 rather than 300 because the request is answerable once the caller says which registry they meant. Omitting a version applies section 17.1's selector rules, so a prerelease is never chosen implicitly. Validation is delegated to reference/, installed into the service environment rather than reimplemented. One validator means the service and the CLI cannot disagree about what a valid package is; a service accepting something the CLI rejects would be the divergence this project exists to prevent. Importing it is not changing it — reference/ stays the dependency-light conformance witness. Storage keeps the format's distinctions: an immutable package version, its files as content rather than parsed rows, and an index entry recording arrival. Only reserved paths and manifest-referenced files are stored (section 2), and a re-publish of identical content is accepted while different content under the same id@version is a conflict (section 17). Handles a real test-vs-production difference: SQLite autoincrements INTEGER PRIMARY KEY only, never BIGINT, so the SQLite-backed tests could not insert a row. BigInteger().with_variant(Integer, "sqlite") keeps BIGINT on PostgreSQL while letting the tests exercise the same models and migration. Verified live against a seeded store holding this repo's examples and four helix-forge prompt packages. Service tests 11 -> 22. 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 20:23:25 +02:00
**Done**, with the route shape worth recording. Package ids contain `/`, so the
obvious `/packages/{id}/{version}` is ambiguous under a greedy path parameter.
Rather than invent an HTTP-specific identifier, the routes speak the format's
own `<registry>:<id>@<version>` syntax and parse it — `:` and `@` are both legal
in a path segment, each route keeps a distinct prefix, and the API therefore
tests § 3.2's reference notation rather than working around it.
Ambiguity returns **409 with the candidates**, not 300: the request is
answerable once the caller says which registry they meant. Omitting a version
applies § 17.1's selectors, so a prerelease is never chosen implicitly.
Validation is delegated to `reference/`, installed into the service
environment rather than reimplemented. One validator means the service and the
CLI cannot disagree about what a valid package is — a service accepting
something the CLI rejects would be exactly the divergence this project exists to
prevent. Importing it is not changing it; `reference/` stays the
dependency-light conformance witness.
**Test-vs-production difference found and handled.** SQLite autoincrements
`INTEGER PRIMARY KEY` only, never `BIGINT`, so the SQLite-backed tests could not
insert a row. `BigInteger().with_variant(Integer, "sqlite")` keeps `BIGINT` on
PostgreSQL, where this actually runs, while letting the tests exercise the same
models and the same migration.
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
## Publish API
```task
id: CANP-WP-0006-T04
CANP-WP-0006 T04: publish API POST /packages takes {registry, source, files}, where files is the same shape GET /archives returns — so an archive round-trips into a publish without translation and a mirror is a GET followed by a POST. A test verifies the round-trip by digest rather than asserting it. Validation is the reference implementation's, applied to the posted files materialized in a temporary directory. Only reserved paths and manifest-referenced files are stored (section 2), path traversal is refused, identical re-publishes are accepted, and different content under the same id@version is 409 (section 17). The identity mechanism, stated plainly rather than implied: a single shared bearer token proving the caller is the operator of this service. It is not per-publisher identity — every token holder is indistinguishable — and auth.py says so where someone might otherwise assume more. With no token configured the service is read-only. That is the correct default rather than an inconvenience: section 20.1 asks a registry to refuse publication into a closed namespace it does not consider the publisher to own, and an unauthenticated service considers nobody to own anything. Namespace claims live in namespace_claims (migration 0002) and are enforced here, which a filesystem registry cannot do at all — but only as precisely as the identity allows. A closed namespace is protected from anonymous callers; it cannot be attributed among several publishers. Per-publisher identity is deferred and is the main thing between this and a registry several people can publish to. Migration hygiene found while adding 0002: autogenerate proposed an ALTER COLUMN TYPE on package_files.package_version_id, because the foreign key's type was left to inference and compared as a variant against a reflected plain type. SQLite cannot alter a column type, so 0002 failed halfway — table created, revision unstamped, the partially-applied state that is worst to debug later. Fixed at the cause: the column is typed explicitly, and 0001 was corrected rather than patched over, which is legitimate only because it has never run outside this repo's tests. alembic check now reports no drift. Health tests now compute the expected migration head from the script directory instead of hardcoding it, so adding a migration cannot fail them spuriously. Service tests 22 -> 33; reference 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 20:30:00 +02:00
status: done
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
priority: high
state_hub_task_id: "929932b6-931b-5409-a648-b6814f07f0b4"
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
```
`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.
CANP-WP-0006 T04: publish API POST /packages takes {registry, source, files}, where files is the same shape GET /archives returns — so an archive round-trips into a publish without translation and a mirror is a GET followed by a POST. A test verifies the round-trip by digest rather than asserting it. Validation is the reference implementation's, applied to the posted files materialized in a temporary directory. Only reserved paths and manifest-referenced files are stored (section 2), path traversal is refused, identical re-publishes are accepted, and different content under the same id@version is 409 (section 17). The identity mechanism, stated plainly rather than implied: a single shared bearer token proving the caller is the operator of this service. It is not per-publisher identity — every token holder is indistinguishable — and auth.py says so where someone might otherwise assume more. With no token configured the service is read-only. That is the correct default rather than an inconvenience: section 20.1 asks a registry to refuse publication into a closed namespace it does not consider the publisher to own, and an unauthenticated service considers nobody to own anything. Namespace claims live in namespace_claims (migration 0002) and are enforced here, which a filesystem registry cannot do at all — but only as precisely as the identity allows. A closed namespace is protected from anonymous callers; it cannot be attributed among several publishers. Per-publisher identity is deferred and is the main thing between this and a registry several people can publish to. Migration hygiene found while adding 0002: autogenerate proposed an ALTER COLUMN TYPE on package_files.package_version_id, because the foreign key's type was left to inference and compared as a variant against a reflected plain type. SQLite cannot alter a column type, so 0002 failed halfway — table created, revision unstamped, the partially-applied state that is worst to debug later. Fixed at the cause: the column is typed explicitly, and 0001 was corrected rather than patched over, which is legitimate only because it has never run outside this repo's tests. alembic check now reports no drift. Health tests now compute the expected migration head from the script directory instead of hardcoding it, so adding a migration cannot fail them spuriously. Service tests 22 -> 33; reference 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 20:30:00 +02:00
**Done.** `POST /packages` takes the same `files` shape `GET /archives`
returns, so an archive round-trips into a publish without translation and a
mirror is a GET followed by a POST — verified by a test, not just asserted.
**The identity mechanism, stated plainly.** A single shared bearer token
proving the caller is *the operator of this service*. Not per-publisher
identity: every token holder is indistinguishable. With no token configured the
service is read-only, which is the right default rather than an inconvenience —
§ 20.1 asks a registry to refuse publication into a closed namespace it does not
consider the publisher to own, and an unauthenticated service considers nobody
to own anything.
Namespace claims live in `namespace_claims` (migration `0002`) and are enforced,
which a filesystem registry cannot do at all — but only as precisely as the
identity allows. A closed namespace is protected from anonymous callers; it
cannot be attributed among several publishers. Until per-publisher identity
exists, a claim's `owner` is documentation rather than an access decision, and
`auth.py` says so rather than letting the code imply more than it delivers.
**Handed forward:** per-publisher identity is the main thing between this and a
registry several people can publish to.
**Migration hygiene found while adding `0002`.** Autogenerate proposed an
`ALTER COLUMN TYPE` on `package_files.package_version_id`, because the foreign
key's type was left to inference and compared as a variant against a reflected
plain type. SQLite cannot alter a column type, so `0002` failed halfway,
leaving the table created and the revision unstamped — the partially-applied
state that is worst to debug later. Fixed at the cause: the column is typed
explicitly, and `0001` was corrected rather than patched over, which is
legitimate only because it has never run outside this repo's tests. `alembic
check` now reports no drift.
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
## HTTP registry in the reference CLI
```task
id: CANP-WP-0006-T05
CANP-WP-0006 T05: HTTP registries, and one finding --registry now accepts an http(s):// URL as well as a path, implemented with urllib so reference/ keeps PyYAML as its only dependency. This was the first real test of INTENT principle 10's claim that a hosted registry layers on without changing package semantics. Verified against the running service, almost everything survived the transport unaltered: identity and its ambiguity rules (a bare id in two registries returns 409 over HTTP just as it does locally), immutability of a published id@version (identical content accepted, changed content refused, version bump accepted), strict packaging, validation, and the index. A package published and then installed over HTTP was byte-identical to its source — diff -r clean — and its canonical-fidelity eval still passed after the round trip. One thing did not survive: a URL is not a registry. A filesystem registry IS one registry and section 20.1 names it from its directory; an HTTP service HOSTS SEVERAL behind one base URL. The address therefore cannot name the registry, so it must be named separately — --as when publishing, a qualified reference when installing. Recorded as section 20.4 rather than worked around silently in the client, because the gap is in the specification's list of registry kinds, not in the CLI. Publishing to an HTTP registry without --as fails with that explanation rather than guessing a registry name. Registry responses are treated as untrusted input (section 19): decode_files refuses path traversal, with a test. The wire shape round-trips binary content through base64, also tested. Reference tests 99 -> 105. 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 20:39:04 +02:00
status: done
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
priority: medium
state_hub_task_id: "8dacbb68-aaa2-5f91-942d-a0667f1f1fc7"
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
```
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.
CANP-WP-0006 T05: HTTP registries, and one finding --registry now accepts an http(s):// URL as well as a path, implemented with urllib so reference/ keeps PyYAML as its only dependency. This was the first real test of INTENT principle 10's claim that a hosted registry layers on without changing package semantics. Verified against the running service, almost everything survived the transport unaltered: identity and its ambiguity rules (a bare id in two registries returns 409 over HTTP just as it does locally), immutability of a published id@version (identical content accepted, changed content refused, version bump accepted), strict packaging, validation, and the index. A package published and then installed over HTTP was byte-identical to its source — diff -r clean — and its canonical-fidelity eval still passed after the round trip. One thing did not survive: a URL is not a registry. A filesystem registry IS one registry and section 20.1 names it from its directory; an HTTP service HOSTS SEVERAL behind one base URL. The address therefore cannot name the registry, so it must be named separately — --as when publishing, a qualified reference when installing. Recorded as section 20.4 rather than worked around silently in the client, because the gap is in the specification's list of registry kinds, not in the CLI. Publishing to an HTTP registry without --as fails with that explanation rather than guessing a registry name. Registry responses are treated as untrusted input (section 19): decode_files refuses path traversal, with a test. The wire shape round-trips binary content through base64, also tested. Reference tests 99 -> 105. 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 20:39:04 +02:00
**Done, and it produced exactly one finding.**
*What survived unaltered*, verified against the running service: identity and
its ambiguity rules (a bare id in two registries came back 409 over HTTP just as
it does locally), immutability of a published `<id>@<version>` (identical
content accepted, changed content refused, a version bump accepted), strict
packaging, validation, and the index. A package published and installed over
HTTP was **byte-identical** to its source — `diff -r` clean — and its
`canonical-fidelity` eval still passed after the round trip.
*What did not*: **a URL is not a registry.** A filesystem registry *is* one
registry and § 20.1 names it from its directory; an HTTP service *hosts
several* behind one base URL. So the address cannot name the registry, and it
has to be named separately — `--as` when publishing, a qualified reference when
installing. Recorded as § 20.4 rather than papered over in the client, because
the gap is in the specification's list of registry kinds, not in the CLI.
Implemented with `urllib` rather than a library, so `reference/` keeps PyYAML as
its only dependency. Registry responses are treated as untrusted input (§ 19):
`decode_files` refuses path traversal, with a test.
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
## Image and smoke contract
```task
id: CANP-WP-0006-T06
CANP-WP-0006 T06: container image and smoke checks Two-stage python:3.12-slim build with no toolchain in the runtime layer, running non-root (uid 10001) and writing nothing to disk — its state is the database. reference/ is a real build input, because the service delegates validation to it so the two cannot disagree about what a valid package is. Migrations deliberately do not run at start-up. A schema change is a deployment step with its own rollback, not something that races between replicas. tools/smoke.py asserts what can be known about a running service: liveness, readiness, fleet health shape, migration revision, and that the index and registry are queryable. stdlib only, so it runs inside the runtime image; non-zero exit, so a deployment gate can call it directly. Verified by running it, not by inspection: the container starts, all six checks pass against it, the reference CLI installs a package from it over HTTP, and the checks fail correctly against a wrong --expect-migration — so migration-at-head is a real check rather than a decorative one. Without --expect-migration the check can only confirm the schema is stamped at all, and says so rather than implying it verified the head. Cluster-level checks a rapp contract also names — NetworkPolicies present, external secrets ready, private-Service-only, live image digest match — are properties of the deployment and belong to rapp-canned-prompts. The digest rapp.yaml would pin does not exist yet. The image was built locally and verified, but never pushed; that digest exists only once the image is published to the fleet registry, which needs credentials and is outward-facing enough not to do unasked. The follow-on sequence for rapp-canned-prompts is recorded in the workplan. CANP-WP-0006 is finished. Service tests 33, reference 105. 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 21:25:48 +02:00
status: done
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
priority: medium
state_hub_task_id: "3c008ebe-bf07-5cee-8678-7b1ed27283ef"
Register CANP-WP-0006: hosted registry and index service Toward a production deployment as rapp-canned-prompts on railiance01. A rapp repo packages a deployable its ownership_repo already produces — rapp.yaml pins upstream_components.version to a container digest. canned-prompts has no deployable: a CLI with no HTTP surface, no image, no database. A rapp pointing at nothing is a stub that tells the fleet's tooling a deployment exists when none does. So the service comes first, and rapp-canned-prompts is created once there is a digest to pin. Decisions recorded: hosted registry plus index as the v1 surface, the service living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the sbom-nexus shape), and deployment as a platform service whose data is nevertheless tenant-keyed from the first migration per business-app-service-contract section 1.3 — cheap now, a rewrite later. Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic, PostgreSQL. The deployment guide additionally requires unauthenticated /healthz and /readyz. INTENT's deliberate boundary still holds: the service hosts packages and does not become an agent runtime. reference/ stays untouched and dependency-light as the format's conformance witness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM EOF Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
```
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.
CANP-WP-0006 T06: container image and smoke checks Two-stage python:3.12-slim build with no toolchain in the runtime layer, running non-root (uid 10001) and writing nothing to disk — its state is the database. reference/ is a real build input, because the service delegates validation to it so the two cannot disagree about what a valid package is. Migrations deliberately do not run at start-up. A schema change is a deployment step with its own rollback, not something that races between replicas. tools/smoke.py asserts what can be known about a running service: liveness, readiness, fleet health shape, migration revision, and that the index and registry are queryable. stdlib only, so it runs inside the runtime image; non-zero exit, so a deployment gate can call it directly. Verified by running it, not by inspection: the container starts, all six checks pass against it, the reference CLI installs a package from it over HTTP, and the checks fail correctly against a wrong --expect-migration — so migration-at-head is a real check rather than a decorative one. Without --expect-migration the check can only confirm the schema is stamped at all, and says so rather than implying it verified the head. Cluster-level checks a rapp contract also names — NetworkPolicies present, external secrets ready, private-Service-only, live image digest match — are properties of the deployment and belong to rapp-canned-prompts. The digest rapp.yaml would pin does not exist yet. The image was built locally and verified, but never pushed; that digest exists only once the image is published to the fleet registry, which needs credentials and is outward-facing enough not to do unasked. The follow-on sequence for rapp-canned-prompts is recorded in the workplan. CANP-WP-0006 is finished. Service tests 33, reference 105. 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 21:25:48 +02:00
**Done, with one honest limit.**
The image builds and was verified by running it: a two-stage `python:3.12-slim`
build with no toolchain in the runtime layer, non-root (uid 10001), writing
nothing to disk. All six smoke checks passed against the running container, and
the reference CLI installed a package from it over HTTP.
Migrations deliberately do **not** run at start-up. A schema change is a
deployment step with its own rollback, not something that races between
replicas.
`tools/smoke.py` covers what can be known about a *running service* — liveness,
readiness, fleet health shape, migration revision, index and registry
queryable. stdlib only, so it runs inside the runtime image; non-zero exit, so
a deployment gate can call it. Verified in both directions: it passes against
the container and fails against a wrong `--expect-migration`, so
`migration-at-head` is a real check rather than a decorative one.
Cluster-level checks a rapp contract also names — NetworkPolicies present,
external secrets ready, private-Service-only, live image digest match — are
properties of the deployment and belong to `rapp-canned-prompts`.
**The digest does not exist yet.** The image was built locally
(`sha256:4878b208…`, 76 MB) but never pushed. `rapp.yaml` pins
`upstream_components.version` to a digest from the fleet's registry, which
exists only once the image is published — an operator action needing registry
credentials, and outward-facing enough that it is not mine to take unasked.
## Follow-on: rapp-canned-prompts
Not a task in this workplan; recorded so the sequence is not lost.
1. Publish the image to the fleet registry and capture its digest.
2. Create `rapp-canned-prompts` with `ownership_repo: canned-prompts`,
`readiness_state: draft`, and that digest in `upstream_components`.
3. Add the deployment-level smoke checks, calling `tools/smoke.py` for the
service-level half.
4. Decide the PostgreSQL binding with `rapp-postgres` and the credential
broker, per the shape `rapp-sbom-nexus` uses.