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
130 lines
5.6 KiB
Markdown
130 lines
5.6 KiB
Markdown
# 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` |
|
||
|
||
## 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.
|
||
|
||
## 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
|
||
`<id>@<version>` 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.
|
||
|
||
## Status
|
||
|
||
`CANP-WP-0006` T01–T04 are done: skeleton, health surface, tenant-keyed schema
|
||
(migrations `0001`–`0002`), and the read and publish APIs. The HTTP registry
|
||
client in the CLI and the container image (T05–T06) are not built yet. T06
|
||
produces the image digest that `rapp-canned-prompts` needs to pin.
|
||
|
||
Per-publisher identity is deferred, and is the main thing standing between this
|
||
and a service that several people can publish to.
|