# Canonical Claims Browser Read Path Status: implementation-ready, not deployed The canonical claims browser exposes a protected, read-only summary of `public.claims` for the LivingIP Observatory. It is deliberately separate from the public Markdown/Qdrant knowledge browser. ## Contract `GET /api/kb/claims` - protected by a route-scoped Observatory token; the browser never receives it - schema: `livingip.canonical-claims.v1` - filters: `q`, `status`, `type`, and `tag` - opaque keyset cursor over `(updated_at, id)` - default requested page size 25; maximum 100; the server may return fewer rows while preserving `has_more` and `next_cursor` to keep the serialized response at or below 500,000 bytes - allowlisted output only: canonical UUID, bounded claim text, type, status, confidence, tags, timestamps, supersession pointer, evidence count, and edge count - response headers: `Cache-Control: private, no-store, max-age=0` - no mutation methods, source excerpts, storage paths, credentials, internal endpoints, or embeddings Only the exact `GET /api/kb/claims` path bypasses the global Argus middleware so its handler can authenticate the route-scoped token. The legacy `/api/kb/claims/{uuid}` and `/kb/claims/{uuid}` routes require the global Argus key and fail closed when that key is not configured. The Observatory does not receive, reuse, or expose the global Argus key. Those legacy detail routes are explicitly outside `livingip.canonical-claims.v1`: they include source-rich evidence and still use the pre-existing claim-review database credential path. The Observatory adapter must not call them. Migrating legacy detail reads to a separate role (including a deliberate `public.sources` grant) is residual hardening, not a prerequisite for this summary-only browser. Create a separate high-entropy token in a root-managed, service-readable file: ```text /opt/teleo-eval/secrets/kb-observatory-api-key ``` The file contains only a 24-to-512-character ASCII token with no whitespace (a trailing newline is accepted), and it must not be world-accessible. Set `KB_CLAIM_BROWSER_API_KEY_FILE` only when using a different root-managed path. Configure the same value as the server-only `OBSERVATORY_CANONICAL_API_KEY` secret in Vercel; never use a `NEXT_PUBLIC_` variable. ## Dedicated Database Role The handler accepts either both explicit `KB_CLAIM_BROWSER_ROLE` and `KB_CLAIM_BROWSER_SECRETS_FILE` settings, or the protected default password file at `/opt/teleo-eval/secrets/kb-observatory-read-password`. The default path is file-gated: if the file is absent, partial environment configuration is present, or the file fails its permission/format checks, the endpoint fails closed. The only accepted role name is `kb_observatory_read`, and every response is withheld unless Postgres reports that the current session has `transaction_read_only=on`. Create a dedicated login using a password supplied out of band, then grant only the relations needed by the list query: ```sql create role kb_observatory_read login password :'observatory_read_password'; alter role kb_observatory_read set default_transaction_read_only = on; grant connect on database teleo to kb_observatory_read; grant usage on schema public to kb_observatory_read; grant select on public.claims, public.claim_evidence, public.claim_edges to kb_observatory_read; ``` Store the password in a dedicated root-managed file readable by the Argus service. Its format is intentionally narrow: blank lines and `#` comments are allowed, followed by exactly one literal assignment (no shell expansion): ```text PGPASSWORD='the-observatory-read-role-password' ``` No `KB_APPLY_PASSWORD`, `KB_APPLY_DB_PASSWORD`, or additional assignments are accepted. The file must not be world-accessible; a suitable deployment mode is `root:teleo 0640`. Never place the password in Git, systemd unit text, browser configuration, or a `NEXT_PUBLIC_` environment variable. Optional explicit Argus overrides: ```text KB_CLAIM_BROWSER_ROLE=kb_observatory_read KB_CLAIM_BROWSER_SECRETS_FILE=/opt/teleo-eval/secrets/kb-observatory-read-password ``` Optional connection overrides use the same prefix: `KB_CLAIM_BROWSER_CONTAINER`, `KB_CLAIM_BROWSER_DB`, `KB_CLAIM_BROWSER_HOST`. The query has three independent bounds: a three-second Postgres connection timeout, a five-second Postgres statement timeout, and an eight-second process timeout. The aiohttp handler runs the blocking database work in a worker thread and stops awaiting it after ten seconds, so a slow database cannot block the Argus event loop. ## Promotion Checks 1. Run `pytest tests/test_kb_claim_routes.py -q` and Ruff on the route and test. 2. Deploy both dedicated secret files without changing the global Argus key. 3. Verify an unauthenticated list request returns `401`. 4. Verify an authenticated request returns the versioned contract and no-store headers. 5. Verify every response body is at most 500,000 bytes and two cursor pages are disjoint and stable, including when the first page is shortened by the byte cap. 6. From the database session, verify `current_setting('transaction_read_only')` is `on` and INSERT/UPDATE/DELETE all fail. 7. Configure the Vercel adapter with a server-only URL and API key only after preview Deployment Protection is enabled.