# 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](flex-auth-review-contract.md) 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, then the reviewer's decision overview (below) and the memo identifier form | | `GET /review?memo_id=...` | Entitled current memo; existing intent retrieves its original view | | `GET /presentations/` | Entitled historical presentation and current submission result | | `GET /presentations//packet/` | Fresh read permission and full attachment bytes | | `POST /presentations//ack` | Explicit acknowledgment of selected highlights | | `POST /presentations//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. ## Decision overview Added under `INFD-WP-0003`. After sign-in, `GET /` lists the memos this repository holds that name the signed-in person as the person bound (`binding.principal`, kind `person`). The latest version of each memo is placed in one group: | Group | Condition | | --- | --- | | Needs attention | the accepted entry is `in_flight` or `unresolved` and needs operator recovery | | Open for you | no decision recorded on the latest version, or a prepared accept not yet submitted, and the approval is `requested`/`approved` or its status is unknown | | Accepted | Approval Engine confirmed the entry. The row shows `approved_at` and the live status | | Declined | the latest response on the latest version is `decline` | | Returned or in discussion | the latest response on the latest version is `return` or `discuss`. A revised version moves the memo back to Open | | Closed without your response | the approval is `expired`, `revoked`, `superseded` or `consumed` and you recorded no decision | | Not available | the permission check refused or failed. Only the memo id and a bounded reason are shown | Each row links to `/review?memo_id=...`. It also lists the person's earlier responses on every version. The overview is not an inbox and not approval state: - **It checks permission per row.** Each row gets its own fresh Flex Auth `list` decision before its question is shown, and each decision is stored as a policy observation (`INFD-WP-0004`). `list` is not `read`: opening the memo still asks `read`, and each act asks its own action. A refused row does not call Approval Engine. Until the served package answers `list`, every row shows as Not available. - **It reads Approval Engine by id only.** The overview looks up the approval id the memo already carries. It never polls for work. The status is shown live and never stored. An unavailable engine, or an act digest that no longer matches, degrades only that row. - **It records nothing.** It creates no presentation, acknowledgment or disposition. Binding still happens only on the review page, which runs its own checks again. - **It shows little memo content.** A row shows only the question, the memo id and version, and the status. The brief and packet appear only on the review. ## 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: ```json { "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 require separate work. Single-writer container packaging and the review-only deployment packet are now supplied in [deploy/README.md](../deploy/README.md); publication and native rollout remain open. 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: ```sh 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.