informed-decision/docs/protected-browser-review.md
tegwick 83849b75d4 Connect policy-gated browser review and audit runtime
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-11 00:31:03 +02:00

9.3 KiB

Protected browser review

Implemented under INFD-WP-0001-T08, release 0.2.0. The configured service now presents one named memo, records explicit highlight acknowledgments and accept/return/discuss/decline responses, and submits an accepted request to Approval Engine. This source has passed a disposable Chromium exercise; native policy, human login, custody and deployment admission remain open.

This first browser profile accepts English, organizational approve memos only. Other locales, weaker steps and AES/QES levels refuse before rendering, so an English interface cannot attest to a German presentation or imply a signature level it does not implement. German UI acceptance (PR-60), broader steps and awareness controls remain T08 product work; the domain model retains their vocabulary. The initial factory browser proof uses this bounded profile.

Request and evidence path

  1. KeyCape login verifies the human, paired token claims and MFA facts. The browser holds only an opaque session cookie. The human access token is used only for Approval Engine and never as the Flex Auth workload credential.
  2. review.py checks the named recipient and requests a fresh Flex Auth read decision before exposing memo/packet content or calling Approval Engine. The consumer contract defines all six actions.
  3. The immutable policy observation is persisted before use. The returned package/version/digest, submitted request digest, core binding and lifetime must match. Unsupported effects/obligations and unavailable callers refuse. Registry enrichment is observed, never reproduced by this consumer.
  4. The live Approval Engine object must declare human_control=true and match the memo's carried act digest. The requesting party and named human approver remain distinct. The presentation privately retains the observed binding.
  5. Every acknowledgment or response obtains another applicable decision. Accept requires the current memo, required acknowledgments, a live human session, current audit readiness and an open matching approval. Immediately before the engine POST, the adapter rechecks the binding and the controller checks policy/session freshness and reserves the single durable attempt.
  6. Acceptance stores the original engine entry correlation. Reload does not POST again. A lost response, in-flight attempt or unknown duplicate remains visible and cannot acquire a new presentation link. Return, discussion and decline record local human responses and never create an engine entry.

The governed canonicalizer remains the sole writer of view_hash. No presentation/acknowledgment fact is submitted to its own permission decision. Policy observations stay in private content, outside the presentation claim. decision_attributable=false remains explicit: checking the current unsigned envelope is not GH-DEC-2026-010 decision-path validation (FLEX-WP-0024).

The renderer requires memo.ui_release=informed-decision@0.2.0. An older memo must receive a new immutable version; it is not silently relabelled or rendered as though the old interface produced the current view. Schema v1 databases migrate atomically to v2 by adding append-only policy observations, preserving old evidence and submission records.

Browser surface

Route Behavior
GET / Sign-in or named memo identifier form; no approvals inbox
GET /review?memo_id=... Entitled current memo; existing intent retrieves its original view
GET /presentations/<id> Entitled historical presentation and current submission result
GET /presentations/<id>/packet/<index> Fresh read permission and full attachment bytes
POST /presentations/<id>/ack Explicit acknowledgment of selected highlights
POST /presentations/<id>/act Accept, return, discuss or decline with an operation id

Forms require the exact fixed origin and session CSRF token, with bounded bodies and rejection of duplicate/unknown fields. Session expiry and actor/version checks apply server-side, including when a user bypasses a disabled button. The page names the complete request and packet as the object of acceptance. Highlights do not narrow its scope. Untrusted memo text is escaped, previews are bounded text, and downloads are attachments with nosniff.

Pages use Referrer-Policy: same-origin, while authentication paths and downloads use no-referrer. The Chromium test caught that using no-referrer on a form page produces Origin: null and rejects legitimate submissions. Missing, foreign and null form origins remain refused. Cross-origin referrers, inline scripts, framing and caching remain prohibited. There is no telemetry or client-side token storage. The reverse proxy must still omit auth queries, bearer headers and cookies from its logs.

Owner configuration and audit readiness

Set INFD_REVIEW_CONFIG to an absolute, owned, regular 0600 JSON file. Its parent evidence directory must already exist with mode 0700. Without this configuration, the entrypoint remains the login shell and /readyz stays 503. The following is a shape example, not an admitted configuration; the placeholders intentionally do not pass validation:

{
  "schema": "informed-decision.review-runtime.v1",
  "evidence_db": "/OWNER-PRIVATE-PATH/review.sqlite",
  "approval_origin": "https://APPROVAL-OWNER-ORIGIN",
  "policy": {
    "origin": "https://FLEX-OWNER-ORIGIN",
    "package": "OWNER-ADMITTED-PACKAGE",
    "version": "OWNER-ADMITTED-VERSION",
    "package_digest": "OWNER-ADMITTED-SHA256",
    "caller_token_file": "/OWNER-PROJECTED-PATH/flex-caller-token"
  },
  "audit": {
    "origin": "https://AUDIT-OWNER-ORIGIN",
    "sender_token_file": "/OWNER-PRIVATE-PATH/audit-sender-token"
  }
}

INFD_KEYCAPE_ISSUER remains mandatory. Credential files are read afresh when needed, so projected rotation does not require storing a token in configuration or SQLite. Runtime loading provisions nothing. Fixed internal .svc origins are supported explicitly; public cleartext origins and redirects are refused. The server remains one Waitress process, loopback port 8080, four threads. Multi-replica sessions/storage and deployment packaging require separate work.

The audit thread ticks every 30 seconds, generates the declared per-class heartbeats, drains at most ten records per tick and writes a private audit-reconciliation.json at most every five minutes. A successful drain and reconciliation with no retained delivery debt make readiness current for 90 seconds. Errors or blocked records clear readiness; accept refuses while it is closed. This is a process/delivery health signal, not an assertion of native policy admission or authorization. Dispositions still persist transactionally.

Credential/schema refusals need operator repair and explicit requeue; no failed event is discarded. Source occurred_at and receiver accepted_at are retained as different count windows, without an automatic loss or completeness claim. An independent auditor reads the receiver in component tests. Production sender/receiver custody, backup/restore and reconciliation procedures remain admission work under T08 and AUDIT-WP-0009-T11.

Uncertain entry recovery must preserve causation. Finding a later entry is insufficient because Approval Engine stores no caller presentation/operation id. Inspect the original immutable intent and independent evidence; never reset an unresolved attempt merely to make the button available. If causation cannot be established, keep it unresolved and obtain an owner-directed disposition of the approval. This UI offers no shortcut that fabricates the missing link.

Reproduce the component and browser checks

Build the checked Flex Auth source with its own go build ./cmd/flex-auth, then set explicit source paths and the resulting binary:

export INFD_APPROVAL_ENGINE_SOURCE=/path/to/approval-engine
export INFD_AUDIT_CORE_SOURCE=/path/to/audit-core
export INFD_FLEX_AUTH_BINARY=/absolute/path/to/flex-auth
make check

With the Python dev dependencies, Playwright and Chromium installed, start PYTHONPATH=. python tests/browser_fixture.py /tmp/infd-browser-UNIQUE and run node tests/browser_review.mjs /tmp/infd-browser-UNIQUE in another terminal. INFD_PLAYWRIGHT_PACKAGE can name an installed Playwright package, and INFD_CHROMIUM_EXECUTABLE can name its Chromium executable. The driver stops the fixture and writes the result plus desktop/mobile screenshots.

The harness uses real local HTTPS, browser redirects/forms/cookies, the actual Flex Auth server with enforced caller authentication, and the actual Approval Engine/Audit Core APIs with separate SQLite stores. Its issuer, TokenReview, policy assignments, identities, credentials and act are synthetic. Browser hostnames resolve to loopback; only the fixture certificate's public key is trusted by the override. The harness control endpoints live only in tests. Exact production callback/registration strings remain covered separately.

The result covers login, escaped content, acknowledgment bypass refusal, explicit acknowledgment, packet download, return, mobile fit, a single accepted entry/reload, receiver delivery, workload caller refusal, sign-out and absence of script execution. It proves no native human login, production policy/custody, deployed UI or factory execution. See the dated JSON evidence for exact sources.