Some checks are pending
CI / lint-and-test (push) Waiting to run
* Add protected canonical claims read API * Normalize claim loader exits in worker thread * Allow file-gated claims reader defaults
119 lines
5.2 KiB
Markdown
119 lines
5.2 KiB
Markdown
# 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.
|