Compatibility Record — WP02 MCP/Client Spike
Evidence for the delivery plan’s WP02 risk gate: “Exact 2026-07-28 live discovery/tool exchange and wrong-audience/expired/scope rejection; record exact dependency/client versions before freezing adapters.”
Verdict
Bounded stdlib adapter is viable; the official MCP SDK is deferred. The complete exchange (initialize → tools/list → tools/call, JSON-RPC over streamable-shaped HTTP with the Mcp-Session-Id header) was exercised over real loopback sockets with zero runtime dependencies. Nothing observed justifies taking the SDK dependency before WP07’s parity work; revisit if a future slice needs SSE streaming responses, transports other than streamable HTTP, or client auth flows beyond a bearer token.
Exact versions exercised
| Component | Version |
|---|---|
| CPython | 3.13.13 (macOS aarch64) |
| uv | 0.12.12 (c4be69153, 2026-09-09) — upgraded from 0.11.16 mid-spike; see below |
| Runtime dependencies | none (stdlib http.server, urllib.request, json, hmac, hashlib) |
| MCP protocol negotiated | 2026-07-28 (initialize echo) |
| Dev tools | pytest 9.1.1, ruff 0.16.6, mypy 2.3.1 (strict) |
| Lock | uv.lock (committed) |
Exchange proven
initialize→ protocolVersion2026-07-28+Mcp-Session-Idresponse headernotifications/initialized→ 202tools/list→ all 17 tools from the vendoredcontracts/interface-v1.1.0/mcp-tools.json(stg_v1_*namespace) — the corpus itself, not a fixture copytools/call(stg_v1_gateway_info) → content result
Authentication proven (benchweave.interfaces.identity)
Local test issuer, HMAC-SHA256, principal/audience/scopes/expiry, injected clock (pure, deterministic), constant-time signature compare, fail-closed rejections. Status mapping exercised over the live loopback:
| Condition | HTTP | Reason token |
|---|---|---|
| Valid scoped token | 200 | — |
| Wrong audience | 403 | wrong_audience |
| Missing scope | 403 | insufficient_scope |
| Expired | 401 | expired |
| Malformed / bad signature | 401 | malformed_token / bad_signature |
Enforcement point in the spike server: initialize open, tools/* gated — recorded as a decision to revisit when the interface contract grows an auth vocabulary (it has none today; checked, not assumed).
Environment note (venv editable install — ROOT CAUSE)
The intermittent import benchweave → ModuleNotFoundError (2026-09-10, 5 occurrences across uv 0.11.16 AND 0.12.12) is NOT a uv bug: the venv’s .pth files were carrying the macOS UF_HIDDEN file flag, and CPython’s site.py refuses hidden .pth files (python -v shows Skipping hidden .pth file), silently disabling the editable install while .pth + dist-info look intact. Proof: stat -f '%Sf' showed flags=hidden; python -v showed the skip.
Heal (instant): chflags nohidden .venv/lib/python3.13/site-packages/*.pth then re-run. (uv sync --reinstall-package benchweave also works; it rewrites the file without the flag.) The actor setting the flag between runs is UNIDENTIFIED — on recurrence, run ls -lO .venv/lib/python3.13/site-packages/*.pth immediately and check holders via lsof +D .venv. An earlier draft of this section attributed the failures to the uv 0.11.16 → 0.12.12 upgrade; the recurrence on 0.12.12 disproved that and prompted this correction.
WP07 Qualification Record — FastMCP/FastAPI Gateway (2026-09-13)
Supersedes the WP02 verdict above for WP07 and later: the deferred SDK adoption happened (principal directive, 2026-09-12). The WP02 stdlib exchange above remains proven evidence and the recorded fallback.
Verdict
FastMCP 4.0.3 over the official MCP SDK is qualified for the gateway. One FastAPI ASGI application serves the 20 REST routes under /v1/* and a FastMCP server mounted at /mcp on the same loopback port; both adapters wrap the one typed core-operations seam. REST↔︎MCP parity is proven per operation — the full contract envelope compared field-for-field across transports, each reachable failure class at its contract code (tests/integration/test_interface_parity.py) — and event recovery after SIGKILL is proven (tests/integration/test_event_recovery.py).
Exact versions exercised (uv.lock, committed)
| Component | Version |
|---|---|
| fastapi | 0.141.1 |
| uvicorn | 0.52.4 |
| fastmcp | 4.0.3 (pin ==4.0.3, server extra) |
| mcp (official SDK, under FastMCP) | 2.2.0 |
| CPython (project venv) | 3.13.13 (macOS aarch64) |
| Dev tools | pytest 9.1.1, ruff 0.16.6, mypy 2.3.1 (strict) |
Transport verified
Real loopback HTTP throughout: uvicorn on an ephemeral port, REST /v1 and MCP /mcp served by the one application. Not verified — out of PoC scope: TLS, OAuth flows beyond the local HMAC bearer issuer, and non-loopback binding.
Introspection notes (what actually worked)
initializerequesting2026-07-28counter-offers2025-11-25(LATEST_HANDSHAKE_VERSION;HANDSHAKE_PROTOCOL_VERSIONS≤ 2025-11-25 in this SDK generation, so initialize can never echo a modern revision). The 2026-07-28 revision is served viaserver/discover;gateway_infoadvertisesmcp_version "2026-07-28"on both transports.- FastMCP 4.0.3 signature inference cannot reproduce vendored schemas, so every tool is constructed with the vendored
inputSchemaverbatim via the SDK’s explicit-schema override route (the function signature is only the callable).tools/listdeep-equals the vendored corpus: all 17stg_v1_*tools,requiredincluded.
Deviation register (D1–D16, pinned by the parity suite)
Each deviation has a pinning test or an explicit disclosure; none is silent. Full wording for D1–D7 lives in the tests/integration/test_interface_parity.py module docstring. D8–D13 are the final-fix-wave register (2026-09-13, from the two whole-branch reviews); each names its WP08 reconciliation. D14–D16 were registered during WP08 itself (rendered-envelope divergence, the lease_create replay asymmetry, the session-wide admission lock).
WP08 close-out (2026-09-14): every row in the register below carries its disposition — CLOSED naming the closing commit(s), ACCEPTED as a pinned structural posture, or RE-LEDGERED naming the target (the WP09 async-posture re-ledger is pre-registered and untouched). Dispositions were verified against the code at close — git log is authoritative — not copied from the register’s own history.
Final-fix-wave code corrections shipped the same day (behavior pins updated with them): the §7 retention-overtake failure is event_gap, not cursor_expired (the overtake branch is event_gap’s raise site; cursor_expired now has no emitter — trim deletes contiguous prefixes, so a hole cannot arise by construction); run_cancel is owner-or-admin scoped (§6 — a control-tier stranger gets 403); the artifact offset floor moved into the seam (negative offsets serve head bytes on both transports); the five mutating MCP tools hold the app’s WriteGate (mirroring REST’s seven gated handlers); and MCP failure envelopes carry isError: true (D10, closed).
| # | Deviation |
|---|---|
| D1 | CLOSED (WP07 Task 10, commit cfba9c7; carried green through the WP08 close — test_run_get_catalog_tier_observe_control_hierarchy + the matrix’s observe-token case): run_get tier: seam fixed to catalog authority (observe); both tiers pinned |
| D2 | CLOSED (WP08 Task 6, commit 3ff6484): change_apply body — the 1.1.0 catalog’s REST schema omitted approver_token while the adapter forwarded it as a seam kwarg. The amendment ships as the versioned corpus revision interface-v1.1.1 (vendored byte-identical from docs/interface-v1.1.1/; manifest identity.interface_errata: "1.1.1"), adding approver_token as an OPTIONAL string property to change_apply’s requestBody — required and additionalProperties: false untouched, every other file a byte-copy, and the 1.1.0 corpus bytes unchanged (pinned against git HEAD digests by tests/contract/test_corpus_revision.py, which also pins that the 1.1.0 corpus rejects the token-bearing body the 1.1.1 corpus admits). The seam validates the token-bearing body the adapter sends; the adapter-path optionality (body.get) is mirrored by the kwarg’s absence for corpus-literal bodies. Errata rationale: docs/interface-v1.1.1/README.md |
| D3 | CLOSED (WP08 Task 1, side effect of D8; commits 33a34f6+6217668): the MCP tool-signature defaults still fire (fastmcp 4.0.3 does not enforce the pinned schema’s required at dispatch), but the seam now validates the defaulted payload — every empty-string default violates the corpus pattern/minLength, so an omitted required string param surfaces invalid_request on MCP too (parity with REST’s 400; test_interface_parity.py::test_invalid_request_required_param_rest_vs_mcp_default). Residuals, pinned: the paging defaults (limit=1, cursor=None/after=None) are schema-valid well-formed requests and still succeed over MCP — a transport default, not a validation gap; and fastmcp’s signature-based argument handling (numeric strings coerce to the annotated int; string-where-object is rejected pre-seam with fastmcp’s own non-contract error) remains the D3-family root, disclosed in test_mcp_signature_coercion_boundary |
| D4 | event evidence: the seam emits free-form evidence dicts while the contract’s evidence def is a closed {id, version, sha256} document ref. D12 exception (WP08 Task 3): authority_changed emits the def’s closed ref exactly (the takeover pins the binding document, the release the commissioned bench configuration) — the first kind reconciled; the legacy free-form family is pinned per-family by test_interface_parity.py::test_event_evidence_shape_deviation (realigned). Related finding, RESOLVED by the fix-wave rename (reviewer I1, controller-ruled): the def’s stream_id pattern ^[a-z][a-z0-9_.-]*$ forbids the seam’s colon-bearing bench:{bench_id} naming for EVERY kind (a live contract violation every emitted event inherited, pre-D12). The seam now constructs bench.{bench_id} (dot — in the pattern’s allowed class) at both naming sites (append_bench_event and events_get), NO errata: the D2 errata stays scoped to approver_token. Wire-visible change: stream_id values and the stream-scoped cursor namespace are now dot-form; pins flipped (test_seam_events, test_seam_admin, test_interface_parity, the takeover suites) and the takeover validators now check the VERBATIM vendored def (un-patched) — verbatim conformance is the green proof. No data migration: pre-release, no deployed clients (contract §12: no compatibility claim). WP08 close-out: PARTIAL — the reconciled halves are CLOSED (5119a5e D12 exception; 5173d61 stream_id, both naming sites); the legacy free-form family is RE-LEDGERED to the whole-branch review / interface-errata decision (same lane as D14 — reshaping emitted event payloads is an interface-version question, not a slice fix), pinned meanwhile by the realigned deviation test. G2 accepted deviation (WP09, 2026-09-14): the legacy free-form family is ACCEPTED for the G2 claim, resolution deferred to the dedicated interface-errata slice — registered in docs/evidence/poc/decisions/deferred-deviations.md (D4 section: reshaping emitted event evidence is a client-visible change to every event kind at once, an interface-version decision the pre-release window owes a deliberate slice, not an acceptance-package side effect). CLOSED (interface-errata slice, 2026-09-17, commit a1a99c4): every event kind now emits the closed doc-ref — an omitted evidence resolves to the run’s binding document or the commissioned bench configuration at the ONE construction site (append_bench_event), explicit emitters (the D12 takeovers, the change targets) pass the closed ref, and every served event schema-validates against the vendored event def (the deviation pin flipped to all-kinds conformance, REST==MCP). The stored run binding now carries the TRUE document digest (the caller’s binding_ref.sha256, aligning recovery-path terminal records with the normal path). Free-form operational context left the wire for corpus-sanctioned homes: worker poison errors and retention counts ride the gateway log keyed by run id (§10 excludes crash detail from event payloads), cancel/release/recovery reasons log server-side, change identity stays with change_get, and lease_changed events no longer carry the lease id in the run_id field (the def names a run). No corpus byte moved |
| D5 | ACCEPTED (WP08 close-out — unchanged, pinned): wire tools/list schemas are vendored-minus-$defs (the corpus $defs are unreferenced and inert; serve-time middleware is the SDK’s; the D2 errata corpus reproduces mcp-tools.json byte-identically, so the posture is untouched) |
| D6 | WP08 close-out — the 401 collapse is ACCEPTED as structural (no envelope can exist pre-auth; the WP02 map’s 403 semantics surface only at REST, pinned on both transports), and the REST-only payload_too_large half is CLOSED (commit 7008f71, D13 batch B): the MCP adapter now enforces the same limits["max_json_bytes"] ceiling over the tool-call arguments (canonically re-serialised; arguments arrive parsed, REST measures raw bytes) with the identical envelope text — pinned by test_interface_parity.py::test_payload_too_large_envelope_parity |
| D7 | ACCEPTED (WP08 close-out — unchanged, pinned): token-shape probes pinned on both transports: expired → 401 unauthenticated; wrong-audience → 403 forbidden; an stg-audience token as approver_token fails closed 403 |
| D8 | CLOSED (WP08 Task 1, commits 33a34f6+6217668): the seam validates every public operation’s payload against the vendored corpus — interfaces/validation.py::SeamValidator builds one schema per operation (the MCP inputSchema where a tool twin exists, else the OpenAPI requestBody; their required sets cross-checked at registry build with path params reconciled) and every Operations method validates as its first act after require_permission, so type-confused/shape-invalid inputs are the contract invalid_request on both transports (tests/unit/test_seam_validation.py + the parity suite’s D8 re-pins: 500/coerced-409/garbage-201 all flipped to 400). Boundary disclosed and pinned: fastmcp dispatch validates the tool SIGNATURE, not the vendored schema — numeric strings coerce to the annotated int and string-where-object is rejected pre-seam on MCP with a non-contract error, so those two classes never reach the seam over MCP; the seam itself rejects both (REST arms prove it end-to-end). Named residual, same root: extra-property enforcement is seam-only — the REST adapter’s named-field extraction (_field and the per-route keyword lists) DROPS unknown body properties before the seam (the request succeeds with the property silently ignored), and MCP rejects an unknown tool argument at dispatch with fastmcp’s own non-contract error (unexpected_keyword_argument); neither wire surfaces the contract invalid_request for this class, and wire-level enforcement requires adapter changes frozen out of this slice (test_interface_parity.py::test_extra_property_wire_truth_on_both_transports pins both wires) |
| D9 | CLOSED (WP08 Task 2, commit fe0fadd): §5 accept-time pre-checks live in the seam. run_start refuses synchronously — 409 conflict, before the request key is written — when (a) the bench has a live run (busy oracle: Store.list_run_states, states accepted/running/protecting; the store had no per-bench run view, so activity is derived from run_states) or (b) the binding document’s own request_id (content-store resolution, run_check’s idiom) differs from the §9 request id. §9 replay stays ahead of both: an already-filed request key returns the existing run before any §5 check (peek via find_request; accept_request remains the atomic race authority). Lease authority is modeled on the run row (runs.authority, migration v3: lease when a lease_id was named, else gateway) — takeover itself is Task 3. Postures pinned in tests/integration/test_seam_prechecks.py: an unstored binding digest is not decided at accept time (the worker’s poison guard keeps owning that async failure); the catalog blesses no busy-specific code, so contention rides conflict. Consequence disclosed: a second run can no longer queue behind a live one (“no queue waits indefinitely for control”) — the pre-D9 queued-cancel recorded-no-op pin was unreachable-premised and was replaced by the §5 contention pin (test_event_recovery.py::test_second_run_start_on_live_run_conflicts_and_frees_after_terminal); the worker’s FIFO drain remains an internal residual. Bonus closure: the Task-10 repeated-binding worker crash (same binding, different §9 id) is now refused at the seam (binding-mismatch conflict before acceptance) |
| D10 | MCP isError — FIXED by the WP07 final fix wave (acfb9e3/d0eda43): failure envelopes now serve isError: true with the contract envelope intact as structured content (test_interface_parity.py::test_mcp_is_error_flag_on_failure_and_success). Closed |
| D11 | CLOSED (WP08 Task 5, commit 3c1cb58; ALIGNED to the §8 letter): input coercion stays clamp-not-reject for the numeric bounds — limit/length clamp to [1, max], artifact offset floors at 0 (at the seam) — while the beyond-size offset case follows the contract letter exactly: “At EOF an offset equal to size yields zero bytes; offsets beyond size fail.” At-size still serves the zero-byte eof=true chunk (pinned); beyond-size now raises in ContentStore.artifact_chunk (Python slicing used to clamp it to a silent empty tail) and the seam maps it to invalid_request — the artifact exists (not not_found) and the caller already holds its size, so the un-existable window is a bad request. One construction site at the seam ⇒ identical envelopes on both transports; pinned in tests/integration/test_interface_parity.py::test_artifact_offset_letter_beyond_size_fails_at_size_serves_empty |
| D12 | CLOSED (WP08 Task 3, commits 5119a5e+5173d61): run_start honors lease_id as a commissioned-takeover assertion, woven into the §5 pre-check flow (_assert_bench_acceptable, after the generation fence and the §9 replay peek). Validation: resolve via _find_lease → unknown/released/expired/consumed not_found; wrong-bench conflict; expiry by seam clock not_found; holder §6 owner-or-admin forbidden (the shared is_owner_or_admin predicate — same one run_cancel scoping uses). A validated lease is CONSUMED before the request key is written (Store.consume_lease, state→released — the closed lease enum has no ‘overridden’ value; the authority_changed event + runs.authority='lease' are the audit trail), which (a) lets the worker’s reserve see an idle bench instead of bench_busy (the real-coordinator proof is the REST pin: a takeover run reaches passed), (b) makes the lease single-shot — a NEW request re-presenting a consumed lease fails not_found, while a §9 replay of the same request returns the existing run (the peek precedes validation), and (c) never waives §5 contention — another live run still conflicts (corpus: “another active controlling run … still conflicts”). authority_changed gains its first emitters: the takeover path (before enqueue, evidence = the closed binding-ref doc) and lease_release (evidence = the commissioned bench configuration ref). authority='lease' on the run row is now evidence a real validated lease was presented (closes the Task-2 unvalidated-column warning). Pinned in tests/integration/test_takeover.py (seam) + test_event_recovery.py::test_run_start_with_lease_takeover_over_http (wire); collateral pins realigned: the Task-2 authority pin now creates a real lease, the D4 evidence pin asserts the closed ref per-family. Residuals at WP08 close-out: the run row does not record WHICH lease (Task-2 concern) — lease:{id} encoding deferred, re-ledgered to the whole-branch ledger; and the seam-consume→worker-reserve window this row disclosed as raceable by an unrelated lease_create was CLOSED at Task 5 — lease_create now rejects live-run/live-lease contention (ed75450, D13 batch B), so the race no longer exists |
| D13 | batch A (store side) CLOSED (WP08 Task 4, commits d4ce50e+2c9b124): lease expiry is enforced on every seam busy read — Operations._live_lease treats the stored expires_at as the read-time oracle at the injected clock (limits.max_lease_ms bounds minting, never reads), so an expired-unreleased lease stops pinning its bench busy (the bench projection and the configuration-activation idle boundary both read it; an unparseable stamp cannot wedge the bench either; §5 run-side busy stays the LIVE_RUN_STATES oracle — expiry clears only the LEASE side); the events cursor-read is index-served — idx_events_stream_seq (migration v4), proven by EXPLAIN QUERY PLAN in tests/unit/test_store_hygiene.py (the v1 composite-PK autoindex was already usable; the explicit index makes the serving structure declared and planner-preferred); the accept_request→create_run crash-window is reconciled at startup — Store.reconcile_dangling_requests rides _recover_interrupted_runs (the lifespan’s ONE recovery entrypoint, the interrupted-run idiom): a RUN key whose run never materialized is purged so the same request id proceeds — the sweep is scoped to run keys (its anti-join must resolve in EVERY durable table — batch B added leases: a lease_renew §9 key’s run_id column holds the LEASE id, which has resolved since creation, so renewal keys are never dangling and keep replay protection; keys resolving in changes or leases, and run keys with a runs row — live or tombstoned — keep replay protection; review fix I1 pinned in test_d13_sweep_spares_change_submit_keys, the lease leg in test_d13_sweep_spares_lease_renew_keys), no event is emitted (nothing observable happened; the seven-kind fence has no vocabulary for it); hygiene fold: release_lease/consume_lease share one guarded _close_lease transition. batch B (transport/error side) CLOSED (WP08 Task 5, commits 7008f71, ee9eb0b, 59937f5, 563eb12, ed75450, 3101d75): the MCP transport carries the body ceiling (the D6 residual — every registered tool enforces limits["max_json_bytes"] over the canonically re-serialised tool-call arguments, same payload_too_large code and message text as REST’s pre-parse 413; arguments arrive parsed, REST measures raw bytes); internal_error is ONE construction site (errors.internal_failure) — message text transport-invariant, per-instance detail (the exception class only) parameterised into details, and every envelope mints uuid4().hex[:16] into correlation_id (the §10 “a correlation ID links internal diagnostics” link); change_apply’s undecided-crash envelope advertises retry: never (re-entry on an unknown change is the two-phase conflict — the state machine’s own answer, pinned — so same_request lied about re-entry semantics); §6 lease semantics closed: lease_create rejects conflicts with existing manual or gateway-owned authority (a live run OR a live lease → conflict; _assert_bench_acceptable’s busy oracle + the _live_lease idiom — closing the D12-disclosed consume→reserve window: an unrelated lease_create landing while a takeover run is live is refused instead of bench_busy-ing the worker’s reserve into the poison guard), and lease_renew files §9 request keys (a duplicate returns the SAME renewal, no second extension; a different body under the key → conflict; the replay peek precedes validation) and refuses to revive an expired-unreleased row (not_found at the seam clock — expired means expired). Fixture consequence disclosed: the §6 one-live-manual-lease-per-bench invariant re-structured three test surfaces — the parity module’s lease inventory spreads over seeded spare benches, the rest_routes setup lease moved to bench-two behind a terminal poll, and the retention-overtake wire test drives its twelve emissions as one create + eleven renewals (identical emission arithmetic). REMAINING (pre-registered re-ledger to WP09 — untouched by close-out; the measured read/admission targets decide fix vs permanent disclosure): the async single-loop posture runs blocking SQLite on one event loop (head-of-line blocking under concurrent calls). Adjacent residual disclosed, NOT folded: a crash between create_run and put_run_state still replays not_found (run exists, no queue-state row — same wedge class, needs a run-state-aware sweep; deferred because distinguishing a never-accepted run from a recovery-finalized run requires ordering analysis against _recover_interrupted_runs, a material diff). WP09 OUTCOME (Task 10, 2026-09-14): the async-posture remainder is CLOSED AS PERMANENT DISCLOSURE — protocol branch three applied as pre-stated (PRD load met, stress clean): prd-load-reads p95 4.746 ms vs target_p95_ms 500 and prd-load-acceptance p95 4.790 ms vs target_p95_ms 2000 (both verdict pass), stress-16 p95 24.614 ms / max 32.833 ms over 1600 reads at 16 observers (non-gating) — the head-of-line signal observed, bounded, non-material at PoC scale; single-loop blocking SQLite stands as the disclosed posture, no fix slice named. Decision record docs/evidence/poc/decisions/d13-async-posture.md (protocol verbatim, both tiers quoted from the artifacts, acceptance-sequential caveat, binding disclosure phrasing rules, reopen rule); measurements commit 939fb5e, retained evidence 545d527. The adjacent create_run→put_run_state crash-window residual REMAINS LEDGERED, untouched by this close |
| D14 | PARTIAL — correlation_id half CLOSED (WP09 Task 1, commit 3ebfcb5): every failure envelope mints a real id; the details six-key half remains RE-LEDGERED to the interface-errata slice. History (WP08): REGISTERED WITHOUT BEHAVIOR CHANGE (surfaced by the Task-5 review, registered 3101d75) and RE-LEDGERED to the whole-branch review / interface-v1.1.1 errata decision: the RENDERED error envelope diverged from the vendored $defs/error de-facto — non-internal_error failures rendered correlation_id: "" (the def requires minLength 1; only internal_failure minted one) and every failure renders details: {} (the def requires the six closed keys findings/current_revision/stream_id/oldest_sequence/current_sequence/retry_after_ms; only run_check’s findings ride it today, free-form). The Task-5 fix wave removed the one NEW violation — internal_failure no longer puts the exception class on the wire; it is logged server-side (errors._LOG, the app’s first operational module logger) keyed by the correlation_id, per §10’s “a correlation ID links internal diagnostics”. WP09 Task 1 (RED-first, 3ebfcb5): failure() now mints uuid4().hex[:16] on every envelope unless an explicit id is passed (internal_failure() unchanged — already minted and passed explicitly), so every rendered correlation_id satisfies the def’s minLength 1 on both transports (one mint call site per envelope family — failure() mints unless an id is passed; internal_failure() passes its own mint); the cross-transport parity pins mask exactly correlation_id (per-envelope mint — two transports are two requests, ids distinct by design; every other field exact). The details half remains RE-LEDGERED to the interface-errata slice — collapsing it would reshape every envelope clients see, an interface-version question; pinned as the rendered (not contract) shape by tests/unit/test_error_model.py::test_failure_body_is_the_rendered_error_envelope. G2 accepted deviation (WP09, 2026-09-14): the details six-key half is ACCEPTED for the G2 claim, resolution deferred to the same interface-errata slice as D4 — registered in docs/evidence/poc/decisions/deferred-deviations.md (D14-details section). CLOSED (interface-errata slice, 2026-09-17, commit 41d547a): every failure envelope now serves the CLOSED six-key details object — failure() takes typed detail parameters (findings / current_revision / stream_id / oldest_sequence / current_sequence / retry_after_ms) with no free-form escape hatch; the SeamValidator surfaces typed {field, reason} findings; event_gap carries the §7 watermarks plus the stream they address; the generation-conflict sites (run_start, lease_create, activation) carry current_revision; internal_failure serves the all-null closed object; and the rendered error schema-validates against the vendored $defs/error (pinned in test_error_model.py). B2 closed with it (07c3805): Failure.__post_init__ refuses an empty correlation_id — direct construction can no longer mint an id that violates minLength 1. No corpus byte moved — the def’s five nullable keys admit honest nulls |
| D15 | RE-LEDGERED (WP08 close-out → whole-branch ledger / future slice): M3 note (Task-5 review) — lease_create has no §9 replay peek ahead of the new §6 contention check — a same-request retry after a successful mint returns conflict (the bench holds the minted lease). Corpus-permitted (§9’s duplicate handling names starts and admin changes, not lease creation), and safer than the pre-Task-5 silent second mint — but asymmetric with lease_renew’s §9 keys. G2 accepted deviation (WP09, 2026-09-14): ACCEPTED for the G2 claim, remaining ledgered (not interface errata — a corpus-permitted asymmetry); named in the G2 gate record (docs/evidence/poc/g2-gate-record.md, Task 12) |
| D16 | RE-LEDGERED (WP08 close-out → whole-branch review, as a design question — deliberate no-behavior-change): REGISTERED (WP08 Task 7 review carry, 88cf5da; re-verified at close — bootstrap.py still pins work_root / "packages.lock.json", _admit_registry_target still passes session.lock_path): the admission lock identity is session-wide — build_registry_session pins one lock_path = work_root / "packages.lock.json" and every _admit_registry_target passes session.lock_path regardless of which bench/configuration target the admin change names, so sequential admissions rewrite the one lock document + .admission.json sidecar (byte-determinism pinned by tests/contract/test_registry_admission.py::test_lock_deterministic_across_admissions). Single-operator PoC posture makes this benign today (admissions serialize on the one lock file). Design question for whole-branch review: should the lock identity become per-target when multi-bench admission concurrency arrives, or does the session-wide lock remain the intended single admission ledger? G2 accepted deviation (WP09, 2026-09-14): ACCEPTED for the G2 claim, remaining ledgered (a design question, no wire divergence); named in the G2 gate record (docs/evidence/poc/g2-gate-record.md, Task 12) |
Licence/provenance display (Task 12 verdict: store-retained, not wire-exposed)
The ratified §D carry is served nowhere on the interface-v1.1.0 wire — by the schema’s own decision. The bench and device $defs are closed (additionalProperties: false) with no licence field, and the fallback landings are closed the same way: evidence_get’s data object admits exactly {evidence_id, kind, content_ref, artifact_id}, and document_get’s document object exactly {id, version, sha256}. The honest landing: the licence bootstrap admits stays in the benches and devices store rows (Task-2 columns; "proprietary" where the fixture lattice declares no licence) — queryable at the store, never wire-exposed. Pinned by tests/unit/test_seam_observe.py::test_bench_projection_carries_licence_where_schema_permits, which also pins the closed $defs, so a future interface version that admits a licence field flips that test and forces the wire carry.
Watch-flake closure — poison-survival test (WP08 Task 8, 2026-09-13)
The WP07 final gate disclosed a one-time trip of tests/integration/test_event_recovery.py::test_worker_survives_poisoned_build_run across its final full-suite batches (isolated re-runs were 3/3 green at the time). WP08 closure protocol, run 2026-09-13T15:30:05Z–15:32:13Z on a clean tree at main 78a8857 (macOS aarch64, CPython 3.13.13): twenty dedicated runs of the test interleaved with three full-suite runs (7 · full · 7 · full · 6 · full), uv run pytest … -q --junitxml per run. Result: 20/20 PASS (1.4–1.7 s each); fulls 683 passed / 0 failed / 0 errors / 0 skipped, three times (28.6 / 28.5 / 28.0 s). The test never tripped.
Disposition: evidence-backed no-repro. Recorded hypothesis (unproven): environment delta — cross-run state (temp-dir/SQLite residue or machine load from the concurrent gate batches the original trip occurred inside), not a deterministic code defect; the original one-time trip remains unexplained and unreproduced under the 20+3 protocol. Reopen rule: on any recurrence, capture DB/env/temp residue at the failing run and write a RED test before any fix.