* Add protected canonical claims read API * Normalize claim loader exits in worker thread * Allow file-gated claims reader defaults
5.2 KiB
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, andtag - opaque keyset cursor over
(updated_at, id) - default requested page size 25; maximum 100; the server may return fewer rows
while preserving
has_moreandnext_cursorto 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:
/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:
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):
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:
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
- Run
pytest tests/test_kb_claim_routes.py -qand Ruff on the route and test. - Deploy both dedicated secret files without changing the global Argus key.
- Verify an unauthenticated list request returns
401. - Verify an authenticated request returns the versioned contract and no-store headers.
- 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.
- From the database session, verify
current_setting('transaction_read_only')isonand INSERT/UPDATE/DELETE all fail. - Configure the Vercel adapter with a server-only URL and API key only after preview Deployment Protection is enabled.