167 lines
9.3 KiB
Markdown
167 lines
9.3 KiB
Markdown
|
|
# 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.
|