# canned-prompts service The hosted registry and index (`CANP-WP-0006`). Product ownership lives here alongside the specification and the reference CLI; deployment and operation will belong to `rapp-canned-prompts` once there is an image digest to pin. This service **hosts** packages. It does not execute them — `INTENT.md`'s deliberate boundary holds, so rendering stays deterministic and a `derive` default remains a declaration the service does not satisfy. `reference/` is deliberately untouched by this. It is the format's conformance witness and stays dependency-light; the service is a separate consumer of the same package semantics. ## Stack FastAPI, SQLAlchemy, Alembic, PostgreSQL — matching `state-hub` and `sbom-nexus`. ```bash cd service uv venv && uv pip install -e ".[dev]" .venv/bin/python -m pytest -q export CANNED_PROMPTS_DATABASE_URL=postgresql+psycopg://... .venv/bin/alembic upgrade head .venv/bin/uvicorn canned_prompts_service.api:create_app --factory ``` `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 `RailianceAppDeploymentGuide.md` requires unauthenticated `/healthz` and `/readyz`; `/state/health` matches the rest of the fleet. | Endpoint | Answers | Fails when | |---|---|---| | `/healthz` | is the process up | never — deliberately checks nothing else, so a database blip does not restart pods | | `/readyz` | can it serve | no database configured, unreachable, or schema not migrated | | `/state/health` | fleet-shaped status | same as `/readyz`, reported as `degraded` | Connectivity and schema are checked separately. Both mean not-ready, but they send an operator to different places — credentials and network, or an unrun migration — so collapsing them into one message would send people to the wrong one. ## Tenancy Every table holding owned data carries a `tenant` key from migration `0001`, per `business-app-service-contract_v0.1` § 1.3. The service is deployed single-tenant today; the key is present so a later consolidation is a data copy rather than a rewrite, and no query may assume it is the only tenant (§ 1.4). Uniqueness is `(tenant, registry, package_id, version)` — registry-scoped because identity is (§ 3.2), and tenant-scoped so two tenants may legitimately hold the same id. ## Schema | Table | Holds | |---|---| | `package_versions` | one immutable `:@`, its manifest, and indexed discovery fields | | `package_files` | the package's files as content, not parsed into rows | | `index_entries` | how a version arrived here (§ 20.3): source, method, `included_at` never overwritten, `last_seen_at` | ## Read API Routes address packages with the format's own reference syntax (`:@`, § 3.2 and § 17) rather than a second one invented for HTTP. A package id contains `/`, so the reference is captured as a greedy path and parsed; each route keeps a distinct prefix so the greediness cannot swallow a neighbouring segment. | Route | Returns | |---|---| | `GET /packages?q=®istry=` | search over id, name, summary and tags | | `GET /packages/{id}` | the versions of an id | | `GET /packages/{id}@{version}` | one manifest | | `GET /archives/{id}@{version}` | the package's files, text or base64 | | `GET /index` | § 20.3 entries: source, method, `included_at`, `last_seen_at` | A bare id present in more than one registry returns **409 with the candidates**, never a guess (§ 3.2). 409 rather than 300 because the request is answerable — once the caller says which registry they meant. Asking for a package without naming a version applies § 17.1's selector rules, so a prerelease is never chosen implicitly. ## 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. 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 (§ 2); path traversal is refused. Re-publishing identical content is accepted, different content under the same `@` is 409 (§ 17). ### What identity means here A single shared bearer token (`CANNED_PROMPTS_PUBLISH_TOKEN`), proving the caller is **the operator of this service** — not per-publisher identity. Every holder of the token is indistinguishable. With no token configured the service is **read-only**. That is the correct 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 (§ 20.1) live in `namespace_claims` and are **enforced** here, which a filesystem registry cannot do at all — but only as precisely as the identity behind them. 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 the code says so where it matters. ## Image and smoke ```bash make image # builds from the repo root; reference/ is a real dependency make smoke BASE=http://... # service-level checks, exits non-zero on failure ``` The image is a two-stage `python:3.12-slim` build carrying no build toolchain, running as a non-root user, writing nothing to disk — its state is the database. It deliberately does **not** run migrations on 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, the migration revision, and that the index and registry are queryable. It is stdlib-only so it runs inside the runtime image, and it exits non-zero so a deployment gate can call it directly. 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 rather than of this process, and belong to `rapp-canned-prompts`. `--expect-migration` matters: without it the check can only confirm the schema is stamped at all, and it says so rather than implying it verified the head. ## Status `CANP-WP-0006` is complete: skeleton and health surface, tenant-keyed schema (migrations `0001`–`0002`), read and publish APIs, HTTP registries in the reference CLI, and a verified container image with smoke checks. **Not done, and needed before `rapp-canned-prompts`:** the image has been built and verified locally but never pushed. `rapp.yaml` pins `upstream_components.version` to a digest from the fleet's registry, and that digest only exists once the image is published — an operator action needing registry credentials. **Per-publisher identity** remains deferred, and is the main thing between this and a registry several people can publish to.