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
|
|
|
|
# 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 `<registry>:<id>@<version>`, 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` |
|
|
|
|
|
|
|
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
|
|
|
|
## Read API
|
|
|
|
|
|
|
|
|
|
|
|
Routes address packages with the format's own reference syntax
|
|
|
|
|
|
(`<registry>:<id>@<version>`, § 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.
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
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
|
|
|
|
`CANP-WP-0006` T01–T03 are done: skeleton, health surface, tenant-keyed schema
|
|
|
|
|
|
and migration `0001`, and the read API. The publish API, HTTP registry client
|
|
|
|
|
|
and container image (T04–T06) are not built yet. T06 produces the image digest
|
|
|
|
|
|
that `rapp-canned-prompts` needs to pin.
|