docs(ADR-0173): Workbench Ratification Trust Boundary (W0) (#394)

W0 of the workbench-UI wave per WORKBENCH-UI-WAVE-SCOPING.md.
Pure docs PR; no UI code.

Decision: the workbench is a local keyboard accelerator for the
existing local-CLI ratification surface (ADR-0161 Surface C), not a
fourth ratification surface and not a new trust boundary. Every
workbench-driven ratification action invokes the same Python
entrypoint as the corresponding CLI invocation, with identical
preconditions, exceptions, and append-only JSONL effects.

Amends ADR-0160 v1 read-only stance narrowly: admits driving the
three Tier 1.5 handlers (apply_lexical_claim, apply_frame_claim,
apply_composition_claim) and the existing core teaching review path
through the workbench API. Honors ADR-0161's surface set unchanged
(workbench is part of Surface C, not a new surface). Honors ADR-0162
no-go list and pins the keyboard contract referenced in §7.

Five open questions from the scoping brief resolved:
  Q1 in-process via existing Python entrypoints
  Q2 single-operator (multi-operator deferred)
  Q3 same chat/telemetry.py JSONL sink, new event kinds
     (operator_ratify / operator_reject / operator_defer /
     operator_navigate); no parallel log
  Q4 fonts and icons bundled locally; no CDN
  Q5 dist/ gitignored; CI verifies build, does not commit artifact

Ratification record extended with ratifier_kind: "workbench" — audit
forensic discriminant only, not a permission gate. proposal_id + to
remain the only load-bearing replay fields (ADR-0161 §5 unchanged).

Compatibility audit covers ADR-0146, 0150, 0152, 0160, 0161, 0162,
0167, 0168/0168.1, 0169/0169.1, 0172. Forbidden surface enumerated:
no remote operator, no CORS relaxation, no new mutation paths, no
auto-ratify, no batch ratification, no engine_state writes outside
the existing checkpoint path, no parallel workbench-events log, no
mobile/responsive form factors (ratification stays on Surfaces A+B
for mobile per ADR-0161).

Acceptance gates pinned per W1..W4 of the scoping brief.
This commit is contained in:
Shay 2026-05-27 15:34:43 -07:00 committed by GitHub
parent 44c0aa2896
commit b37aadc972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,558 @@
# ADR-0173 — Workbench Ratification Trust Boundary
**Status:** Proposed (W0 of the workbench-UI wave; no UI code in this PR)
**Date:** 2026-05-27
**Author:** Shay
**Parent:** [ADR-0160](./ADR-0160-core-workbench-v1.md), [ADR-0161](./ADR-0161-hitl-async-queue.md), [ADR-0162](./ADR-0162-workbench-design-system.md)
**Related:** ADR-0146 (checkpoints), ADR-0150 (engine_state durability), ADR-0152 (replay), ADR-0167 (audit-as-evidence), ADR-0168 / 0168.1 (FrameClaim), ADR-0169 / 0169.1 (CompositionClaim), ADR-0172 (math contemplation corridor), `docs/handoff/WORKBENCH-UI-WAVE-SCOPING.md`
---
## Context
The workbench UI wave (`WORKBENCH-UI-WAVE-SCOPING.md`) requires a
trust-boundary update before any frontend code lands. The relevant
prior doctrine:
- **ADR-0160 §"Trust boundary"** declared workbench v1 **read-only by
default**. Forbidden in v1: "accepting proposals", "rejecting
proposals as durable state", "mutating teaching corpus", "mutating
packs", "writing engine_state except through normal runtime
checkpoint path".
- **ADR-0161 §5** pinned three ratification surfaces and explicitly
stated CI workflows **cannot ratify**:
- Surface A — GitHub PR (mobile-primary, inspect-only)
- Surface B — `workflow_dispatch` (mobile-primary, transition-capable)
- Surface C — local CLI (`core teaching review …`, audit-grade)
Both ADRs pre-date the Tier 1.5 math ratification handlers that now
exist on `origin/main`:
- `teaching/math_lexical_ratification.py::apply_lexical_claim` (W2-D of ADR-0167)
- `teaching/math_frame_ratification.py::apply_frame_claim` (PR #389 / ADR-0168)
- `teaching/math_composition_ratification.py::apply_composition_claim` (PR-β of ADR-0169, in flight)
Each handler is:
- replay-gated
- hazard-pinned (case 0050 mandatory pin)
- partition-tested (math/cognition isolation)
- idempotent (`AlreadyRatified` on duplicate)
- append-only on a reviewed JSONL artifact under
`language_packs/data/en_core_math_v1/{lexicon,frames,compositions}/*.jsonl`
- accessible only via local Python entrypoints
The original ADR-0160 prohibition was a safety stance against
mutation paths that did not yet have proper gates. Those gates now
exist. The question is therefore not whether ratification can land
through the workbench — it is **how** without inventing a new trust
boundary.
The current operator-burden cost (named in the scoping brief's five
pain points) is the gating constraint on the teaching corridor's
compounding loop. Every audit refusal we cannot ratify quickly is a
learning event the engine never gets.
---
## Prior ADR compatibility audit
This ADR is not final until it remains compatible with prior ADR
doctrine. The following audit was performed before opening
implementation work.
| Prior ADR | Load-bearing rule | ADR-0173 compatibility result |
|---|---|---|
| ADR-0146 | engine_state durability via checkpoint path | Compatible. ADR-0173 explicitly forbids workbench writes to `engine_state/*` outside the existing checkpoint path. |
| ADR-0150 | engine_state schema discipline | Compatible. No new engine_state shapes from this ADR. |
| ADR-0152 | replay invariants | Compatible. Workbench-driven ratifications produce byte-identical replay outcomes to CLI-driven ratifications. |
| ADR-0160 | Workbench v1 read-only by default | **Amended**, narrowly. The "read-only" stance is preserved for corpus/packs/engine_state; mutation is admitted only through the existing reviewed handlers, with the workbench acting as a local keyboard accelerator (see Decision). |
| ADR-0161 | Three ratification surfaces (A/B/C); CI workflows cannot ratify | **Compatible without amendment** to the surface set. The workbench is *not* a fourth surface — it is a local keyboard accelerator for Surface C. Every UI ratification action invokes the same Python entrypoint as the CLI. The ratification record gains a new `ratifier_kind: "workbench"` discriminant for audit-forensic clarity, but `proposal_id` and `to` remain the only load-bearing fields for replay. |
| ADR-0162 | Workbench design system; v1 must-ship; no-go list | Compatible. ADR-0173 pins the keyboard contract referenced in ADR-0162 §7 and resolves the `RatificationCommandPanel` semantics that ADR-0162 §9 named as must-ship without pinning. |
| ADR-0167 | Audit rows become teaching evidence; LexicalClaim-first; partition guarantees | Compatible. ADR-0173 does not alter evidence-floor semantics, partition tests, or sub-type discipline. |
| ADR-0168 / 0168.1 | FrameClaim doctrine + math-domain proposal adapter | Compatible. ADR-0173 admits driving `apply_frame_claim()` from the UI; preconditions, hazards, and partition tests are unchanged. |
| ADR-0169 / 0169.1 | CompositionClaim doctrine + math-domain proposal adapter | Compatible. ADR-0173 admits driving `apply_composition_claim()` from the UI subject to the same scope as ADR-0169 (SAFE_COMPOSITION_CATEGORIES allowlist; case 0050 pin). |
| ADR-0172 | Math contemplation corridor (Tier 1.5) | Compatible. The workbench-driven ratification corridor is the operator-facing throughput pair to ADR-0172's mechanism. |
### Resolved tension: ADR-0161 surface set
ADR-0161 §5 says "CI workflows cannot ratify". This ADR honors that:
- The workbench backend (`workbench/api.py`) runs **locally only**
bound to `127.0.0.1` per ADR-0160. It is not a CI workflow and
cannot be reached remotely.
- The workbench is **not** a fourth ratification surface in ADR-0161's
taxonomy. It is a keyboard accelerator over Surface C (local CLI).
- The ratification record gains a `ratifier_kind: "workbench"`
discriminant **for audit forensic clarity only** — not because the
workbench has different trust authority than Surface C. Replay
remains driven by `proposal_id` and `to` alone (ADR-0161 §5
"Replay invariants").
What is forbidden:
- exposing the workbench backend on a non-loopback interface
- introducing a CI workflow that drives `apply_*_claim()` via the
workbench API
- adding a "remote operator" authentication path
- treating `ratifier_kind: "workbench"` as a permission discriminant
rather than an audit field
This section is the compatibility trip-wire for any implementation PR.
---
## Decision
> The workbench is a **local keyboard accelerator for the existing
> local ratification handlers**. It is not a new trust boundary, not
> a fourth ratification surface, and not a new mutation path.
>
> Every workbench-driven ratification action invokes the same Python
> entrypoint — `apply_lexical_claim()`, `apply_frame_claim()`,
> `apply_composition_claim()`, `core teaching review …` — with the
> same evidence, the same preconditions, the same exceptions, and the
> same append-only JSONL effect as a direct CLI invocation.
This ADR therefore:
1. amends ADR-0160's v1 read-only stance narrowly to admit the three
existing handlers as UI-driven ratification targets;
2. honors ADR-0161's surface set unchanged (A/B/C — workbench is C);
3. extends the ratification record with a `ratifier_kind: "workbench"`
discriminant for audit forensics;
4. pins the keyboard contract referenced by ADR-0162 §7;
5. resolves the five open questions named in
`WORKBENCH-UI-WAVE-SCOPING.md`.
The ADR does **not** approve any new mutation paths, any new corpora,
any new pack types, any remote operator authentication, or any
handler the implementation PRs have not already proven on the server
side.
---
## Five resolved open questions
The scoping brief deferred these to W0. Each is pinned below.
### Q1 — In-process ratification vs out-of-process dispatch
**Decision: in-process.**
The workbench API (`workbench/api.py`) imports and calls the existing
`apply_*_claim()` Python entrypoints directly. The UI sends a
`POST /math-proposals/{id}/ratify` (existing route) and the API
invokes the corresponding handler in the same process.
Rationale:
- Out-of-process dispatch (subprocess of `core teaching review`)
would add operator latency for no audit gain — both paths execute
the same code.
- In-process keeps the exception surface usable: the handler raises
`AlreadyRatified`, `WrongClaimSubType`, `WrongCompositionCategory`,
etc., and the API translates them into structured JSON errors the
UI renders verbatim.
- Replay invariants are unaffected — both paths produce identical
`proposals.jsonl` / JSONL-artifact appends.
What this does **not** admit:
- Hidden background ratification (no auto-ratify, no "approve all
replay-passed").
- Dispatching anything beyond the three existing handlers. A new
handler requires its own scoping ADR (the ADR-0168 / 0169 pattern).
### Q2 — Single-operator vs multi-operator concurrency
**Decision: single-operator.**
v1 of the workbench UI assumes one operator at a time. There is no
proposal locking, no review-state contention, no operator presence
indicator, no real-time collaboration.
Rationale:
- The existing handlers are idempotent (`AlreadyRatified` on
duplicate) — multi-operator is *safe*, just not *ergonomic*.
- A second operator browsing the same queue would see stale state
until the next backend poll; this is acceptable for v1 because the
primary deployment is one operator on one laptop.
- Multi-operator features (proposal claim, presence cursors, conflict
banners) are explicitly deferred to a follow-up ADR if the need is
ever proven.
### Q3 — Telemetry path for operator-action events
**Decision: same JSONL sink as `chat/telemetry.py`, new event kinds.**
The workbench backend emits operator-action events to the existing
telemetry sink (the one configured by `chat/telemetry.py`) using new
discriminant kinds:
```json
{"event": "operator_ratify", "proposal_id": "...", "handler": "...", "outcome": "applied|rejected_precondition", ...}
{"event": "operator_reject", "proposal_id": "...", "handler": "...", "note": "...", ...}
{"event": "operator_defer", "proposal_id": "...", ...}
{"event": "operator_navigate", "proposal_id_from": "...", "proposal_id_to": "...", "kind": "j|k|search|click", ...}
```
Rationale:
- A single sink keeps audit reconstruction simple: one log to fold.
- Discriminant `event` kinds keep workbench events isolable without a
parallel file.
- `redact-by-default` discipline from the existing sink applies: no
surface text or token content in workbench events unless the sink
is explicitly opted into `include_content=True`.
Forbidden:
- a parallel "workbench-only" log file
- emitting operator-action events that include corpus or evidence
text content
- emitting an event before the handler has actually attempted the
ratification (no "intent-to-ratify" telemetry — only outcomes)
### Q4 — Font and icon bundling
**Decision: bundled locally.**
ADR-0162 pins typography but did not commit on remote vs bundled.
ADR-0173 commits to **bundled**: fonts and icon glyphs ship in
`workbench-ui/dist/` and the running UI loads them from
`127.0.0.1:<port>`.
Rationale:
- ADR-0160 forbids "remote network dependencies for cognition" — the
same posture applies to the operator-facing UI, even though the UI
is not itself cognition.
- A CDN font fetch creates a determinism asymmetry (font renders
differently with/without network) that the workbench's read-only
invariants must not depend on.
- Local-first is the workbench's defining property; bundling fonts
preserves it under offline development (and offline operators —
see [[user-circumstances]]).
License compliance for bundled fonts is the responsibility of the
W1 PR.
### Q5 — Build artifact location
**Decision: gitignored; CI verifies the build is green but does not
commit `dist/`.**
`workbench-ui/dist/` is added to `.gitignore` in the W1 PR. The
local `core workbench serve` command builds (or uses a previously-
built) `dist/` and serves it as static assets through the existing
stdlib HTTP server.
Rationale:
- Committing build artifacts pollutes the diff with regenerated
bundle bytes that are not load-bearing for replay.
- A CI job (`workbench-ui-build`) builds `dist/` to verify the source
compiles, then discards the artifact. This is enough audit signal
without bloating the repo.
- Operators run `npm ci && npm run build` once after `git clone`;
the brief acceptance criterion ("a new operator can run `core
workbench serve` and open the UI") covers this in its setup step.
---
## Keyboard contract (pinned)
ADR-0162 §7 named "Keyboard contract" but did not commit specific
bindings for the ratification corridor. ADR-0173 pins them.
**Global (every view):**
| Key | Action |
|---|---|
| `?` | Show keyboard help overlay |
| `:` | Focus command palette |
| `g p` | Go to proposal queue |
| `g e` | Go to eval center |
| `g t` | Go to trace drawer (last viewed) |
| `Esc` | Dismiss overlay / blur input |
**Proposal queue + detail panel:**
| Key | Action |
|---|---|
| `j` / `↓` | Focus next proposal |
| `k` / `↑` | Focus previous proposal |
| `Enter` | Open detail panel for focused proposal |
| `Esc` | Close detail panel, return to queue |
| `/` | Search/filter the queue |
**Ratification command panel (only when proposal is focused AND replay-passed):**
| Key | Action |
|---|---|
| `r` | Ratify (apply handler) |
| `x` | Reject with note |
| `d` | Defer (mark as deferred-by-operator; remains pending) |
| `y` | Copy `suggested_ratify_cli` to clipboard (operator audit fallback) |
| `?` | Show panel-specific help |
Bindings that ratify (`r`) MUST require the proposal to be in
`pending` state AND `replay_evidence.replay_equivalent == true` AND
the handler to be one of the admitted set. If any precondition
fails, the key is a no-op and a status-footer message names the
failing precondition. No silent failures.
Bindings MUST NOT chord with `Cmd`/`Ctrl` to avoid colliding with
browser-level shortcuts (`Cmd+R` reload, etc.).
The W1 PR ships the keyboard help overlay (`?`) populated from this
table; W3 wires the ratify/reject/defer keys.
---
## What the workbench MAY do
The narrow amendments to ADR-0160's read-only stance.
- **Read everything** the existing API routes expose (proposals,
math-proposals, artifacts, evals, replays, traces).
- **Drive `apply_lexical_claim()`** on a `LexicalClaim` proposal via
`POST /math-proposals/{id}/ratify`.
- **Drive `apply_frame_claim()`** on a `FrameClaim` proposal via the
same route.
- **Drive `apply_composition_claim()`** on a `CompositionClaim`
proposal once ADR-0169's CC-2 lands.
- **Drive `core teaching review --accept|--reject|--withdraw`** on
cognition `TeachingChainProposal` records via the existing
`/proposals/{id}` ratification route once Surface C parity is
proven in tests (this is the cognition analog; see Sequencing).
- **Emit operator-action telemetry** to the existing JSONL sink per
Q3 above.
- **Render** the `suggested_ratify_cli` field for audit fallback
(operator may copy and run via shell if they prefer).
---
## What the workbench MUST NOT do
The non-negotiable forbidden surface.
- **No new mutation paths.** The workbench does not edit corpus,
packs, lexicon, frame registries, or composition registries
*except* via the admitted handlers above.
- **No bypass of handler preconditions.** UI cannot ratify a
proposal that fails `replay_evidence.replay_equivalent` or that
the handler would reject. The UI's `r` keybind calls the handler;
the handler's exception path is the UI's failure path.
- **No remote operator.** The backend binds to `127.0.0.1` only.
CORS is restrictive. No "operator login" endpoint, no token-
bearer auth, no remote workflow_dispatch trigger.
- **No engine_state writes** outside the existing checkpoint path
(ADR-0146 / 0150).
- **No silent ratification.** Every ratification produces an
append-only telemetry event AND a JSONL artifact append. UI must
surface both to the operator visually before transitioning the
proposal off the queue.
- **No auto-ratify** under any condition. Replay-passed proposals
remain `pending` until an explicit keypress (`r`) from a focused,
expanded proposal detail.
- **No batch ratification** in v1. Each ratification is one
keypress on one focused proposal. ("Select multiple, ratify all"
is explicitly deferred — see Non-goals.)
- **No mutation of cognition `TeachingChainProposal` flow** that
ADR-0161 has not already admitted. The workbench wraps Surface C;
it does not create a fourth surface.
- **No emission of cognition-domain artifacts from math handlers
and vice versa.** Partition guarantees from ADR-0167 / 0168 / 0169
hold unchanged.
---
## Ratification record extension
ADR-0161 §5 defines the ratification record schema. ADR-0173 extends
the `ratifier_kind` enum by one value:
```text
ratifier_kind ∈ {"cli", "workflow_dispatch", "workbench"}
```
A workbench-driven ratification appends a record whose only
divergence from a CLI ratification is `ratifier_kind: "workbench"`.
`actor` carries the OS username of the process running the workbench
server (same as Surface C). `workflow_run_id` is `null`.
**`ratifier_kind` is not a permission discriminant.** It is an
audit-forensic discriminant only. Any code path treating it as a
permission gate is a bug.
**`proposal_id` and `to` remain the only load-bearing fields for
replay** (ADR-0161 §5 "Replay invariants" unchanged).
---
## Sequencing
Per ADR-0166:
### Q1 — Capability
Adds **zero** new capabilities. The workbench ratification corridor
is an ergonomics layer over capabilities that already exist on
`origin/main`:
- `apply_lexical_claim()` — merged
- `apply_frame_claim()` — merged
- `apply_composition_claim()` — pending (PR-β of CompositionClaim wave)
- `core teaching review` — merged
Cognition `TeachingChainProposal` driving is admitted by this ADR but
implementation is deferred until Surface C parity tests are in place
(see Acceptance gates).
### Q2 — Lane
**No new eval lane.** Existing lanes remain the proof surface.
### Q3 — Invariant
Must preserve:
- `wrong == 0` on `core eval gsm8k_math`
- replay equivalence (ADR-0161 §5)
- deterministic claim hashing (ADR-0167 / 0168.1 / 0169.1)
- refusal-first semantics
- handler-side hazard pins (especially case 0050)
- partition (math vs cognition)
- ADR-0160 forbidden-set minus the narrow handler-driving amendment
The implementation PRs (W1..W4 in `WORKBENCH-UI-WAVE-SCOPING.md`)
pass only when all seven are mechanically proven.
---
## Acceptance gates for W1..W4
Each subsequent wave PR must provide:
### W1 (scaffold)
- `workbench-ui/` directory created at repo root per ADR-0162
Branch 1.
- `dist/` is gitignored.
- A CI job builds the UI green; no `dist/` committed.
- No remote font/icon fetches.
- Backend binds to `127.0.0.1` only (existing); UI verified to
refuse cross-origin requests.
### W2 (read surfaces)
- Reads only through existing API routes.
- No new backend routes introduced.
- Cognition and math proposal surfaces remain partitioned in the
UI (separate routes, separate components, separate stores).
- `StableJsonViewer` does not alter bytes — render is canonical.
### W3 (ratification corridor)
- Every `r` keypress executes the same Python entrypoint as the
corresponding CLI invocation.
- Equivalent ratification (CLI vs workbench) produces byte-equal
JSONL artifact appends except for `ratifier_kind`.
- Case 0050 hazard pin remains green end-to-end (ratifying any
admitted handler via the UI does not cause case 0050 to admit).
- `AlreadyRatified` / `WrongClaimSubType` /
`WrongCompositionCategory` exceptions surface verbatim in the UI
status footer.
- `r` keypress is a no-op (with named status footer message) on any
proposal not in `pending` state OR without `replay_equivalent == true`.
- Operator-action telemetry emits per Q3.
- Cross-process replay equivalence test: ratify via UI, fold the
proposals log, replay — outcome identical to CLI ratification.
- Partition tests: math UI ratifications do not produce cognition
artifacts and vice versa.
### W4 (verify surfaces)
- Trace and replay surfaces are read-only.
- Eval lanes run via existing read-only routes (`/evals`, `/evals/run`).
- No new eval lane introduced.
### All waves
- `core test --suite teaching -q` green.
- `core test --suite runtime -q` green.
- `core eval gsm8k_math` green.
- No new files under `engine_state/*` in the diff.
- No CDN/remote runtime dependencies.
---
## Non-goals
This ADR does NOT approve:
- A fourth ADR-0161 ratification surface (workbench is part of Surface C).
- Remote operators, operator login, or any authentication beyond
local filesystem authority.
- Multi-operator concurrency (Q2).
- Batch / multi-select ratification.
- Auto-ratify on replay pass (operator keypress is mandatory).
- Mutation of any handler not enumerated above.
- New corpus families.
- New pack types.
- A workbench-events parallel log (Q3).
- CDN-loaded assets (Q4).
- Committed `dist/` artifacts (Q5).
- ADR-0162 follow-up components (PackInspector, CorpusInspector,
VaultRecallInspector, MetricGateTable, RegressionDiffPanel, etc.).
- A node-graph builder, workflow canvas, or "approve all"
affordance (ADR-0162 no-go list).
- Toast notifications that auto-dismiss audit events
(ADR-0162 no-go list).
- Mobile / responsive form factors (workbench is laptop-class only;
mobile ratification stays on Surfaces A + B per ADR-0161).
---
## Decision summary
> The CORE workbench may extend ADR-0160 v1's read-only posture
> narrowly to admit operator-driven invocation of the three Tier 1.5
> ratification handlers (`apply_lexical_claim`, `apply_frame_claim`,
> `apply_composition_claim`) and the existing cognition
> `core teaching review` path, provided:
>
> - the workbench backend remains local-only (127.0.0.1, no CORS
> relaxation, no remote auth)
> - every ratification action invokes the same Python entrypoint as
> the corresponding CLI invocation, with identical preconditions,
> exceptions, and append-only artifacts
> - the workbench is treated as a keyboard accelerator over ADR-0161
> Surface C, not as a fourth ratification surface
> - the ratification record gains a `ratifier_kind: "workbench"`
> discriminant for audit forensics only — not as a permission gate
> - case 0050, partition, and handler hazard pins remain mechanically
> green end-to-end
> - all engine_state writes remain on the existing checkpoint path
> - no new mutation paths, corpora, packs, or handlers are admitted
>
> The operator burden is the bottleneck on the teaching corridor's
> compounding loop. This ADR makes ergonomics a first-class concern
> while keeping the trust boundary unchanged in substance.
Reopening this ADR requires evidence that:
1. the keyboard contract (above) is the wrong default and a
different contract would materially improve operator throughput;
2. multi-operator concurrency has become load-bearing for the
project's actual operator population; or
3. a new mutation path needs admittance that the existing
handler-handler discipline cannot express.
Reopening is not required to admit additional handlers as they ship
— each new handler is admitted automatically once its server-side
scoping ADR (the ADR-0168 / 0169 pattern) lands and its acceptance
gates are mechanically pinned.