informed-decision/docs/protected-browser-review.md

167 lines
9.3 KiB
Markdown
Raw Normal View History

# 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 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:
```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 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:
```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.