# 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.
