"`irrevon serve` — the loopback read-only workbench surface"
Context
The workbench (web/, ADR-0016) shipped fixture-backed because no read server existed
(review-queue BI-4). The owner’s rebuild directive of 2026-07-21 ordered the serve
workstream: a local read surface so the packaged CLI can serve the workbench over real
ledger data. Constraints: ADR-0013’s thin-stack discipline (no new runtime deps without
cause), the read-only-in-depth posture (master doc §9: the browser can never mutate), and
ADR-0018’s single-wheel distribution (the CLI ships the built web assets and serves them
Node-free). Proposed by the BE builder from the serve design spec; implementation lives in
src/irrevon/serve.py + src/irrevon/api/readviews.py with migration
0005_read_role.sql and the tests/serve/ suite.
Decision
- stdlib
http.server(ThreadingHTTPServer), zero new runtime deps. FastAPI/Flask rejected: no request bodies, one local user, thin-stack discipline (ADR-0013). - Loopback-only: hard-coded 127.0.0.1 bind; no
--hostflag and no env override exists; a post-bind assertion refuses non-loopback (tested by monkeypatching the bind constant). - Read-only in depth, three layers: GET/HEAD-only handler (405 otherwise; row-count
proof test) →
irrevon_readSELECT-only role (migration 0005; no EXECUTE on the locked ledger functions; grants audit test) →default_transaction_read_only=onper session AND at role level. - Q1 lean-item deviation from RFC-002 §9: the served list item is
{record, classification, finding}(the shipped workbench shape); the full composition lives on the inspect route. Adding fields later is additive, never breaking. - stable_ids served unredacted on this surface — loopback single-user trust domain;
redaction stays a CLI-inspect affordance (
--reveal). Flagged for explicit ratification (review-queue §3). - Additive error codes:
query_invalid,method_not_allowed,internaljoin the established API error set (MINOR events). - Version handshake:
/api/v1namespace + payloadschema_version+Irrevon-Schema-Version: 1response header on every API response including errors; the workbench refuses to render on a mismatch (full-surface refusal, never partial). - Entry point remains
irrevon.cli:entrypoint(ADR-0018’s text saysmain; noted here, not churned).
Alternatives
- FastAPI/Flask/Starlette — rejected: dependency weight and attack surface for a loopback GET-only surface with one user.
- Serving the workbench from Vite/Node — rejected: ADR-0018 requires strangers never
to need Node; the wheel embeds
web/distand the CLI serves it statically. - Reusing
irrevon_appfor reads — rejected: the app role holds EXECUTE on the locked transition functions; the read surface must be unable to mutate even if the handler is wrong (defense in depth).
Consequences
- The workbench live mode (
web/useLiveStatus, LIVE chip, disconnected banner, version refusal) has a real backend; the joint E2E (make serve-live+ the Playwright live suite) proves the integration against the real engine. - Migration 0005 becomes part of the baseline schema;
irrevon doctorgains aserve_readycheck. - Enforced by:
tests/serve/(loopback refusal, method rejection, privilege audit, version handshake, route conformance, inspect byte-parity, traversal/SPA-fallback, zero-outbound), and the workbench live-boundary suite.
Risks
- A future route added outside
api/readviews.pycould bypass the single-producer discipline — mitigated by route-conformance tests and review. - stable_ids exposure ruling (Decision 5) could be reversed by the owner — the change would be a redaction layer on the serve views, additive.
Reopen trigger
A multi-user or non-loopback deployment need arises (forces the auth/bind questions this ADR explicitly excludes); or the owner rejects the stable_ids exposure ruling in the review queue; or RFC-002 §9’s full Q1 composition becomes required by a consumer.