From e7387b639c6d93b2a3bd56e196968729ebeb9193 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:25:50 +1000 Subject: [PATCH 01/16] =?UTF-8?q?docs(design):=20warpline=20interfaces=20?= =?UTF-8?q?=E2=80=94=20preflight=20consumer=20+=20per-SEI=20attestation=20?= =?UTF-8?q?read?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for warpline request item 5: (a) advisory warpline preflight read (warpline_preflight_get, sibling to the governance honesty reads, HTTP client mirroring HttpFiligreeClient) and (b) per-SEI attestation_get returning human-cleared attestation facts (not a proven-good verdict). Boundary: governance verdicts byte-identical when warpline is absent. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-06-24-legis-warpline-interfaces-design.md | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md diff --git a/docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md b/docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md new file mode 100644 index 0000000..c7be0ce --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md @@ -0,0 +1,167 @@ +# Legis ↔ warpline interfaces — preflight consumer + per-SEI attestation read — design + +**Date:** 2026-06-24 +**Status:** Design approved (brainstorm), pre-implementation +**Scope:** The Legis side of warpline's requested interfaces (request item 5): **(a)** read warpline affected-set summaries as *advisory* preflight facts, and **(b)** expose a per-SEI attestation/governance-posture read so warpline can implement governance-as-verification (Rung 2). The rename feed warpline consumes (`git_rename_list` / `git_rename_feed_get`) **already ships** and is not rebuilt here. + +--- + +## 1. Problem & motivation + +Warpline (a Weft suite member; impact-radius / reverify-worklist analysis over `base..head`) has requested two interfaces from Legis: + +1. **Advisory preflight context.** Before an agent acts, it is useful to see warpline's *affected set* (impact radius) and *reverify worklist* **next to** Legis's own governance honesty reads (`identity_gap_list`, `lineage_integrity_get`). Warpline is a heuristic impact analyzer; it must **never** decide a governance verdict — its output is purely advisory. + +2. **Governance-as-verification (Rung 2).** Warpline wants to treat an entity that Legis has **attested at a given commit** as "proven-good" and skip reverifying it. Legis exposes no per-SEI attestation read today (it only serves `serve` / `mcp` / the governance-gate). This is **optional/future** on warpline's side — without it, warpline reports `governance=unavailable` (with a reason triple) and proceeds. Exposing it lets warpline shrink its reverify worklist using Legis attestations. + +### The boundary that governs every decision below + +> **Legis remains the only governance / sign-off / attestation authority. Warpline context is purely advisory.** +> +> **Acceptance:** governance decisions are **byte-identical** whether warpline is present or absent. + +This is the same honesty discipline already pervasive in the codebase (the discriminated `checked` / `unavailable` / `diverged` reads, fail-closed verification). The novel risk is a *new* one: Legis is becoming an HTTP **client** of a sibling for the first time on a path that sits *next to* governance reads. The invariant is that warpline data is **structurally incapable** of reaching a verdict path — it lives in its own tool and is never an input to `policy_evaluate`, the gates, sign-off, or any honesty read. + +## 2. Goals / non-goals + +### Goals +- A new env-gated HTTP client (`HttpWarplineClient`, `WARPLINE_API_URL`) modeled exactly on `HttpFiligreeClient` — stdlib `urllib`, injectable `fetch`, loopback/HTTPS URL gating, response-size bound, no-redirect. +- A new MCP tool **`warpline_preflight_get`** (a *sibling*, not embedded in the governance reads) returning an honest discriminated read: `checked` (advisory facts) vs `unavailable` (with reasons). +- A new MCP tool **`attestation_get`** returning, for an SEI, the **verified human-cleared attestation facts** (no `proven_good` verdict), through the same fail-closed trail-verification path the other honesty reads use. +- An acceptance test proving governance verdicts are unchanged when `WARPLINE_API_URL` is unset vs set. +- Full surface bookkeeping (outputSchema, `_AGENT_TOOLS`, surface conformance, tool-count, output-schema conformance vectors). + +### Non-goals +- Rebuilding the rename feed — `git_rename_list` / `git_rename_feed_get` ship today. Two-way rename-parser conformance vectors remain tracked by the existing open ticket **`legis-c4cbf78fdb`** (G16) and are out of scope here. +- Legis becoming an MCP *client* of warpline (rejected in brainstorm; HTTP mirrors the established sibling pattern). +- Any "skip reverification" / proven-good decision inside Legis — that is warpline's Rung-2 call. Legis returns facts. +- Warpline writing to Legis, or Legis feeding warpline data into any governance computation. +- Pinning warpline's exact wire format. The client is built to a **documented inferred contract** (see §6) that is shape-validated; it is corrected when warpline's real format lands. + +## 3. Part (a) — warpline preflight consumer + +### 3.1 Client: `HttpWarplineClient` + +New module `src/legis/warpline_preflight/client.py`, a near-clone of `src/legis/filigree/client.py`: + +- `class WarplineError(RuntimeError)` — a warpline call failed at the transport or decode layer. +- `_validate_base_url` — http(s) + host required; `http` to a non-loopback host rejected unless `LEGIS_ALLOW_INSECURE_REMOTE_HTTP=1` (with the same logged warning Filigree emits — warpline responses are advisory but a tampered "nothing impacted" should still be opt-in only). +- `MAX_RESPONSE_BYTES = 1_000_000`, no-redirect opener, JSON content-type check. +- Injectable `Fetch` callable so tests run fully offline (no network). +- `@runtime_checkable` `WarplineClient` Protocol with the two methods. +- Two methods, both read-only `GET`: + - `impact_radius(base: str, head: str) -> dict` + - `reverify_worklist(base: str, head: str) -> dict` + +Wired in `build_runtime` exactly like Filigree: +```python +warpline = None +warpline_url = os.environ.get("WARPLINE_API_URL") +if warpline_url: + from legis.warpline_preflight.client import HttpWarplineClient + warpline = HttpWarplineClient(warpline_url) +``` +and held on `McpRuntime` as `warpline: Any | None = None`. + +### 3.2 Tool: `warpline_preflight_get` + +- **Input:** `{base: string (required), head?: string}` — `head` defaults to `"HEAD"` (mirrors `git_rename_feed_get`). +- **Handler `_tool_warpline_preflight_get`:** delegates to a transport-agnostic service function `read_warpline_preflight(warpline_client, base, head)` in a new `src/legis/service/preflight.py`, mirroring how `read_identity_gaps` / `read_lineage_integrity` live in `service/governance.py`. +- **Honest discriminated output** (the house pattern — an empty affected-set must NOT read as "nothing impacted"): + + `status: "checked"`: + ```json + {"status": "checked", "impact_radius": {...}, "reverify_worklist": {...}} + ``` + `status: "unavailable"` (client not configured, transport failure, **or payload shape mismatch**): + ```json + {"status": "unavailable", "unavailable": [{"reason": "warpline client not configured"}]} + ``` +- The service function calls both warpline methods; **either** failing degrades the whole read to `unavailable` with a reason (partial advisory context is not surfaced as `checked`). A `WarplineError` is caught and converted to an `unavailable` reason — it never escapes as `INTERNAL_ERROR`, exactly as `read_identity_gaps` converts `LoomweaveError`. +- Added to `_AGENT_TOOLS`. + +### 3.3 Why a sibling tool, not embedded + +`identity_gap_list` / `lineage_integrity_get` carry outputSchema conformance tests and *are* the governance authority surface this work must protect. Embedding an advisory external read into a governance honesty read muddies "Legis is the authority" and risks an advisory failure perturbing a governance read. A dedicated sibling keeps advisory and authority **structurally** separate; an agent composes a preflight view by calling all three. + +## 4. Part (b) — per-SEI attestation read + +### 4.1 Tool: `attestation_get` + +- **Input:** `{sei: string (required)}`. Legis does **not** accept a commit. Warpline resolves the SEI's content_hash at commit X via Loomweave (which it already queries for `timeline` / `changed`) and matches it against the `content_hash` Legis returns. The match — and the "skip reverify" decision — is warpline's Rung-2 call, not Legis's. +- **Service function `read_sei_attestations(runtime_records, sei)`** in `service/governance.py` (next to the other honesty reads). It consumes the **verified governance trail** via the existing `verified_records` / `_governance_trail_records` path, so a tampered trail raises `AuditIntegrityError → AUDIT_INTEGRITY_FAILURE` — a forged "attested" is never returned. +- **Honest discriminated output:** + + `status: "checked"`: + ```json + {"status": "checked", "sei": "", "attestations": [ + {"kind": "signoff_cleared", "content_hash": "...", "recorded_at": "...", "seq": 12, "signoff_seq": 7}, + {"kind": "operator_override", "content_hash": "...", "recorded_at": "...", "seq": 19} + ]} + ``` + `status: "unavailable"` (no governance trail wired — no `LEGIS_HMAC_KEY`, so no protected/sign-off gate): + ```json + {"status": "unavailable", "sei": "", "attestations": [], "unavailable": [{"reason": "..."}]} + ``` + +### 4.2 What counts as an attestation (decided: human-cleared only) + +Only governance records that represent a **human clearance** for the SEI count — the strongest "governed-good" signal warpline can safely skip reverification on: + +1. **Cleared sign-offs** — a `SIGNED_OFF` record (`extensions.signoff_state == "signed_off"`) whose `entity_key` is the queried SEI and `identity_stable` is true. Its `content_hash` is joined from the matching `PENDING` request via `extensions.request_seq` (the cleared record itself carries no loomweave content_hash; the request does, at `extensions.loomweave.content_hash`). `kind: "signoff_cleared"`, `signoff_seq` = the request seq. +2. **Protected operator-overrides** — an operator-override verdict record for the SEI (content_hash inline at `extensions.loomweave.content_hash`). `kind: "operator_override"`. + +**Explicitly excluded:** chill/coached self-clear overrides and `BLOCKED` verdicts — they are not proof of anything warpline should skip reverification on. (The decision is conservative on purpose; broadening the set later is additive.) + +The exact record discriminators (the operator-override marker, the SEI/`identity_stable` filter, the request-join) are pinned against the real record shapes in `enforcement/signoff.py` and `enforcement/protected.py` during implementation and covered by unit tests. + +## 5. Rename feed (part b, item 1) — confirm, don't rebuild + +`git_rename_list` (committed renames over a rev range) and `git_rename_feed_get` (base/head + optional working-tree renames, Loomweave-ready) already ship and are unchanged. They are what warpline consumes to keep its `timeline` / `changed` stable across file moves. The only follow-up — two-way rename-parser conformance vectors — is the existing open ticket `legis-c4cbf78fdb` (G16). No code change here. + +## 6. Inferred warpline contract (TO-CONFIRM) + +Warpline's real wire format was not supplied. The client is built to this **inferred, shape-validated** contract; a mismatch degrades to `unavailable` (never fabricates an affected-set). When warpline ships its real shape, only the parser + one fixture change. + +- `GET {base}/api/impact-radius?base=&head=` → object. Inferred shape: `{"affected": [{"sei": "...", "path": "...", ...}], "count": , ...}`. +- `GET {base}/api/reverify-worklist?base=&head=` → object. Inferred shape: `{"entries": [{"sei": "...", "reason": "...", ...}], "count": , ...}`. + +Shape validation is **minimal and tolerant**: the client requires each response to be a JSON object (else `WarplineError`); the *fields within* are passed through verbatim to the advisory payload. This keeps Legis from coupling to warpline's evolving internal vocabulary while still refusing to present a non-object/garbage response as `checked`. **⚠️ Confirm paths + payloads with warpline before treating happy-path parsing as final.** + +## 7. Honesty & error handling (the recurring bug class here) + +- **No silent empties.** Both new reads discriminate `checked` from `unavailable`. An unreachable/unconfigured warpline → `unavailable` with a reason, never an empty affected-set that reads as "nothing impacted". An unwired governance trail → `unavailable`, never an empty attestations list that reads as "never attested". +- **Advisory failure is contained.** A `WarplineError` is caught in the service layer and mapped to `unavailable`; it never becomes `INTERNAL_ERROR` and never perturbs a governance read (different tool, different call). +- **Attestation reads are fail-closed.** Tamper → `AUDIT_INTEGRITY_FAILURE` via the shared `verified_records` path. No forged attestation is ever returned. +- **Insecure transport is opt-in.** `http` to non-loopback warpline is rejected unless `LEGIS_ALLOW_INSECURE_REMOTE_HTTP=1`, with the same warning Filigree logs. + +## 8. Testing + +- **`tests/service/test_preflight.py`** — `read_warpline_preflight`: checked (both methods succeed via injected fetch); unavailable when client is `None`; unavailable on `WarplineError`; unavailable on non-object payload. +- **`tests/service/test_governance.py` (extend)** — `read_sei_attestations`: cleared sign-off surfaces with joined content_hash + signoff_seq; operator override surfaces; chill/coached self-clear and `BLOCKED` are excluded; SEI filter and `identity_stable` filter; unavailable when no trail wired; `AUDIT_INTEGRITY_FAILURE` on a tampered trail. +- **`tests/warpline_preflight/test_client.py`** — URL validation (loopback ok, remote http rejected unless opt-in), response-size bound, no-redirect, non-JSON content-type → `WarplineError`, offline via injected fetch. +- **`tests/mcp/test_server.py` (extend)** — both tools dispatch end-to-end; `warpline_preflight_get` returns `unavailable` when `WARPLINE_API_URL` unset; `attestation_get` happy path + unavailable. +- **`tests/mcp/test_output_schema_conformance.py` (extend)** — outputSchema vectors for both new tools. +- **Surface/conformance** — `tests/checks/test_check_surface.py` (or the surface conformance test) tool-count + `_AGENT_TOOLS` membership. +- **Acceptance / invariant — `tests/mcp/test_warpline_advisory_boundary.py` (new):** run a representative governance path (e.g. `policy_evaluate` / an override submit / a sign-off read) with `WARPLINE_API_URL` unset and again with it set to an injected warpline that returns arbitrary impact data; assert the governance result is **byte-identical**. This is the spine of the whole change. + +## 9. File manifest + +**New** +- `src/legis/warpline_preflight/__init__.py` +- `src/legis/warpline_preflight/client.py` — `HttpWarplineClient`, `WarplineClient` Protocol, `WarplineError`. +- `src/legis/service/preflight.py` — `read_warpline_preflight`. +- `tests/warpline_preflight/test_client.py` +- `tests/service/test_preflight.py` +- `tests/mcp/test_warpline_advisory_boundary.py` + +**Modified** +- `src/legis/mcp.py` — `McpRuntime.warpline`; `build_runtime` wiring; `warpline_preflight_get` + `attestation_get` tool definitions, handlers, `_TOOL_HANDLERS`, `_AGENT_TOOLS`; recovery hints for any new error codes. +- `src/legis/service/governance.py` — `read_sei_attestations`. +- `tests/service/test_governance.py`, `tests/mcp/test_server.py`, `tests/mcp/test_output_schema_conformance.py`, surface conformance test — extended. + +## 10. Open items / future state + +- **Warpline wire format** (§6) — confirm real paths + payloads; swap the inferred fixture. +- **Attestation set breadth** — human-cleared only for now; broadening to other verified records is additive if warpline asks. +- **Rename conformance vectors** — tracked separately as `legis-c4cbf78fdb` (G16). From 09b9e6ca723623f2510a3438464f4cace51e3aba Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 07:44:19 +1000 Subject: [PATCH 02/16] docs(plan): warpline interfaces TDD implementation plan 8-task plan for the advisory preflight consumer + per-SEI attestation read. Grounded + adversarially reviewed; Tasks 1-7 ready, Task 8 (classifier) BLOCKED pending owner ratification of the forge-proof discriminator. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...26-06-24-legis-warpline-interfaces-plan.md | 1108 +++++++++++++++++ 1 file changed, 1108 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-24-legis-warpline-interfaces-plan.md diff --git a/docs/superpowers/plans/2026-06-24-legis-warpline-interfaces-plan.md b/docs/superpowers/plans/2026-06-24-legis-warpline-interfaces-plan.md new file mode 100644 index 0000000..f83faaf --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-legis-warpline-interfaces-plan.md @@ -0,0 +1,1108 @@ +# Legis ↔ warpline interfaces — preflight consumer + per-SEI attestation read — TDD implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build the Legis side of warpline's two requested interfaces — an env-gated advisory HTTP preflight consumer (`warpline_preflight_get`) and a fail-closed per-SEI attestation read (`attestation_get`) — such that warpline data is structurally incapable of reaching any governance verdict path and governance verdicts are byte-identical whether `WARPLINE_API_URL` is set or unset. + +**Architecture:** `HttpWarplineClient` is a near-clone of `HttpFiligreeClient` (stdlib `urllib`, injectable `fetch`, loopback/HTTPS gating, response-size bound, no-redirect), held as a plain optional `warpline: Any | None` field on `McpRuntime` and read ONLY inside the new `warpline_preflight_get` handler. The preflight read lives in a new transport-agnostic `service/preflight.py` mirroring `read_identity_gaps`; the attestation read lives in `service/governance.py` next to the other discriminated honesty reads and consumes the existing fail-closed `verified_records` trail path. Both tools return the house discriminated `checked`/`unavailable` shape so no empty ever reads as "nothing impacted" / "never attested". + +**Tech Stack:** Python 3.13, stdlib only for the client (`urllib`, `http.client`, `ipaddress`, `json`, `logging`); `pytest` + `jsonschema` (`Draft202012Validator`) for tests; existing Legis MCP adapter (`src/legis/mcp.py`), service layer (`src/legis/service/`), and enforcement trail (`src/legis/enforcement/{protected,signoff}.py`). + +--- + +## Global Constraints + +These are load-bearing invariants. Copy them verbatim into every relevant task; do not soften. + +- **ADVISORY BOUNDARY.** Legis is the ONLY governance / sign-off / attestation authority. Warpline context is PURELY ADVISORY. `runtime.warpline` may be referenced ONLY inside the `warpline_preflight_get` handler (`_tool_warpline_preflight_get`) and the `read_warpline_preflight` service function. It must be STRUCTURALLY ABSENT from `policy_evaluate` / `_engine` / `_coached_engine` / the gates / sign-off / the honesty reads (`identity_gap_list`, `lineage_integrity_get`, `policy_boundary_check`) and from `read_sei_attestations` / `attestation_get`. +- **BYTE-IDENTICAL ACCEPTANCE.** Governance verdicts are BYTE-IDENTICAL whether `WARPLINE_API_URL` is set or unset. Warpline data must be structurally incapable of reaching a verdict path. +- **ASYMMETRIC ERROR RULE (`attestation_get` feeds warpline's skip-reverify decision).** A FALSE "attested" (surfacing a record a human never cleared) → warpline skips reverify on un-cleared code → SECURITY HOLE. An OMITTED real attestation → warpline reverifies something fine → wasted work, SAFE. Therefore EVERY ambiguous or failure case resolves toward "not attested": OMIT the record, or return the discriminated `status: "unavailable"`, NEVER toward surfacing it. An omission must still NOT read as a silent "never attested" empty — use the discriminated `unavailable` status with a `reason`, never a bare empty list under `status: "checked"` that did not actually check. +- **STDLIB-ONLY CLIENT.** `HttpWarplineClient` uses stdlib `urllib` with an injectable `Fetch = Callable[[str, str, "dict | None"], dict]` so tests run fully offline. No new dependency. +- **LOOPBACK / HTTPS GATING.** `http` to a non-loopback host is rejected (`WarplineError`) unless `LEGIS_ALLOW_INSECURE_REMOTE_HTTP == "1"` (string compare), which logs the same forgeable-responses warning Filigree emits. `localhost` and any `ipaddress.ip_address(host).is_loopback` host are allowed over `http`. +- **`MAX_RESPONSE_BYTES = 1_000_000`.** Responses are read with `resp.read(MAX_RESPONSE_BYTES + 1)` and rejected (`WarplineError`) if larger. +- **NO-REDIRECT.** `_NoRedirectHandler.redirect_request` returns `None`; a 3xx surfaces as `WarplineError("... redirect not allowed: ")`. +- **CLONE, do not refactor-share (ticket `legis-bc407a86ed`).** The URL-validation + fetch helpers are duplicated verbatim into `warpline_preflight`, NOT extracted to a shared module. Accepted duplication; do NOT plan a shared-helper refactor. + +--- + +## File Structure + +| File | New/Modified | Responsibility | +|---|---|---| +| `src/legis/warpline_preflight/__init__.py` | New | Package marker for the warpline preflight client. | +| `src/legis/warpline_preflight/client.py` | New | `HttpWarplineClient`, `@runtime_checkable WarplineClient` Protocol, `WarplineError`, the cloned `_validate_base_url`/`_urllib_fetch`/`_decode_json_response`/`_require_dict`/`_NoRedirectHandler`/`_open_no_redirect`/`_is_loopback` helpers, `MAX_RESPONSE_BYTES`, `Fetch`. Two read-only GET methods `impact_radius(base, head)` / `reverify_worklist(base, head)`. | +| `src/legis/service/preflight.py` | New | `read_warpline_preflight(warpline_client, base, head)` — transport-agnostic discriminated `checked`/`unavailable` read; catches `WarplineError`, never escapes as `INTERNAL_ERROR`. | +| `src/legis/service/__init__.py` | Modified | Export `read_warpline_preflight` (new `from legis.service.preflight import ...` line + `__all__` entry, per the `route_wardline_scan` precedent) and `read_sei_attestations` (existing governance import block + `__all__`). | +| `src/legis/service/governance.py` | Modified | `read_sei_attestations(verified_runtime_records, sei)` — fail-closed per-SEI attestation classifier (positive admission BLOCKED pending owner ratification; see Task 8). | +| `src/legis/mcp.py` | Modified | `McpRuntime.warpline` field (end of dataclass); `build_runtime` `WARPLINE_API_URL` gating + pass into ctor; `warpline_preflight_get` + `attestation_get` tool definitions, handlers, `_TOOL_HANDLERS` registration, `_AGENT_TOOLS` membership; `_governance_trail_records`-based attestation handler with the protected-gate fail-closed pre-gate. | +| `tests/warpline_preflight/__init__.py` | New | Test package marker. | +| `tests/warpline_preflight/test_client.py` | New | Client unit tests: URL validation, size bound, no-redirect, non-JSON content-type, offline-via-injected-fetch. | +| `tests/service/test_preflight.py` | New | `read_warpline_preflight` service tests. | +| `tests/service/test_governance.py` | Modified | `read_sei_attestations` classifier tests (verified-trail, tamper, SEI/`identity_stable` filters, omit rules). | +| `tests/mcp/test_warpline_advisory_boundary.py` | New | The byte-identical acceptance spine: governance path unset vs hostile-injected warpline. | +| `tests/mcp/test_server.py` | Modified | Dispatch + env-pairing + surface-set literal (22→24) + `runtime.warpline` structural-boundary tests. | +| `tests/mcp/test_output_schema_conformance.py` | Modified | outputSchema conformance vectors for both new tools. | + +--- + +## Task 1 — `HttpWarplineClient` + client unit tests + +**Files** +- Create: `src/legis/warpline_preflight/__init__.py`, `src/legis/warpline_preflight/client.py` +- Create test: `tests/warpline_preflight/__init__.py`, `tests/warpline_preflight/test_client.py` + +**Interfaces** +- Produces: `class WarplineError(RuntimeError)`; `Fetch = Callable[[str, str, "dict | None"], dict]`; `MAX_RESPONSE_BYTES = 1_000_000`; `@runtime_checkable class WarplineClient(Protocol)` with `impact_radius(self, base: str, head: str) -> dict[str, Any]` and `reverify_worklist(self, base: str, head: str) -> dict[str, Any]`; `class HttpWarplineClient.__init__(self, base_url: str, *, fetch: Fetch | None = None) -> None`. +- Consumes: stdlib only. + +Steps: + +- [ ] **Write failing test** `tests/warpline_preflight/test_client.py`: +```python +import json + +import pytest + +from legis.warpline_preflight.client import ( + HttpWarplineClient, + WarplineClient, + WarplineError, + MAX_RESPONSE_BYTES, +) + + +def _recorder(responses): + """An injectable Fetch that returns queued dicts and records calls.""" + calls = [] + + def fetch(method, url, body): + calls.append((method, url, body)) + return responses.pop(0) + + fetch.calls = calls + return fetch + + +def test_protocol_is_runtime_checkable(): + client = HttpWarplineClient("http://localhost:9100", fetch=_recorder([{}])) + assert isinstance(client, WarplineClient) + + +def test_impact_radius_is_a_get_with_base_head_query(): + fetch = _recorder([{"affected": [], "count": 0}]) + client = HttpWarplineClient("http://localhost:9100", fetch=fetch) + out = client.impact_radius("aaa", "bbb") + assert out == {"affected": [], "count": 0} + method, url, body = fetch.calls[0] + assert method == "GET" and body is None + assert url == "http://localhost:9100/api/impact-radius?base=aaa&head=bbb" + + +def test_reverify_worklist_is_a_get_with_base_head_query(): + fetch = _recorder([{"entries": [], "count": 0}]) + client = HttpWarplineClient("http://localhost:9100", fetch=fetch) + out = client.reverify_worklist("aaa", "bbb") + assert out == {"entries": [], "count": 0} + method, url, body = fetch.calls[0] + assert method == "GET" and body is None + assert url == "http://localhost:9100/api/reverify-worklist?base=aaa&head=bbb" + + +def test_non_object_response_is_a_warpline_error(): + client = HttpWarplineClient("http://localhost:9100", fetch=_recorder([["not", "a", "dict"]])) + with pytest.raises(WarplineError): + client.impact_radius("a", "b") + + +def test_loopback_http_ok_remote_http_rejected_unless_optin(monkeypatch): + monkeypatch.delenv("LEGIS_ALLOW_INSECURE_REMOTE_HTTP", raising=False) + HttpWarplineClient("http://127.0.0.1:9100") # loopback IP ok + HttpWarplineClient("http://localhost:9100") # localhost ok + HttpWarplineClient("https://warpline.example.com") # https ok + with pytest.raises(WarplineError, match="HTTPS unless it is loopback"): + HttpWarplineClient("http://warpline.example.com") + monkeypatch.setenv("LEGIS_ALLOW_INSECURE_REMOTE_HTTP", "1") + HttpWarplineClient("http://warpline.example.com") # opt-in permits it + + +def test_base_url_must_be_http_with_host(): + with pytest.raises(WarplineError, match="http\\(s\\) URL with a host"): + HttpWarplineClient("ftp://warpline") + with pytest.raises(WarplineError, match="http\\(s\\) URL with a host"): + HttpWarplineClient("not-a-url") + + +def test_response_too_large_via_real_decode_path(): + # Exercise the real _decode_json_response size guard with a fake resp object. + from legis.warpline_preflight.client import _decode_json_response + + big = json.dumps({"x": "y" * (MAX_RESPONSE_BYTES + 10)}).encode("utf-8") + + class _Resp: + headers = {"Content-Type": "application/json"} + + def read(self, n): + return big[:n] + + with pytest.raises(WarplineError, match="response too large"): + _decode_json_response(_Resp(), "GET test") + + +def test_non_json_content_type_rejected(): + from legis.warpline_preflight.client import _decode_json_response + + class _Resp: + headers = {"Content-Type": "text/html"} + + def read(self, n): + return b"" + + with pytest.raises(WarplineError, match="non-JSON content type"): + _decode_json_response(_Resp(), "GET test") + + +def test_no_redirect_handler_returns_none(): + from legis.warpline_preflight.client import _NoRedirectHandler + + h = _NoRedirectHandler() + assert h.redirect_request(None, None, 302, "Found", {}, "http://elsewhere") is None +``` + +- [ ] **Run to fail:** `uv run pytest tests/warpline_preflight/test_client.py -q` — fails with `ModuleNotFoundError: legis.warpline_preflight`. + +- [ ] **Implement** `src/legis/warpline_preflight/__init__.py` (empty) and `src/legis/warpline_preflight/client.py`. CLONE `src/legis/filigree/client.py` exactly, substituting `Filigree`→`Warpline`, DROP the `weft_signing` import (`_json_body_bytes`) — both methods are GET with `body=None`, so use plain `json.dumps(body).encode("utf-8")` in `_urllib_fetch`. Reuse the env var `LEGIS_ALLOW_INSECURE_REMOTE_HTTP` verbatim. Full module: +```python +"""Warpline preflight client — legis reads ADVISORY impact/reverify hints. + +Stdlib ``urllib`` with an injectable ``fetch`` so tests run offline; no new +dependency. SECURITY: Warpline is PURELY ADVISORY. This client exposes only +read-only GETs; nothing it returns may reach a governance verdict path +(policy_evaluate, the gates, sign-off, or the honesty reads). Governance +verdicts are byte-identical whether WARPLINE_API_URL is set or unset. +""" + +from __future__ import annotations + +import json +import http.client +import ipaddress +import logging +import os +import urllib.error +import urllib.parse +import urllib.request +from typing import Any, Callable, Protocol, runtime_checkable + +Fetch = Callable[[str, str, "dict | None"], dict] + +logger = logging.getLogger(__name__) + + +class WarplineError(RuntimeError): + """A Warpline call failed at the transport or decode layer.""" + + +MAX_RESPONSE_BYTES = 1_000_000 + + +@runtime_checkable +class WarplineClient(Protocol): + def impact_radius(self, base: str, head: str) -> dict[str, Any]: ... + def reverify_worklist(self, base: str, head: str) -> dict[str, Any]: ... + + +def _urllib_fetch( + method: str, url: str, body: dict | None, headers: dict[str, str] | None = None +) -> dict: + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, method=method) + if data is not None: + req.add_header("Content-Type", "application/json") + for name, value in (headers or {}).items(): + req.add_header(name, value) + try: + with _open_no_redirect(req) as resp: # noqa: S310 (trusted Warpline URL) + decoded = _decode_json_response(resp, f"{method} {url}") + except urllib.error.HTTPError as exc: + if 300 <= exc.code < 400: + raise WarplineError(f"{method} {url} redirect not allowed: {exc.code}") from exc + raise WarplineError(f"{method} {url} failed: {exc}") from exc + except (urllib.error.URLError, ValueError, OSError, http.client.HTTPException) as exc: + raise WarplineError(f"{method} {url} failed: {exc}") from exc + return _require_dict(decoded, f"{method} {url}") + + +class _NoRedirectHandler(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[override] + return None + + +def _open_no_redirect(req: urllib.request.Request) -> Any: + opener = urllib.request.build_opener(_NoRedirectHandler()) + return opener.open(req, timeout=10.0) + + +def _decode_json_response(resp: Any, context: str) -> Any: + headers = getattr(resp, "headers", {}) or {} + content_type = headers.get("Content-Type", "application/json") + if "json" not in content_type.lower(): + raise WarplineError(f"{context} returned non-JSON content type: {content_type}") + raw = resp.read(MAX_RESPONSE_BYTES + 1) + if len(raw) > MAX_RESPONSE_BYTES: + raise WarplineError(f"{context} response too large") + return json.loads(raw.decode("utf-8")) + + +def _require_dict(value: Any, context: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise WarplineError(f"{context} returned {type(value).__name__}, expected object") + return value + + +def _is_loopback(host: str) -> bool: + if host == "localhost": + return True + try: + return ipaddress.ip_address(host).is_loopback + except ValueError: + return False + + +def _validate_base_url(base_url: str) -> str: + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + raise WarplineError("Warpline base URL must be an http(s) URL with a host") + allow_insecure_remote = os.environ.get("LEGIS_ALLOW_INSECURE_REMOTE_HTTP") == "1" + if parsed.scheme == "http" and not _is_loopback(parsed.hostname): + if not allow_insecure_remote: + raise WarplineError("Warpline base URL must use HTTPS unless it is loopback") + # ID-SEI-1: plaintext to a remote Warpline. TLS is the only integrity + # control on responses (the request HMAC authenticates requests, not + # responses), so an on-path attacker can tamper with what legis reads + # back. Dev/loopback only; never production. + logger.warning( + "LEGIS_ALLOW_INSECURE_REMOTE_HTTP=1 is permitting a plaintext HTTP " + "connection to non-loopback Warpline host %r; responses are forgeable " + "without TLS. Dev/loopback use only.", + parsed.hostname, + ) + return base_url.rstrip("/") + + +class HttpWarplineClient: + def __init__( + self, + base_url: str, + *, + fetch: Fetch | None = None, + ) -> None: + self._base = _validate_base_url(base_url) + self._fetch = fetch if fetch is not None else self._transport_fetch + + def _transport_fetch(self, method: str, url: str, body: dict | None) -> dict: + return _urllib_fetch(method, url, body, {}) + + def impact_radius(self, base: str, head: str) -> dict[str, Any]: + q = urllib.parse.urlencode({"base": base, "head": head}) + return _require_dict( + self._fetch("GET", f"{self._base}/api/impact-radius?{q}", None), + "Warpline impact_radius", + ) + + def reverify_worklist(self, base: str, head: str) -> dict[str, Any]: + q = urllib.parse.urlencode({"base": base, "head": head}) + return _require_dict( + self._fetch("GET", f"{self._base}/api/reverify-worklist?{q}", None), + "Warpline reverify_worklist", + ) +``` +> NOTE — INFERRED CONTRACT (spec §6, TO-CONFIRM): the route paths `/api/impact-radius`, `/api/reverify-worklist` and the `base`/`head` query-param names are inferred from the Filigree GET pattern, NOT grounded in a warpline spec. When warpline ships its real shape only this client's URL construction + one fixture change. The shape-validation is intentionally minimal: `_require_dict` rejects a non-object (→ `WarplineError`), fields pass through verbatim. + +- [ ] **Add a clone-parity guard.** The clone-not-share decision (ticket `legis-bc407a86ed`) means warpline's SSRF / redirect / DoS primitives can silently diverge from filigree's on a future security patch. Pin them — the repo precedent is the canonical-JSON golden mirror against Wardline. Append to `tests/warpline_preflight/test_client.py`: +```python +import inspect + +import legis.filigree.client as fc +import legis.warpline_preflight.client as wc + + +def _normalize(src): + # The ONLY intended differences are the sibling name and its error class. + return src.replace("Filigree", "Warpline").replace("filigree", "warpline") + + +def test_security_primitives_are_faithful_clones_of_filigree(): + # If a future patch hardens filigree's SSRF/redirect/DoS handling this fails + # loudly so warpline is patched in lockstep. _urllib_fetch is EXCLUDED — it + # intentionally drops filigree's weft_signing body bytes (warpline is GET-only). + for name in ("_validate_base_url", "_is_loopback", "_open_no_redirect", "_decode_json_response"): + assert _normalize(inspect.getsource(getattr(fc, name))) == inspect.getsource( + getattr(wc, name) + ), f"{name} diverged from the filigree clone" + assert _normalize(inspect.getsource(fc._NoRedirectHandler)) == inspect.getsource( + wc._NoRedirectHandler + ) +``` + +- [ ] **Run to pass:** `uv run pytest tests/warpline_preflight/test_client.py -q`. + +- [ ] **Commit:** `feat(warpline): add stdlib HttpWarplineClient advisory preflight client`. + +--- + +## Task 2 — `read_warpline_preflight` service function + tests + +**Files** +- Create: `src/legis/service/preflight.py` +- Modify: `src/legis/service/__init__.py` +- Create test: `tests/service/test_preflight.py` + +**Interfaces** +- Produces: `read_warpline_preflight(warpline_client: WarplineClient | None, base: str, head: str) -> dict[str, Any]` returning either `{"status": "checked", "impact_radius": {...}, "reverify_worklist": {...}}` or `{"status": "unavailable", "unavailable": [{"reason": }]}`. +- Consumes: `legis.warpline_preflight.client.WarplineError` (caught locally, lazy import — mirrors `read_identity_gaps` catching `LoomweaveError`). + +Steps: + +- [ ] **Write failing test** `tests/service/test_preflight.py`: +```python +from legis.service.preflight import read_warpline_preflight +from legis.warpline_preflight.client import WarplineError + + +class _OkWarpline: + def impact_radius(self, base, head): + return {"affected": [{"sei": "S1"}], "count": 1} + + def reverify_worklist(self, base, head): + return {"entries": [{"sei": "S1", "reason": "edited"}], "count": 1} + + +class _ImpactRaisesWarpline: + def impact_radius(self, base, head): + raise WarplineError("boom") + + def reverify_worklist(self, base, head): + return {"entries": [], "count": 0} + + +class _WorklistRaisesWarpline: + def impact_radius(self, base, head): + return {"affected": [], "count": 0} + + def reverify_worklist(self, base, head): + raise WarplineError("timeout") + + +def test_checked_when_both_methods_succeed(): + out = read_warpline_preflight(_OkWarpline(), "aaa", "bbb") + assert out == { + "status": "checked", + "impact_radius": {"affected": [{"sei": "S1"}], "count": 1}, + "reverify_worklist": {"entries": [{"sei": "S1", "reason": "edited"}], "count": 1}, + } + + +def test_unavailable_when_client_is_none_not_a_silent_empty(): + out = read_warpline_preflight(None, "aaa", "bbb") + assert out["status"] == "unavailable" + assert out["unavailable"] == [{"reason": "warpline client not configured"}] + # ASYMMETRIC: never an empty affected-set that reads as "nothing impacted". + assert "impact_radius" not in out + + +def test_unavailable_when_impact_radius_raises_warpline_error(): + out = read_warpline_preflight(_ImpactRaisesWarpline(), "aaa", "bbb") + assert out["status"] == "unavailable" + assert out["unavailable"][0]["reason"].startswith("warpline check failed:") + + +def test_unavailable_when_worklist_raises_warpline_error(): + # Partial advisory context is NOT surfaced as checked — either method failing + # degrades the WHOLE read to unavailable. + out = read_warpline_preflight(_WorklistRaisesWarpline(), "aaa", "bbb") + assert out["status"] == "unavailable" + assert out["unavailable"][0]["reason"].startswith("warpline check failed:") + + +def test_warpline_error_never_escapes_as_internal_error(): + # The transport error is caught and converted, never re-raised. + out = read_warpline_preflight(_ImpactRaisesWarpline(), "aaa", "bbb") + assert out["status"] == "unavailable" # no exception propagated +``` + +- [ ] **Run to fail:** `uv run pytest tests/service/test_preflight.py -q` — fails: no `legis.service.preflight`. + +- [ ] **Implement** `src/legis/service/preflight.py`: +```python +"""The warpline advisory preflight read — discriminated checked/unavailable. + +SECURITY: warpline is PURELY ADVISORY. This read is a SIBLING of the governance +honesty reads, never embedded in one; a failure here is contained as +``unavailable`` and never escapes as INTERNAL_ERROR, exactly as +``read_identity_gaps`` converts a ``LoomweaveError``. An unreachable/unconfigured +warpline → ``unavailable`` with a reason, never an empty affected-set that reads +as "nothing impacted". +""" + +from __future__ import annotations + +from typing import Any + + +def read_warpline_preflight( + warpline_client: Any | None, base: str, head: str +) -> dict[str, Any]: + from legis.warpline_preflight.client import WarplineError + + if warpline_client is None: + return { + "status": "unavailable", + "unavailable": [{"reason": "warpline client not configured"}], + } + try: + impact = warpline_client.impact_radius(base, head) + worklist = warpline_client.reverify_worklist(base, head) + except WarplineError as exc: + return { + "status": "unavailable", + "unavailable": [{"reason": f"warpline check failed: {exc}"}], + } + return { + "status": "checked", + "impact_radius": impact, + "reverify_worklist": worklist, + } +``` + +- [ ] **Export** in `src/legis/service/__init__.py`: add `from legis.service.preflight import read_warpline_preflight` (following the `from legis.service.wardline import route_wardline_scan` precedent) and add `"read_warpline_preflight"` to `__all__`. + +- [ ] **Run to pass:** `uv run pytest tests/service/test_preflight.py -q`. + +- [ ] **Commit:** `feat(warpline): add read_warpline_preflight discriminated service read`. + +--- + +## Task 3 — `warpline_preflight_get` tool wiring + `build_runtime` env gating + dispatch tests + +**Files** +- Modify: `src/legis/mcp.py` +- Modify test: `tests/mcp/test_server.py` + +**Interfaces** +- Produces: MCP tool `warpline_preflight_get` (input `{base: string (required), head?: string}`); handler `_tool_warpline_preflight_get(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, Any]`; `McpRuntime.warpline: Any | None = None`; `build_runtime` wiring gated on `WARPLINE_API_URL`. +- Consumes: `read_warpline_preflight` (Task 2); `HttpWarplineClient` (Task 1); `_tool_result`, `_require`, `_schema`, `_one_of` (mcp.py). + +Steps: + +- [ ] **Write failing tests** appended to `tests/mcp/test_server.py` (reusing the in-file fixtures `_runtime` at lines 72-92 and `call_tool`): +```python +def test_build_runtime_wires_warpline_from_env(monkeypatch, tmp_path): + from legis.mcp import build_runtime + from legis.warpline_preflight.client import HttpWarplineClient + + monkeypatch.setenv("WARPLINE_API_URL", "http://localhost:9100") + monkeypatch.setenv("LEGIS_SOURCE_ROOT", str(tmp_path)) + monkeypatch.delenv("LEGIS_HMAC_KEY", raising=False) # engine-only: no protected gate + # NOTE: build_runtime(agent_id) takes ONLY agent_id (mcp.py:200); source root + # and DBs are env-driven (LEGIS_SOURCE_ROOT, mcp.py:275). There is NO + # source_root= kwarg — passing one raises TypeError before any assertion. + runtime = build_runtime("agent-x") + assert isinstance(runtime.warpline, HttpWarplineClient) + + +def test_build_runtime_leaves_warpline_unwired_without_env(monkeypatch, tmp_path): + from legis.mcp import build_runtime + + monkeypatch.delenv("WARPLINE_API_URL", raising=False) + monkeypatch.setenv("LEGIS_SOURCE_ROOT", str(tmp_path)) + runtime = build_runtime("agent-x") + assert runtime.warpline is None + + +def test_build_runtime_degrades_warpline_to_none_on_bad_url(monkeypatch, tmp_path): + # A misconfigured ADVISORY url must NOT crash the sole governance authority + # at startup; it degrades to no advisory context (governance unaffected). + from legis.mcp import build_runtime + + monkeypatch.setenv("WARPLINE_API_URL", "not-a-valid-url") + monkeypatch.setenv("LEGIS_SOURCE_ROOT", str(tmp_path)) + monkeypatch.delenv("LEGIS_HMAC_KEY", raising=False) + runtime = build_runtime("agent-x") + assert runtime.warpline is None + + +def test_warpline_preflight_get_unavailable_when_unwired(tmp_path): + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) # warpline defaults to None + result = call_tool(runtime, "warpline_preflight_get", {"base": "aaa"}) + assert not result.get("isError") + assert result["structuredContent"] == { + "status": "unavailable", + "unavailable": [{"reason": "warpline client not configured"}], + } + + +def test_warpline_preflight_get_checked_with_injected_client(tmp_path): + from legis.mcp import call_tool + + class _FakeWarpline: + def impact_radius(self, base, head): + return {"affected": [{"sei": "S1"}], "count": 1} + + def reverify_worklist(self, base, head): + return {"entries": [], "count": 0} + + runtime, _store = _runtime(tmp_path) + runtime.warpline = _FakeWarpline() + result = call_tool(runtime, "warpline_preflight_get", {"base": "aaa", "head": "bbb"}) + assert not result.get("isError") + sc = result["structuredContent"] + assert sc["status"] == "checked" + assert sc["impact_radius"] == {"affected": [{"sei": "S1"}], "count": 1} + assert sc["reverify_worklist"] == {"entries": [], "count": 0} +``` + +- [ ] **Run to fail:** `uv run pytest tests/mcp/test_server.py -q -k warpline_preflight` — fails: `UNKNOWN_TOOL` / no `warpline` field. + +- [ ] **Implement (a) the `McpRuntime` field.** At the END of the `McpRuntime` dataclass (after `coached_engine: EnforcementEngine | None = None`, mcp.py:178 — field-order is load-bearing because of the positional ctor at `tests/mcp/test_server.py:150`): +```python + coached_engine: EnforcementEngine | None = None + warpline: Any | None = None # advisory sibling; NEVER read by a verdict path +``` + +- [ ] **Implement (b) the `build_runtime` env-gating block.** Mirror the FILIGREE block (mcp.py:221-226); insert adjacent to it. UNLIKE the Filigree block, wrap construction in try/except — warpline is PURELY ADVISORY, so a misconfigured URL must never crash the governance authority at startup (degrade to `None`; `import logging` is already present at the top of mcp.py): +```python + warpline = None + warpline_url = os.environ.get("WARPLINE_API_URL") + if warpline_url: + from legis.warpline_preflight.client import HttpWarplineClient, WarplineError + + try: + warpline = HttpWarplineClient(warpline_url) + except WarplineError: + logging.getLogger(__name__).warning( + "WARPLINE_API_URL is set but invalid; warpline advisory context " + "disabled (governance unaffected)." + ) + warpline = None +``` +and add `warpline=warpline,` to the `McpRuntime(...)` return (near the `filigree=filigree,` line, mcp.py:283). + +- [ ] **Implement (c) the tool definition** in `tool_definitions()` (mcp.py:351+), modeled on `git_rename_feed_get` (mcp.py:809-840). Use `_one_of` for the discriminated output: +```python + { + "name": "warpline_preflight_get", + "description": ( + "ADVISORY preflight context from the warpline sibling: impact " + "radius + reverify worklist over base..head. Purely advisory — " + "NEVER a governance verdict. Discriminated: 'checked' carries the " + "advisory facts; 'unavailable' (client unconfigured, transport " + "failure, or payload shape mismatch) carries reasons. Never read a " + "missing 'checked' as 'nothing impacted'." + ), + "inputSchema": _schema(["base"], {"base": string, "head": string}), + "outputSchema": _one_of( + [ + _schema( + ["status", "impact_radius", "reverify_worklist"], + { + "status": {"type": "string", "enum": ["checked"]}, + "impact_radius": {"type": "object"}, + "reverify_worklist": {"type": "object"}, + }, + ), + _schema( + ["status", "unavailable"], + { + "status": {"type": "string", "enum": ["unavailable"]}, + "unavailable": { + "type": "array", + "items": _schema(["reason"], {"reason": string}), + }, + }, + ), + ] + ), + }, +``` + +- [ ] **Implement (d) the handler** (near the other read handlers): +```python +def _tool_warpline_preflight_get(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, Any]: + from legis.service.preflight import read_warpline_preflight + + return _tool_result( + read_warpline_preflight( + runtime.warpline, + base=_require(args, "base"), + head=args.get("head", "HEAD"), + ) + ) +``` + +- [ ] **Implement (e) the three registries** (test `test_tool_registries_are_in_sync` enforces equality): + - `_AGENT_TOOLS` (mcp.py:81-106): add `"warpline_preflight_get",`. + - `_TOOL_HANDLERS` (mcp.py:2421-2444): add `"warpline_preflight_get": _tool_warpline_preflight_get,`. + - tool_definitions(): the entry from step (c). + +- [ ] **Run to pass:** `uv run pytest tests/mcp/test_server.py -q -k warpline_preflight`. + +- [ ] **Commit:** `feat(mcp): wire warpline_preflight_get advisory sibling tool`. + +--- + +## Task 4 — Byte-identical advisory-boundary acceptance spine (`test_warpline_advisory_boundary.py`) + +This is the spine of the whole change. It is its own task and proves the invariant directly. + +**Files** +- Create test: `tests/mcp/test_warpline_advisory_boundary.py` + +**Interfaces** +- Consumes: `build_runtime`, `call_tool`, the existing runtime/store fixtures. + +Steps: + +- [ ] **Write failing test** `tests/mcp/test_warpline_advisory_boundary.py`. It runs representative governance paths (a `policy_evaluate`, an override submit, and a sign-off read) twice — with `WARPLINE_API_URL` unset, and again with it set to an injected HOSTILE warpline returning arbitrary impact data — and asserts byte-identical results. Plus the structural-boundary test that `runtime.warpline` is referenced in no verdict-path function: +```python +import inspect +import json + +from legis.policy.grammar import AllowlistBoundary, PolicyGrammar + + +class _HostileWarpline: + """Returns arbitrary/garbage advisory data to prove it cannot perturb a verdict.""" + + def impact_radius(self, base, head): + return {"affected": [{"sei": "EVERYTHING"}], "count": 9999, "block": True} + + def reverify_worklist(self, base, head): + return {"entries": [{"sei": "EVERYTHING", "reason": "force"}], "count": 9999} + + +def _seed_real_verdict_runtime(tmp_path): + """A runtime that returns REAL, DETERMINISTIC verdicts. + + Uses the _runtime fixture's FixedClock (timestamps identical across runs) and + registers a real grammar so policy_evaluate returns an actual VIOLATION / + UNKNOWN verdict — NOT an error envelope. An error envelope on BOTH sides would + make the byte-identity assertion pass trivially and prove nothing — the exact + defect the first draft of this test had. Mirrors the seeding in + test_policy_evaluate_returns_unknown_distinct_from_clear (test_server.py:1225). + """ + tmp_path.mkdir(parents=True, exist_ok=True) + runtime, _store = _runtime(tmp_path) # FixedClock("2026-06-02T12:00:00+00:00") + grammar = PolicyGrammar() + grammar.register(AllowlistBoundary("imports", frozenset({"json"}))) + runtime.grammar = grammar + return runtime + + +def _run_governance_paths(runtime): + """Drive REAL verdict paths and return their structuredContent blobs.""" + from legis.mcp import call_tool + + blobs = [ + # A real VIOLATION verdict (socket not in the {json} allowlist). + call_tool( + runtime, "policy_evaluate", {"policy": "imports", "target": {"value": "socket"}} + ).get("structuredContent"), + # A real UNKNOWN verdict (unknown policy -> provenance gap). + call_tool( + runtime, "policy_evaluate", {"policy": "missing", "target": {}} + ).get("structuredContent"), + ] + # GUARD: these MUST be real verdicts, never error envelopes — otherwise the + # byte-identity assertion below is vacuous. + assert blobs[0]["outcome"] == "VIOLATION" and blobs[0]["provenance_gap"] is False + assert blobs[1]["outcome"] == "UNKNOWN" and blobs[1]["provenance_gap"] is True + return blobs + + +def test_governance_verdicts_byte_identical_warpline_unset_vs_hostile(tmp_path): + # Everything is held IDENTICAL across the two runtimes (same FixedClock, same + # seeded grammar) EXCEPT runtime.warpline. If warpline data could reach a + # verdict path, the hostile side would diverge. + runtime_unset = _seed_real_verdict_runtime(tmp_path / "a") + runtime_unset.warpline = None + unset = _run_governance_paths(runtime_unset) + + runtime_set = _seed_real_verdict_runtime(tmp_path / "b") + runtime_set.warpline = _HostileWarpline() # structurally present, hostile + setval = _run_governance_paths(runtime_set) + + assert json.dumps(unset, sort_keys=True) == json.dumps(setval, sort_keys=True) + + +def test_runtime_warpline_referenced_in_no_verdict_path_function(): + # STRUCTURAL (defense-in-depth): runtime.warpline must appear in NO + # verdict-path / honesty-read source. NOTE inspect.getsource is a SHALLOW text + # scan — it sees only these named functions, not helpers they call — so this + # COMPLEMENTS, never replaces, the byte-identity test above. + import legis.mcp as mcp + + verdict_path_fns = [ + mcp._tool_policy_evaluate, + mcp._engine, + mcp._coached_engine, + mcp._governance_trail_records, + mcp._tool_identity_gap_list, + mcp._tool_lineage_integrity_get, + mcp._tool_policy_boundary_check, + mcp._tool_signoff_status_get, + mcp._tool_override_submit, + ] + for fn in verdict_path_fns: + src = inspect.getsource(fn) + assert ".warpline" not in src, f"{fn.__name__} references warpline" +``` +> If any function name above differs in the codebase, adjust to the actual symbol; the set MUST cover `policy_evaluate`, `_engine`, `_coached_engine` (mcp.py:1526), the gates, sign-off, and the three honesty reads (`identity_gap_list`, `lineage_integrity_get`, `policy_boundary_check`). Task 5 adds `mcp._tool_attestation_get` to this list once that handler exists. + +- [ ] **Run to fail then pass:** Before Task 3 wiring this fails to import the tools; after Task 3 it passes. Run `uv run pytest tests/mcp/test_warpline_advisory_boundary.py -q`. The seeding (`_seed_real_verdict_runtime`) is identical on both sides and produces REAL deterministic verdicts via `FixedClock`, so the byte-identity assertion is meaningful — it would FAIL if warpline presence perturbed a verdict, and the in-test guard rejects a vacuous error-envelope pass. + +- [ ] **Commit:** `test(warpline): byte-identical advisory-boundary acceptance spine`. + +--- + +## Task 5 — `attestation_get` structural scaffolding: schema, registries, no-trail `unavailable`, tamper `AUDIT_INTEGRITY_FAILURE` + +This task builds everything about `attestation_get` EXCEPT the positive-admission classifier (which is BLOCKED — Task 8). The fail-closed paths, the discriminated schema, and the three-registry registration are concrete and testable now. + +**Files** +- Modify: `src/legis/mcp.py`, `src/legis/service/governance.py`, `src/legis/service/__init__.py` +- Modify test: `tests/mcp/test_server.py`, `tests/service/test_governance.py` + +**Interfaces** +- Produces: MCP tool `attestation_get` (input `{sei: string (required)}`); handler `_tool_attestation_get(runtime, args)`; service stub `read_sei_attestations(verified_runtime_records, sei) -> dict[str, Any]` returning the discriminated `checked`/`unavailable` shape. The handler applies the FAIL-CLOSED PRE-GATE: `if runtime.protected_gate is None: return unavailable` (cannot verify signatures in an engine-only deployment), then reads through `_governance_trail_records(runtime)` (which raises `AuditIntegrityError` on a tampered protected trail). +- Consumes: `_governance_trail_records` (mcp.py:2173), `verified_records` (governance.py:156), `AuditIntegrityError` (mapped to `AUDIT_INTEGRITY_FAILURE` by the MCP adapter). + +Steps: + +- [ ] **Write failing tests.** In `tests/mcp/test_server.py` (reusing `_runtime` and `call_tool`; the tamper test defines its OWN inline `_TamperVerifier` / `_FakeProtectedGate` below — do NOT reuse `_TamperedLedger` at test_server.py:2243, which raises `BindingError` from the closure gate, a different model): +```python +def test_attestation_get_unavailable_when_no_protected_gate(tmp_path): + # ENGINE-ONLY DEPLOYMENT: no LEGIS_HMAC_KEY -> runtime.protected_gate is None. + # The trail is not signature-verifiable, so attestation_get MUST return a + # success-envelope unavailable, NOT a silent empty that reads as "never attested". + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) # no protected gate wired + assert runtime.protected_gate is None + result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert not result.get("isError") + sc = result["structuredContent"] + assert sc["status"] == "unavailable" + assert sc["sei"] == "mod.fn#1" + assert sc["attestations"] == [] + assert sc["unavailable"] and "reason" in sc["unavailable"][0] + + +def test_attestation_get_tamper_yields_audit_integrity_failure(tmp_path): + # FAIL-CLOSED: a tampered protected trail -> AUDIT_INTEGRITY_FAILURE, nothing + # surfaced. Build a protected runtime whose trail verifier raises TamperError. + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) + + class _TamperVerifier: + def verify(self, records): + from legis.enforcement.protected import TamperError + + raise TamperError("record 4 hash mismatch") + + class _FakeProtectedGate: + def records(self): + return ["bad-record"] + + runtime.protected_gate = _FakeProtectedGate() + runtime.trail_verifier = _TamperVerifier() + result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert result.get("isError") + assert result["structuredContent"]["error_code"] == "AUDIT_INTEGRITY_FAILURE" +``` +And in `tests/service/test_governance.py` (reusing `_FakeProtectedGate` / `_TamperVerifier` / `verified_records` imports at lines 227-279): +```python +def test_read_sei_attestations_returns_checked_shape_on_empty_verified_trail(): + from legis.service.governance import read_sei_attestations + + out = read_sei_attestations([], "mod.fn#1") + assert out["status"] == "checked" + assert out["sei"] == "mod.fn#1" + assert out["attestations"] == [] +``` + +- [ ] **Run to fail:** `uv run pytest tests/mcp/test_server.py tests/service/test_governance.py -q -k attestation` — fails: `UNKNOWN_TOOL` / no `read_sei_attestations`. + +- [ ] **Implement (a) the service stub** in `src/legis/service/governance.py` next to `read_identity_gaps` / `read_lineage_integrity`. The positive-admission classifier body is BLOCKED (Task 8); ship the discriminated skeleton that always returns `checked` with an empty list for now (the no-trail `unavailable` discrimination is owned by the HANDLER pre-gate, not this function, because a pre-materialized `runtime_records` list cannot carry the verified/unverified distinction — validator high finding): +```python +def read_sei_attestations(verified_runtime_records: list, sei: str) -> dict[str, Any]: + """Per-SEI human-cleared attestation facts from the VERIFIED governance trail. + + ASYMMETRIC ERROR RULE: a FALSE "attested" lets warpline skip reverify on + un-cleared code (security hole); an OMITTED attestation only wastes work + (safe). Every ambiguous/failure case therefore resolves toward "not attested" + — omit the record, never surface it. ``verified_runtime_records`` MUST already + have come through ``verified_records`` — the handler guarantees this via the + protected-gate + trail-verifier pre-gate, and a tampered protected trail has + already raised AuditIntegrityError before this function is called. The + parameter is named for that contract: a future caller passing raw + ``_engine(runtime).records`` is then a self-documenting mistake. This function + takes a MATERIALIZED list (not a callable) — a bare list cannot carry the + verified/unverified distinction, so the gate decision lives in the handler. + + BLOCKED — positive admission classifier (Task 8): which records count as an + attestation, and the forge-proof discriminator, await owner ratification. + Until then this surfaces ZERO attestations (fail-closed: omit everything). + """ + records = list(verified_runtime_records) + attestations: list[dict[str, Any]] = [] + # Task 8: classify `records` for `sei` here once the discriminator is ratified. + return {"status": "checked", "sei": sei, "attestations": attestations} +``` + Export it: add `read_sei_attestations` to the governance import block + `__all__` in `src/legis/service/__init__.py`. + +- [ ] **Implement (b) the handler** in `src/legis/mcp.py`, with the FAIL-CLOSED PRE-GATE moved into the handler (validator high finding: the gate-presence decision must precede any record read, since `verified_records` falls through to unverified `engine_records()` when `trail_owner` is `None`): +```python +def _tool_attestation_get(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, Any]: + from legis.service.governance import read_sei_attestations + + sei = _require(args, "sei") + # FAIL-CLOSED: attestation is only possible when the trail is signature- + # verifiable. `verified_records` ONLY runs TrailVerifier.verify when BOTH a + # protected trail_owner AND a trail_verifier are wired (governance.py:199-205); + # with a protected_gate but no verifier it returns engine records UNVERIFIED. + # Gate on BOTH so the invariant holds by construction, not by the convention + # that build_runtime co-locates them under one `if hmac_key:` block. Return the + # discriminated unavailable (NEVER a silent empty 'checked' that reads as + # "never attested", NEVER unverified field values). + if runtime.protected_gate is None or runtime.trail_verifier is None: + return _tool_result( + { + "status": "unavailable", + "sei": sei, + "attestations": [], + "unavailable": [ + {"reason": "trail not signature-verifiable (no protected gate / verifier)"} + ], + } + ) + # _governance_trail_records runs verified_records, which raises + # AuditIntegrityError (-> AUDIT_INTEGRITY_FAILURE) on a tampered protected trail. + return _tool_result(read_sei_attestations(_governance_trail_records(runtime), sei)) +``` + +- [ ] **Implement (c) the tool definition** via `_one_of` (discriminated, so it routes through `_one_of` per the conformance tests): +```python + { + "name": "attestation_get", + "description": ( + "Per-SEI human-cleared governance attestation FACTS (no proven_good " + "verdict). Through the same fail-closed verified-trail path the " + "honesty reads use: a tampered trail -> AUDIT_INTEGRITY_FAILURE; an " + "engine-only deployment (no protected gate) -> 'unavailable'. Never " + "read an empty attestations list under 'unavailable' as 'never " + "attested'; a forged attestation is never returned." + ), + "inputSchema": _schema(["sei"], {"sei": string}), + "outputSchema": _one_of( + [ + _schema( + ["status", "sei", "attestations"], + { + "status": {"type": "string", "enum": ["checked"]}, + "sei": string, + "attestations": { + "type": "array", + "items": { + "type": "object", + "required": ["kind", "content_hash", "recorded_at", "seq"], + "properties": { + "kind": { + "type": "string", + "enum": ["signoff_cleared", "operator_override"], + }, + "content_hash": string, + "recorded_at": string, + "seq": integer, + "signoff_seq": integer, + }, + }, + }, + }, + ), + _schema( + ["status", "sei", "attestations", "unavailable"], + { + "status": {"type": "string", "enum": ["unavailable"]}, + "sei": string, + "attestations": {"type": "array", "maxItems": 0}, + "unavailable": { + "type": "array", + "items": _schema(["reason"], {"reason": string}), + }, + }, + ), + ] + ), + }, +``` + +- [ ] **Implement (d) the three registries:** add `"attestation_get"` to `_AGENT_TOOLS`, `"attestation_get": _tool_attestation_get` to `_TOOL_HANDLERS`, and the definition above to `tool_definitions()`. + +- [ ] **Run to pass:** `uv run pytest tests/mcp/test_server.py tests/service/test_governance.py -q -k attestation`. + +- [ ] **Commit:** `feat(mcp): add attestation_get fail-closed scaffolding (classifier BLOCKED)`. + +--- + +## Task 6 — Surface bookkeeping: tool-count bump 22→24 + surface-set literal + structural boundary + +**Files** +- Modify test: `tests/mcp/test_server.py` + +**Interfaces** +- Consumes: `_AGENT_TOOLS`, `_TOOL_HANDLERS`, `tool_definitions()`. + +Steps: + +- [ ] **Update the surface-set literal** `test_initialize_and_tools_list_exposes_full_agent_surface` (tests/mcp/test_server.py:266-326): add `"warpline_preflight_get"` and `"attestation_get"` to the `set(by_name) == {...}` literal at lines 282-305 (now 24 names). The binding invariant `test_tool_registries_are_in_sync` (test_server.py:2053-2064: `defined == set(_TOOL_HANDLERS) == set(_AGENT_TOOLS)`) needs NO edit — it is structural and already passes once all three registries carry both new tools (Tasks 3 + 5). + +- [ ] **Confirm no new error code is introduced.** Both tools degrade ambiguous/failure cases to a success-envelope `status: "unavailable"`; the only error path is `AUDIT_INTEGRITY_FAILURE`, already in the pinned list (test_server.py:2067-2093) and `_recovery_for`. So `_recovery_for` (mcp.py:1222-1284) and the pinned-code test require NO change. Add an explicit assertion to lock that: +```python +def test_warpline_tools_introduce_no_new_error_codes(tmp_path): + # warpline_preflight_get / attestation_get degrade to success-envelope + # status:"unavailable"; their only error path is the pre-existing + # AUDIT_INTEGRITY_FAILURE. No new error code => no _recovery_for / pinned-code change. + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) + assert not call_tool(runtime, "warpline_preflight_get", {"base": "x"}).get("isError") + assert not call_tool(runtime, "attestation_get", {"sei": "x#1"}).get("isError") +``` + +- [ ] **Confirm C-8 names pass** (test_c8 at test_server.py:2096-2107): neither `warpline_preflight_get` nor `attestation_get` contains `enable/provision/grant/hmac/sign_key/set_key`. No code change; this test already covers the new names once registered. + +- [ ] **Run to pass:** `uv run pytest tests/mcp/test_server.py -q`. + +- [ ] **Commit:** `test(mcp): bump agent surface to 24 tools for warpline + attestation`. + +--- + +## Task 7 — outputSchema conformance vectors for both new tools + +**Files** +- Modify test: `tests/mcp/test_output_schema_conformance.py` + +**Interfaces** +- Consumes: `_conformant(runtime, name, args)` (lines 97-105), `_tool(name)` (75-78), `_runtime` (81-94), `ERROR_ENVELOPE_SCHEMA`, `call_tool` (all imported in-file). + +Steps: + +- [ ] **Write conformance tests.** The catalog-wide tests (`test_every_tool_declares_a_valid_output_schema`, `test_every_output_schema_declares_top_level_object_type`, `test_one_of_helper_always_injects_top_level_object_type`) auto-cover both new tools because each schema routes through `_one_of` / `_schema`. Add per-tool driven vectors mirroring the `identity_gap_list` unavailable-conformance model (conformance:492-498) and the scan_route error-path model (conformance:418-443): +```python +def test_warpline_preflight_get_unavailable_conforms(tmp_path): + runtime, _store = _runtime(tmp_path) # warpline None + payload = _conformant(runtime, "warpline_preflight_get", {"base": "aaa"}) + assert payload["status"] == "unavailable" + + +def test_warpline_preflight_get_checked_conforms(tmp_path): + class _FakeWarpline: + def impact_radius(self, base, head): + return {"affected": [], "count": 0} + + def reverify_worklist(self, base, head): + return {"entries": [], "count": 0} + + runtime, _store = _runtime(tmp_path) + runtime.warpline = _FakeWarpline() + payload = _conformant(runtime, "warpline_preflight_get", {"base": "aaa", "head": "bbb"}) + assert payload["status"] == "checked" + + +def test_attestation_get_unavailable_conforms(tmp_path): + runtime, _store = _runtime(tmp_path) # no protected gate + payload = _conformant(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert payload["status"] == "unavailable" + + +def test_attestation_get_tamper_error_conforms_to_envelope(tmp_path): + from legis.mcp import ERROR_ENVELOPE_SCHEMA, call_tool + from jsonschema import Draft202012Validator + + runtime, _store = _runtime(tmp_path) + + class _TamperVerifier: + def verify(self, records): + from legis.enforcement.protected import TamperError + + raise TamperError("mismatch") + + class _FakeProtectedGate: + def records(self): + return ["bad"] + + runtime.protected_gate = _FakeProtectedGate() + runtime.trail_verifier = _TamperVerifier() + result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert result.get("isError") + Draft202012Validator(ERROR_ENVELOPE_SCHEMA).validate(result["structuredContent"]) +``` + +- [ ] **Run to pass:** `uv run pytest tests/mcp/test_output_schema_conformance.py -q`. + +- [ ] **Commit:** `test(mcp): outputSchema conformance vectors for warpline + attestation tools`. + +--- + +## Task 8 — `read_sei_attestations` positive-admission classifier — **BLOCKED pending owner confirmation** + +> **STATUS: BLOCKED.** This task does NOT proceed until the owner ratifies the three open questions below. They are SPEC-LEVEL security decisions (they contradict spec lines 92 / 102 / 112), not implementation details deferred to line 116. Do NOT invent a discriminator marker; do NOT surface field values over an unverified trail. The scaffolding (Task 5) ships now and surfaces ZERO attestations (fail-closed) until ratified. The test suite below is written and committed as `@pytest.mark.skip(reason="BLOCKED: owner classifier ratification")`; the skip is removed when the owner answers. + +**The three open questions the owner MUST answer:** + +1. **Operator-override discriminator (forgeable as a bare field check).** The obvious reading `extensions.judge_verdict == "OVERRIDDEN_BY_OPERATOR"` + `extensions.protected_cell == True` is FORGEABLE: the chill `EnforcementEngine` (engine.py:70-71, `judge is None`) writes caller `extensions` VERBATIM with no server-side override, so a self-clear can carry those fields. A false "attested" → warpline skips reverify on un-cleared code → the exact ASYMMETRIC-RULE security hole. **Recommended resolution:** the classifier admits an operator override ONLY when `extensions.judge_metadata_signature` VERIFIES under the protected key over `signing_fields(payload, seq=rec.seq)` (protected.py:45-92, 186-197) AND the signed `judge_verdict == "OVERRIDDEN_BY_OPERATOR"` — never the bare field value. Because the handler pre-gate (Task 5) already requires `runtime.protected_gate` and reads through `verified_records` (which runs `TrailVerifier.verify` and raises on any forged/unsigned protected record), the additional in-classifier requirement is the PRESENCE of `judge_metadata_signature` in `extensions` (a chill record stuffing ONLY `judge_verdict` carries no such signature, passes the verifier because `_requires_verification` does not select it, and MUST therefore be omitted by the classifier's signature-presence check). + +2. **Fail-closed routing in the engine-only deployment.** `verified_records` falls through to UNVERIFIED `engine_records()` when `trail_owner` is `None` (governance.py:205), and only runs `TrailVerifier.verify` when `trail_verifier` is also wired (governance.py:199-205); a pre-materialized records list cannot carry the verified/unverified distinction. **Recommended resolution (already implemented in Task 5):** the handler gates on `runtime.protected_gate is None or runtime.trail_verifier is None → unavailable` BEFORE any records are read; `read_sei_attestations` only ever sees `verified_records` output. Owner must confirm engine-only deployments cannot attest at all (return `unavailable`). + +3. **Unsigned structured (procedural) sign-offs.** `signoff.py:104-105` writes a `SIGNED_OFF` record with NO `signoff_signature` when no signer/key is wired (module docstring 4-6: "structured sign-offs are procedural (unsigned)"). **Recommended resolution:** only a verifying-signed sign-off (`signoff_signature` present and verifying, selected + verified by `TrailVerifier`) is admissible; an unsigned structured `SIGNED_OFF` resolves to omission. In an engine-only deployment this is already covered by the handler pre-gate (no protected gate → `unavailable`). + +**Files (on ratification)** +- Modify: `src/legis/service/governance.py` (`read_sei_attestations` body) +- Modify test: `tests/service/test_governance.py`, `tests/mcp/test_server.py` + +**Interfaces (the ratified shape — recommended)** +- For each surfaced attestation: `{"kind": "signoff_cleared"|"operator_override", "content_hash": , "recorded_at": , "seq": , "signoff_seq"?: }`. NEVER `content_hash == ""`. SEI is `entity_key.value`; `identity_stable` must be true. Sign-off discriminator is `SignoffState.SIGNED_OFF.value` (`"SIGNED_OFF"`, UPPERCASE — never the spec's lowercase `'signed_off'`, which matches nothing). content_hash for a sign-off is JOINED from the PENDING request via `extensions.request_seq` at `extensions.loomweave.content_hash`; for an operator override it is INLINE at `extensions.loomweave.content_hash`. + +**MANDATORY test suite (all `required_test_cases` from the three validators; write now, `@pytest.mark.skip` until ratified):** + +- [ ] **Write the (skipped) classifier test suite** in `tests/service/test_governance.py` and `tests/mcp/test_server.py`. Each marked `@pytest.mark.skip(reason="BLOCKED: owner classifier ratification (Task 8)")`. Cases: + + - [ ] **FORGE-NEGATIVE (protected, stuffed full markers).** A chill self-clear record stuffed with `extensions={"judge_verdict": "OVERRIDDEN_BY_OPERATOR", "protected_cell": True}` but NO verifying `judge_metadata_signature` → `_requires_verification` selects it (`protected_cell is True`) → `TrailVerifier.verify` raises `TamperError` → `attestation_get` returns `isError`, `error_code == "AUDIT_INTEGRITY_FAILURE"` (nothing surfaced). + - [ ] **FORGE-NEGATIVE (subtle — proves the classifier's OWN signature check).** A chill self-clear stuffing ONLY `judge_verdict == "OVERRIDDEN_BY_OPERATOR"` (NO `protected_cell`/`file_fingerprint`/`ast_path`/signature) → NOT selected by `_requires_verification`, so it PASSES the trail verifier → `attestation_get` MUST STILL OMIT it because it lacks a verifying `judge_metadata_signature`. Asserts the classifier requires a verifying signature, not mere passage through `verified_records`. + - [ ] **ENGINE-ONLY DEPLOYMENT.** A real UNSIGNED structured `SIGNED_OFF` record exists for the SEI, no protected gate / no `LEGIS_HMAC_KEY` → `attestation_get` returns `status == "unavailable"`, ZERO attestations, never a bare empty `checked`. (Already passing via Task 5's pre-gate.) + - [ ] **POSITIVE (protected operator-override).** A record with a `judge_metadata_signature` that VERIFIES under the protected key over `signing_fields(payload, seq=rec.seq)` AND signed `judge_verdict == "OVERRIDDEN_BY_OPERATOR"` AND inline `extensions.loomweave.content_hash` present → surfaces `kind: "operator_override"` with that `content_hash` and `seq == rec.seq`. + - [ ] **POSITIVE (protected-cell cleared sign-off).** A `SIGNED_OFF` record with a verifying `signoff_signature`, `identity_stable` true, joined to a PENDING (via `extensions.request_seq`) with non-empty `extensions.loomweave.content_hash` → surfaces `kind: "signoff_cleared"` with the joined `content_hash` and `signoff_seq == request_seq`. + - [ ] **EXCLUSION (BLOCKED verdict).** A `BLOCKED` verdict record for the SEI → omitted. + - [ ] **EXCLUSION (ACCEPTED self-clear).** A coached/chill `ACCEPTED` self-clear (`judge_verdict == "ACCEPTED"`, no operator override) → omitted. + - [ ] **FILTER (`identity_stable`).** A cleared sign-off whose `entity_key.identity_stable` is False (locator-keyed, no rename-stable SEI) → omitted. + - [ ] **FILTER (SEI scoping).** Records for a DIFFERENT SEI are not surfaced under the queried SEI. + - [ ] **JOIN-EMPTY (asymmetric omit).** A cleared `SIGNED_OFF` whose joined PENDING has NO `extensions.loomweave.content_hash` → the record is OMITTED (never surfaced with `content_hash == ""`), but the surrounding read still returns `status: "checked"` (the verified trail was read) with that record simply absent — a per-record omit, NOT a whole-read `unavailable`, and NEVER a silent "never attested". + - [ ] **INLINE-EMPTY (asymmetric omit).** A verifying operator-override with absent inline `extensions.loomweave.content_hash` → OMITTED, never `content_hash == ""`. + - [ ] **TAMPER at the tool level.** A tampered governance trail → `call_tool(runtime, "attestation_get", {"sei"})` returns `isError`, `error_code == "AUDIT_INTEGRITY_FAILURE"`. (Already passing via Task 5.) + - [ ] **UNAVAILABLE shape.** No governance trail wired → success envelope `{"status": "unavailable", "sei", "attestations": [], "unavailable": [{"reason"}]}`; assert NOT `isError` and NOT a silent empty `checked`. (Already passing via Task 5.) + - [ ] **outputSchema conformance.** The discriminated checked/unavailable union routes through `_one_of` (top-level `"type": "object"`); the unavailable path conforms; an `isError` tamper path validates against `ERROR_ENVELOPE_SCHEMA`. (Already passing via Task 7.) + - [ ] **BYTE-IDENTITY (tool-local sanity).** `attestation_get` reads only the local verified trail and never `runtime.warpline`; a structural test asserts `runtime.warpline` is absent from `read_sei_attestations` and `_tool_attestation_get` source. (Already asserted in Task 4's structural test once `_tool_attestation_get` is added to that function set.) + +- [ ] **On ratification:** implement the agreed classifier in `read_sei_attestations` using the verifying-signature discriminator (the recommended resolution above), reusing `_binding_entity_from_backfill`'s iteration idiom (`for rec in records: payload = rec.payload`), `EntityKey.from_dict(payload["entity_key"])` for the SEI/`identity_stable` filter, the `signoff.py` `request_record`/`is_cleared` join for sign-offs, and `verify(signing_fields(payload, seq=rec.seq), sig, key)` for the operator-override signature check. Remove the `@pytest.mark.skip` marks, run the full suite to pass, commit `feat(governance): ratified per-SEI attestation classifier`. + +--- + +## Final verification (run before declaring done) + +- [ ] `uv run pytest tests/warpline_preflight tests/service/test_preflight.py tests/service/test_governance.py tests/mcp/test_server.py tests/mcp/test_output_schema_conformance.py tests/mcp/test_warpline_advisory_boundary.py -q` — all green (Task 8 classifier cases skipped pending ratification). +- [ ] `uv run pytest -q` — full suite green (no regression in the 22 pre-existing tools, now 24). +- [ ] Grep guard: `grep -rn "runtime.warpline\|\.warpline" src/legis/mcp.py` shows references ONLY in `_tool_warpline_preflight_get`, the `McpRuntime` dataclass field, and the `build_runtime` gating block — NOWHERE in `_tool_policy_evaluate`, `_engine`, `_coached_engine`, the gates, sign-off, or the honesty reads. +- [ ] Confirm `_AGENT_TOOLS` has exactly 24 members; `test_tool_registries_are_in_sync` green. From 5a30cd807dac41fb748adc1d6d800845238ecbca Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 07:47:30 +1000 Subject: [PATCH 03/16] feat(warpline): add stdlib HttpWarplineClient advisory preflight client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements HttpWarplineClient (GET-only, injectable fetch, no new deps) as a faithful security-primitive clone of filigree/client.py; clone-parity guard test locks the SSRF/redirect/DoS helpers in lockstep. Routes are inferred (TO-CONFIRM per spec §6). 10 new tests, 1204 pass total. Co-Authored-By: Claude Sonnet 4.6 --- src/legis/warpline_preflight/__init__.py | 0 src/legis/warpline_preflight/client.py | 143 +++++++++++++++++++++++ tests/warpline_preflight/__init__.py | 0 tests/warpline_preflight/test_client.py | 128 ++++++++++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 src/legis/warpline_preflight/__init__.py create mode 100644 src/legis/warpline_preflight/client.py create mode 100644 tests/warpline_preflight/__init__.py create mode 100644 tests/warpline_preflight/test_client.py diff --git a/src/legis/warpline_preflight/__init__.py b/src/legis/warpline_preflight/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/legis/warpline_preflight/client.py b/src/legis/warpline_preflight/client.py new file mode 100644 index 0000000..04a0f2f --- /dev/null +++ b/src/legis/warpline_preflight/client.py @@ -0,0 +1,143 @@ +"""Warpline preflight client — legis reads ADVISORY impact/reverify hints. + +Stdlib ``urllib`` with an injectable ``fetch`` so tests run offline; no new +dependency. SECURITY: Warpline is PURELY ADVISORY. This client exposes only +read-only GETs; nothing it returns may reach a governance verdict path +(policy_evaluate, the gates, sign-off, or the honesty reads). Governance +verdicts are byte-identical whether WARPLINE_API_URL is set or unset. +""" + +from __future__ import annotations + +import json +import http.client +import ipaddress +import logging +import os +import urllib.error +import urllib.parse +import urllib.request +from typing import Any, Callable, Protocol, runtime_checkable + +Fetch = Callable[[str, str, "dict | None"], dict] + +logger = logging.getLogger(__name__) + + +class WarplineError(RuntimeError): + """A Warpline call failed at the transport or decode layer.""" + + +MAX_RESPONSE_BYTES = 1_000_000 + + +@runtime_checkable +class WarplineClient(Protocol): + def impact_radius(self, base: str, head: str) -> dict[str, Any]: ... + def reverify_worklist(self, base: str, head: str) -> dict[str, Any]: ... + + +def _urllib_fetch( + method: str, url: str, body: dict | None, headers: dict[str, str] | None = None +) -> dict: + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, method=method) + if data is not None: + req.add_header("Content-Type", "application/json") + for name, value in (headers or {}).items(): + req.add_header(name, value) + try: + with _open_no_redirect(req) as resp: # noqa: S310 (trusted Warpline URL) + decoded = _decode_json_response(resp, f"{method} {url}") + except urllib.error.HTTPError as exc: + if 300 <= exc.code < 400: + raise WarplineError(f"{method} {url} redirect not allowed: {exc.code}") from exc + raise WarplineError(f"{method} {url} failed: {exc}") from exc + except (urllib.error.URLError, ValueError, OSError, http.client.HTTPException) as exc: + raise WarplineError(f"{method} {url} failed: {exc}") from exc + return _require_dict(decoded, f"{method} {url}") + + +class _NoRedirectHandler(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[override] + return None + + +def _open_no_redirect(req: urllib.request.Request) -> Any: + opener = urllib.request.build_opener(_NoRedirectHandler()) + return opener.open(req, timeout=10.0) + + +def _decode_json_response(resp: Any, context: str) -> Any: + headers = getattr(resp, "headers", {}) or {} + content_type = headers.get("Content-Type", "application/json") + if "json" not in content_type.lower(): + raise WarplineError(f"{context} returned non-JSON content type: {content_type}") + raw = resp.read(MAX_RESPONSE_BYTES + 1) + if len(raw) > MAX_RESPONSE_BYTES: + raise WarplineError(f"{context} response too large") + return json.loads(raw.decode("utf-8")) + + +def _require_dict(value: Any, context: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise WarplineError(f"{context} returned {type(value).__name__}, expected object") + return value + + +def _is_loopback(host: str) -> bool: + if host == "localhost": + return True + try: + return ipaddress.ip_address(host).is_loopback + except ValueError: + return False + + +def _validate_base_url(base_url: str) -> str: + parsed = urllib.parse.urlparse(base_url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + raise WarplineError("Warpline base URL must be an http(s) URL with a host") + allow_insecure_remote = os.environ.get("LEGIS_ALLOW_INSECURE_REMOTE_HTTP") == "1" + if parsed.scheme == "http" and not _is_loopback(parsed.hostname): + if not allow_insecure_remote: + raise WarplineError("Warpline base URL must use HTTPS unless it is loopback") + # ID-SEI-1: plaintext to a remote Warpline. TLS is the only integrity + # control on responses (the request HMAC authenticates requests, not + # responses), so an on-path attacker can tamper with what legis reads + # back. Dev/loopback only; never production. + logger.warning( + "LEGIS_ALLOW_INSECURE_REMOTE_HTTP=1 is permitting a plaintext HTTP " + "connection to non-loopback Warpline host %r; responses are forgeable " + "without TLS. Dev/loopback use only.", + parsed.hostname, + ) + return base_url.rstrip("/") + + +class HttpWarplineClient: + def __init__( + self, + base_url: str, + *, + fetch: Fetch | None = None, + ) -> None: + self._base = _validate_base_url(base_url) + self._fetch = fetch if fetch is not None else self._transport_fetch + + def _transport_fetch(self, method: str, url: str, body: dict | None) -> dict: + return _urllib_fetch(method, url, body, {}) + + def impact_radius(self, base: str, head: str) -> dict[str, Any]: + q = urllib.parse.urlencode({"base": base, "head": head}) + return _require_dict( + self._fetch("GET", f"{self._base}/api/impact-radius?{q}", None), + "Warpline impact_radius", + ) + + def reverify_worklist(self, base: str, head: str) -> dict[str, Any]: + q = urllib.parse.urlencode({"base": base, "head": head}) + return _require_dict( + self._fetch("GET", f"{self._base}/api/reverify-worklist?{q}", None), + "Warpline reverify_worklist", + ) diff --git a/tests/warpline_preflight/__init__.py b/tests/warpline_preflight/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/warpline_preflight/test_client.py b/tests/warpline_preflight/test_client.py new file mode 100644 index 0000000..7e04a88 --- /dev/null +++ b/tests/warpline_preflight/test_client.py @@ -0,0 +1,128 @@ +import inspect +import json + +import pytest + +import legis.filigree.client as fc +import legis.warpline_preflight.client as wc +from legis.warpline_preflight.client import ( + HttpWarplineClient, + WarplineClient, + WarplineError, + MAX_RESPONSE_BYTES, +) + + +def _recorder(responses): + """An injectable Fetch that returns queued dicts and records calls.""" + calls = [] + + def fetch(method, url, body): + calls.append((method, url, body)) + return responses.pop(0) + + fetch.calls = calls + return fetch + + +def test_protocol_is_runtime_checkable(): + client = HttpWarplineClient("http://localhost:9100", fetch=_recorder([{}])) + assert isinstance(client, WarplineClient) + + +def test_impact_radius_is_a_get_with_base_head_query(): + fetch = _recorder([{"affected": [], "count": 0}]) + client = HttpWarplineClient("http://localhost:9100", fetch=fetch) + out = client.impact_radius("aaa", "bbb") + assert out == {"affected": [], "count": 0} + method, url, body = fetch.calls[0] + assert method == "GET" and body is None + assert url == "http://localhost:9100/api/impact-radius?base=aaa&head=bbb" + + +def test_reverify_worklist_is_a_get_with_base_head_query(): + fetch = _recorder([{"entries": [], "count": 0}]) + client = HttpWarplineClient("http://localhost:9100", fetch=fetch) + out = client.reverify_worklist("aaa", "bbb") + assert out == {"entries": [], "count": 0} + method, url, body = fetch.calls[0] + assert method == "GET" and body is None + assert url == "http://localhost:9100/api/reverify-worklist?base=aaa&head=bbb" + + +def test_non_object_response_is_a_warpline_error(): + client = HttpWarplineClient("http://localhost:9100", fetch=_recorder([["not", "a", "dict"]])) + with pytest.raises(WarplineError): + client.impact_radius("a", "b") + + +def test_loopback_http_ok_remote_http_rejected_unless_optin(monkeypatch): + monkeypatch.delenv("LEGIS_ALLOW_INSECURE_REMOTE_HTTP", raising=False) + HttpWarplineClient("http://127.0.0.1:9100") # loopback IP ok + HttpWarplineClient("http://localhost:9100") # localhost ok + HttpWarplineClient("https://warpline.example.com") # https ok + with pytest.raises(WarplineError, match="HTTPS unless it is loopback"): + HttpWarplineClient("http://warpline.example.com") + monkeypatch.setenv("LEGIS_ALLOW_INSECURE_REMOTE_HTTP", "1") + HttpWarplineClient("http://warpline.example.com") # opt-in permits it + + +def test_base_url_must_be_http_with_host(): + with pytest.raises(WarplineError, match="http\\(s\\) URL with a host"): + HttpWarplineClient("ftp://warpline") + with pytest.raises(WarplineError, match="http\\(s\\) URL with a host"): + HttpWarplineClient("not-a-url") + + +def test_response_too_large_via_real_decode_path(): + # Exercise the real _decode_json_response size guard with a fake resp object. + from legis.warpline_preflight.client import _decode_json_response + + big = json.dumps({"x": "y" * (MAX_RESPONSE_BYTES + 10)}).encode("utf-8") + + class _Resp: + headers = {"Content-Type": "application/json"} + + def read(self, n): + return big[:n] + + with pytest.raises(WarplineError, match="response too large"): + _decode_json_response(_Resp(), "GET test") + + +def test_non_json_content_type_rejected(): + from legis.warpline_preflight.client import _decode_json_response + + class _Resp: + headers = {"Content-Type": "text/html"} + + def read(self, n): + return b"" + + with pytest.raises(WarplineError, match="non-JSON content type"): + _decode_json_response(_Resp(), "GET test") + + +def test_no_redirect_handler_returns_none(): + from legis.warpline_preflight.client import _NoRedirectHandler + + h = _NoRedirectHandler() + assert h.redirect_request(None, None, 302, "Found", {}, "http://elsewhere") is None + + +def _normalize(src): + # The ONLY intended differences are the sibling name and its error class. + return src.replace("Filigree", "Warpline").replace("filigree", "warpline") + + +def test_security_primitives_are_faithful_clones_of_filigree(): + # If a future patch hardens filigree's SSRF/redirect/DoS handling this fails + # loudly so warpline is patched in lockstep. _urllib_fetch is EXCLUDED — it + # intentionally drops filigree's weft_signing body bytes (warpline is GET-only). + for name in ("_validate_base_url", "_is_loopback", "_open_no_redirect", "_decode_json_response"): + assert _normalize(inspect.getsource(getattr(fc, name))) == inspect.getsource( + getattr(wc, name) + ), f"{name} diverged from the filigree clone" + assert _normalize(inspect.getsource(fc._NoRedirectHandler)) == inspect.getsource( + wc._NoRedirectHandler + ) From bcea15d06c8b071ae2d5a1d64267460ce7eb050c Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 07:54:33 +1000 Subject: [PATCH 04/16] feat(warpline): add read_warpline_preflight discriminated service read Transport-agnostic preflight read returning checked/unavailable; WarplineError is always caught and converted, never escapes; asymmetric honesty ensures None client or any method failure degrades the whole read to unavailable. Co-Authored-By: Claude Sonnet 4.6 --- src/legis/service/__init__.py | 2 ++ src/legis/service/preflight.py | 38 ++++++++++++++++++++ tests/service/test_preflight.py | 63 +++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/legis/service/preflight.py create mode 100644 tests/service/test_preflight.py diff --git a/src/legis/service/__init__.py b/src/legis/service/__init__.py index ebcc909..62b11aa 100644 --- a/src/legis/service/__init__.py +++ b/src/legis/service/__init__.py @@ -30,6 +30,7 @@ submit_protected_override, verified_records, ) +from legis.service.preflight import read_warpline_preflight from legis.service.wardline import route_wardline_scan __all__ = [ @@ -54,6 +55,7 @@ "submit_override", "submit_operator_override", "submit_protected_override", + "read_warpline_preflight", "route_wardline_scan", "verified_records", ] diff --git a/src/legis/service/preflight.py b/src/legis/service/preflight.py new file mode 100644 index 0000000..f92f511 --- /dev/null +++ b/src/legis/service/preflight.py @@ -0,0 +1,38 @@ +"""The warpline advisory preflight read — discriminated checked/unavailable. + +SECURITY: warpline is PURELY ADVISORY. This read is a SIBLING of the governance +honesty reads, never embedded in one; a failure here is contained as +``unavailable`` and never escapes as INTERNAL_ERROR, exactly as +``read_identity_gaps`` converts a ``LoomweaveError``. An unreachable/unconfigured +warpline → ``unavailable`` with a reason, never an empty affected-set that reads +as "nothing impacted". +""" + +from __future__ import annotations + +from typing import Any + + +def read_warpline_preflight( + warpline_client: Any | None, base: str, head: str +) -> dict[str, Any]: + from legis.warpline_preflight.client import WarplineError + + if warpline_client is None: + return { + "status": "unavailable", + "unavailable": [{"reason": "warpline client not configured"}], + } + try: + impact = warpline_client.impact_radius(base, head) + worklist = warpline_client.reverify_worklist(base, head) + except WarplineError as exc: + return { + "status": "unavailable", + "unavailable": [{"reason": f"warpline check failed: {exc}"}], + } + return { + "status": "checked", + "impact_radius": impact, + "reverify_worklist": worklist, + } diff --git a/tests/service/test_preflight.py b/tests/service/test_preflight.py new file mode 100644 index 0000000..a7538c4 --- /dev/null +++ b/tests/service/test_preflight.py @@ -0,0 +1,63 @@ +from legis.service.preflight import read_warpline_preflight +from legis.warpline_preflight.client import WarplineError + + +class _OkWarpline: + def impact_radius(self, base, head): + return {"affected": [{"sei": "S1"}], "count": 1} + + def reverify_worklist(self, base, head): + return {"entries": [{"sei": "S1", "reason": "edited"}], "count": 1} + + +class _ImpactRaisesWarpline: + def impact_radius(self, base, head): + raise WarplineError("boom") + + def reverify_worklist(self, base, head): + return {"entries": [], "count": 0} + + +class _WorklistRaisesWarpline: + def impact_radius(self, base, head): + return {"affected": [], "count": 0} + + def reverify_worklist(self, base, head): + raise WarplineError("timeout") + + +def test_checked_when_both_methods_succeed(): + out = read_warpline_preflight(_OkWarpline(), "aaa", "bbb") + assert out == { + "status": "checked", + "impact_radius": {"affected": [{"sei": "S1"}], "count": 1}, + "reverify_worklist": {"entries": [{"sei": "S1", "reason": "edited"}], "count": 1}, + } + + +def test_unavailable_when_client_is_none_not_a_silent_empty(): + out = read_warpline_preflight(None, "aaa", "bbb") + assert out["status"] == "unavailable" + assert out["unavailable"] == [{"reason": "warpline client not configured"}] + # ASYMMETRIC: never an empty affected-set that reads as "nothing impacted". + assert "impact_radius" not in out + + +def test_unavailable_when_impact_radius_raises_warpline_error(): + out = read_warpline_preflight(_ImpactRaisesWarpline(), "aaa", "bbb") + assert out["status"] == "unavailable" + assert out["unavailable"][0]["reason"].startswith("warpline check failed:") + + +def test_unavailable_when_worklist_raises_warpline_error(): + # Partial advisory context is NOT surfaced as checked — either method failing + # degrades the WHOLE read to unavailable. + out = read_warpline_preflight(_WorklistRaisesWarpline(), "aaa", "bbb") + assert out["status"] == "unavailable" + assert out["unavailable"][0]["reason"].startswith("warpline check failed:") + + +def test_warpline_error_never_escapes_as_internal_error(): + # The transport error is caught and converted, never re-raised. + out = read_warpline_preflight(_ImpactRaisesWarpline(), "aaa", "bbb") + assert out["status"] == "unavailable" # no exception propagated From eeb79d9d922495f46be92393aedff38c033b8dfd Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:00:00 +1000 Subject: [PATCH 05/16] feat(mcp): wire warpline_preflight_get advisory sibling tool Add McpRuntime.warpline field, build_runtime env-gating (WARPLINE_API_URL, try/except degrade to None), handler _tool_warpline_preflight_get, tool definition with _one_of discriminated schema, and three-registry sync (warpline_preflight_get added to _AGENT_TOOLS, _TOOL_HANDLERS, tool_definitions). TDD: dispatch/env/degrade tests written first (red), then implementation (green). Co-Authored-By: Claude Sonnet 4.6 --- src/legis/mcp.py | 64 +++++++++++++++++++++++++++++++++++ tests/mcp/test_server.py | 72 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/legis/mcp.py b/src/legis/mcp.py index 292007b..db10b4f 100644 --- a/src/legis/mcp.py +++ b/src/legis/mcp.py @@ -102,6 +102,7 @@ "doctor_get", "policy_boundary_check", "posture_get", + "warpline_preflight_get", } ) _OVERRIDE_RATE_NOTE = "measures operator force-pasts; not movable by agent retries" @@ -176,6 +177,7 @@ class McpRuntime: # which _floored_registry treats fail-closed as a missing ledger (structured). posture_ledger: Any | None = None coached_engine: EnforcementEngine | None = None + warpline: Any | None = None # advisory sibling; NEVER read by a verdict path def _load_policy_cell_registry() -> PolicyCellRegistry: @@ -225,6 +227,20 @@ def build_runtime(agent_id: str) -> McpRuntime: filigree = HttpFiligreeClient(filigree_url) + warpline = None + warpline_url = os.environ.get("WARPLINE_API_URL") + if warpline_url: + from legis.warpline_preflight.client import HttpWarplineClient, WarplineError + + try: + warpline = HttpWarplineClient(warpline_url) + except WarplineError: + logging.getLogger(__name__).warning( + "WARPLINE_API_URL is set but invalid; warpline advisory context " + "disabled (governance unaffected)." + ) + warpline = None + protected_gate = None trail_verifier = None signoff_gate = None @@ -288,6 +304,7 @@ def build_runtime(agent_id: str) -> McpRuntime: # install-time action (Phase 6) and build_runtime must not create local # state (audit H6 / the no-local-state-on-init invariant). posture_ledger=PostureLedger(posture_db_url(), initialize=False), + warpline=warpline, ) @@ -861,6 +878,40 @@ def tool_definitions() -> list[dict[str, Any]]: }, ), }, + { + "name": "warpline_preflight_get", + "description": ( + "ADVISORY preflight context from the warpline sibling: impact " + "radius + reverify worklist over base..head. Purely advisory — " + "NEVER a governance verdict. Discriminated: 'checked' carries the " + "advisory facts; 'unavailable' (client unconfigured, transport " + "failure, or payload shape mismatch) carries reasons. Never read a " + "missing 'checked' as 'nothing impacted'." + ), + "inputSchema": _schema(["base"], {"base": string, "head": string}), + "outputSchema": _one_of( + [ + _schema( + ["status", "impact_radius", "reverify_worklist"], + { + "status": {"type": "string", "enum": ["checked"]}, + "impact_radius": {"type": "object"}, + "reverify_worklist": {"type": "object"}, + }, + ), + _schema( + ["status", "unavailable"], + { + "status": {"type": "string", "enum": ["unavailable"]}, + "unavailable": { + "type": "array", + "items": _schema(["reason"], {"reason": string}), + }, + }, + ), + ] + ), + }, { "name": "identity_gap_list", "description": ( @@ -2170,6 +2221,18 @@ def _tool_filigree_closure_gate_get(runtime: McpRuntime, args: dict[str, Any]) - ) +def _tool_warpline_preflight_get(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, Any]: + from legis.service.preflight import read_warpline_preflight + + return _tool_result( + read_warpline_preflight( + runtime.warpline, + base=_require(args, "base"), + head=args.get("head", "HEAD"), + ) + ) + + def _governance_trail_records(runtime: McpRuntime) -> list[Any]: """The verified governance trail the SEI lineage-honesty reads consume. @@ -2441,6 +2504,7 @@ def _tool_posture_get(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, An "doctor_get": _tool_doctor_get, "policy_boundary_check": _tool_policy_boundary_check, "posture_get": _tool_posture_get, + "warpline_preflight_get": _tool_warpline_preflight_get, } diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index 14e130a..a68d864 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -3363,3 +3363,75 @@ def test_check_list_target_type_schema_declares_enum_matching_handler(tmp_path): assert rejected["isError"] is True for value in _CHECK_TARGET_TYPES: assert value in rejected["structuredContent"]["message"] + + +# --------------------------------------------------------------------------- +# Task 3: warpline_preflight_get advisory sibling tool +# --------------------------------------------------------------------------- + + +def test_build_runtime_wires_warpline_from_env(monkeypatch, tmp_path): + from legis.mcp import build_runtime + from legis.warpline_preflight.client import HttpWarplineClient + + monkeypatch.setenv("WARPLINE_API_URL", "http://localhost:9100") + monkeypatch.setenv("LEGIS_SOURCE_ROOT", str(tmp_path)) + monkeypatch.delenv("LEGIS_HMAC_KEY", raising=False) # engine-only: no protected gate + # NOTE: build_runtime(agent_id) takes ONLY agent_id (mcp.py:200); source root + # and DBs are env-driven (LEGIS_SOURCE_ROOT, mcp.py:275). There is NO + # source_root= kwarg — passing one raises TypeError before any assertion. + runtime = build_runtime("agent-x") + assert isinstance(runtime.warpline, HttpWarplineClient) + + +def test_build_runtime_leaves_warpline_unwired_without_env(monkeypatch, tmp_path): + from legis.mcp import build_runtime + + monkeypatch.delenv("WARPLINE_API_URL", raising=False) + monkeypatch.setenv("LEGIS_SOURCE_ROOT", str(tmp_path)) + runtime = build_runtime("agent-x") + assert runtime.warpline is None + + +def test_build_runtime_degrades_warpline_to_none_on_bad_url(monkeypatch, tmp_path): + # A misconfigured ADVISORY url must NOT crash the sole governance authority + # at startup; it degrades to no advisory context (governance unaffected). + from legis.mcp import build_runtime + + monkeypatch.setenv("WARPLINE_API_URL", "not-a-valid-url") + monkeypatch.setenv("LEGIS_SOURCE_ROOT", str(tmp_path)) + monkeypatch.delenv("LEGIS_HMAC_KEY", raising=False) + runtime = build_runtime("agent-x") + assert runtime.warpline is None + + +def test_warpline_preflight_get_unavailable_when_unwired(tmp_path): + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) # warpline defaults to None + result = call_tool(runtime, "warpline_preflight_get", {"base": "aaa"}) + assert not result.get("isError") + assert result["structuredContent"] == { + "status": "unavailable", + "unavailable": [{"reason": "warpline client not configured"}], + } + + +def test_warpline_preflight_get_checked_with_injected_client(tmp_path): + from legis.mcp import call_tool + + class _FakeWarpline: + def impact_radius(self, base, head): + return {"affected": [{"sei": "S1"}], "count": 1} + + def reverify_worklist(self, base, head): + return {"entries": [], "count": 0} + + runtime, _store = _runtime(tmp_path) + runtime.warpline = _FakeWarpline() + result = call_tool(runtime, "warpline_preflight_get", {"base": "aaa", "head": "bbb"}) + assert not result.get("isError") + sc = result["structuredContent"] + assert sc["status"] == "checked" + assert sc["impact_radius"] == {"affected": [{"sei": "S1"}], "count": 1} + assert sc["reverify_worklist"] == {"entries": [], "count": 0} From 7d716812182271ea80185d1921bfe0bb344c0388 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:05:41 +1000 Subject: [PATCH 06/16] test(warpline): byte-identical advisory-boundary acceptance spine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tests prove the governance invariant: 1. Verdicts from policy_evaluate (VIOLATION + UNKNOWN) are byte-identical whether runtime.warpline is None or a hostile client returning garbage advisory data — warpline presence cannot perturb a verdict. 2. Structural scan confirms runtime.warpline is referenced in no verdict-path function source (defense-in-depth complement). Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/mcp/test_warpline_advisory_boundary.py | 163 +++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 tests/mcp/test_warpline_advisory_boundary.py diff --git a/tests/mcp/test_warpline_advisory_boundary.py b/tests/mcp/test_warpline_advisory_boundary.py new file mode 100644 index 0000000..ff76690 --- /dev/null +++ b/tests/mcp/test_warpline_advisory_boundary.py @@ -0,0 +1,163 @@ +"""Byte-identical advisory-boundary acceptance spine. + +Proves the governance-verdict invariant: verdicts are byte-identical regardless +of whether ``runtime.warpline`` is None or points to a hostile advisory client +returning arbitrary garbage data. The structural companion test additionally +asserts that ``runtime.warpline`` is referenced in no verdict-path function +source, giving defense-in-depth coverage. + +If either test fails, warpline data has somehow reached a verdict path — +a security regression. +""" + +import inspect +import json + +from legis.clock import FixedClock +from legis.enforcement.engine import EnforcementEngine +from legis.policy.grammar import AllowlistBoundary, PolicyGrammar +from legis.store.audit_store import AuditStore + + +# --------------------------------------------------------------------------- +# Local helpers (copied verbatim from tests/mcp/test_server.py lines 53-91 +# so this file is self-contained and does not import from a peer test module). +# --------------------------------------------------------------------------- + + +def _chill_posture_ledger(tmp_path): + import hashlib + import uuid + + from legis.posture.ledger import PostureLedger + + ledger = PostureLedger( + f"sqlite:///{tmp_path / f'posture-{uuid.uuid4().hex}.db'}", + initialize=True, + ) + key = b"k" * 32 + ledger.genesis( + key_fingerprint=hashlib.sha256(key).hexdigest(), + agent_id="installer", + recorded_at="t0", + ) + return ledger + + +def _runtime( + tmp_path, + *, + agent_id="agent-launch", + check_surface=None, + judge=None, +): + from legis.mcp import McpRuntime + + store = AuditStore(f"sqlite:///{tmp_path / 'gov.db'}") + engine = EnforcementEngine( + store, FixedClock("2026-06-02T12:00:00+00:00"), judge=judge + ) + return McpRuntime( + agent_id=agent_id, + initialized=True, + engine=engine, + check_surface=check_surface, + posture_ledger=_chill_posture_ledger(tmp_path), + ), store + + +# --------------------------------------------------------------------------- +# Advisory-boundary fixtures +# --------------------------------------------------------------------------- + + +class _HostileWarpline: + """Returns arbitrary/garbage advisory data to prove it cannot perturb a verdict.""" + + def impact_radius(self, base, head): + return {"affected": [{"sei": "EVERYTHING"}], "count": 9999, "block": True} + + def reverify_worklist(self, base, head): + return {"entries": [{"sei": "EVERYTHING", "reason": "force"}], "count": 9999} + + +def _seed_real_verdict_runtime(tmp_path): + """A runtime that returns REAL, DETERMINISTIC verdicts. + + Uses the _runtime fixture's FixedClock (timestamps identical across runs) and + registers a real grammar so policy_evaluate returns an actual VIOLATION / + UNKNOWN verdict — NOT an error envelope. An error envelope on BOTH sides would + make the byte-identity assertion pass trivially and prove nothing — the exact + defect the first draft of this test had. Mirrors the seeding in + test_policy_evaluate_returns_unknown_distinct_from_clear (test_server.py:1225). + """ + tmp_path.mkdir(parents=True, exist_ok=True) + runtime, _store = _runtime(tmp_path) # FixedClock("2026-06-02T12:00:00+00:00") + grammar = PolicyGrammar() + grammar.register(AllowlistBoundary("imports", frozenset({"json"}))) + runtime.grammar = grammar + return runtime + + +def _run_governance_paths(runtime): + """Drive REAL verdict paths and return their structuredContent blobs.""" + from legis.mcp import call_tool + + blobs = [ + # A real VIOLATION verdict (socket not in the {json} allowlist). + call_tool( + runtime, "policy_evaluate", {"policy": "imports", "target": {"value": "socket"}} + ).get("structuredContent"), + # A real UNKNOWN verdict (unknown policy -> provenance gap). + call_tool( + runtime, "policy_evaluate", {"policy": "missing", "target": {}} + ).get("structuredContent"), + ] + # GUARD: these MUST be real verdicts, never error envelopes — otherwise the + # byte-identity assertion below is vacuous. + assert blobs[0]["outcome"] == "VIOLATION" and blobs[0]["provenance_gap"] is False + assert blobs[1]["outcome"] == "UNKNOWN" and blobs[1]["provenance_gap"] is True + return blobs + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_governance_verdicts_byte_identical_warpline_unset_vs_hostile(tmp_path): + # Everything is held IDENTICAL across the two runtimes (same FixedClock, same + # seeded grammar) EXCEPT runtime.warpline. If warpline data could reach a + # verdict path, the hostile side would diverge. + runtime_unset = _seed_real_verdict_runtime(tmp_path / "a") + runtime_unset.warpline = None + unset = _run_governance_paths(runtime_unset) + + runtime_set = _seed_real_verdict_runtime(tmp_path / "b") + runtime_set.warpline = _HostileWarpline() # structurally present, hostile + setval = _run_governance_paths(runtime_set) + + assert json.dumps(unset, sort_keys=True) == json.dumps(setval, sort_keys=True) + + +def test_runtime_warpline_referenced_in_no_verdict_path_function(): + # STRUCTURAL (defense-in-depth): runtime.warpline must appear in NO + # verdict-path / honesty-read source. NOTE inspect.getsource is a SHALLOW text + # scan — it sees only these named functions, not helpers they call — so this + # COMPLEMENTS, never replaces, the byte-identity test above. + import legis.mcp as mcp + + verdict_path_fns = [ + mcp._tool_policy_evaluate, + mcp._engine, + mcp._coached_engine, + mcp._governance_trail_records, + mcp._tool_identity_gap_list, + mcp._tool_lineage_integrity_get, + mcp._tool_policy_boundary_check, + mcp._tool_signoff_status_get, + mcp._tool_override_submit, + ] + for fn in verdict_path_fns: + src = inspect.getsource(fn) + assert ".warpline" not in src, f"{fn.__name__} references warpline" From cf99b1f097b081497981305ffa231fe8f27d0956 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:15:50 +1000 Subject: [PATCH 07/16] feat(mcp): add attestation_get fail-closed scaffolding (classifier BLOCKED) Co-Authored-By: Claude Sonnet 4.6 --- src/legis/mcp.py | 82 ++++++++++++++++++++++++++++++++ src/legis/service/__init__.py | 2 + src/legis/service/governance.py | 25 ++++++++++ tests/mcp/test_server.py | 45 ++++++++++++++++++ tests/service/test_governance.py | 13 +++++ 5 files changed, 167 insertions(+) diff --git a/src/legis/mcp.py b/src/legis/mcp.py index db10b4f..b70a271 100644 --- a/src/legis/mcp.py +++ b/src/legis/mcp.py @@ -103,6 +103,7 @@ "policy_boundary_check", "posture_get", "warpline_preflight_get", + "attestation_get", } ) _OVERRIDE_RATE_NOTE = "measures operator force-pasts; not movable by agent retries" @@ -1260,6 +1261,58 @@ def tool_definitions() -> list[dict[str, Any]]: }, ), }, + { + "name": "attestation_get", + "description": ( + "Per-SEI human-cleared governance attestation FACTS (no proven_good " + "verdict). Through the same fail-closed verified-trail path the " + "honesty reads use: a tampered trail -> AUDIT_INTEGRITY_FAILURE; an " + "engine-only deployment (no protected gate) -> 'unavailable'. Never " + "read an empty attestations list under 'unavailable' as 'never " + "attested'; a forged attestation is never returned." + ), + "inputSchema": _schema(["sei"], {"sei": string}), + "outputSchema": _one_of( + [ + _schema( + ["status", "sei", "attestations"], + { + "status": {"type": "string", "enum": ["checked"]}, + "sei": string, + "attestations": { + "type": "array", + "items": { + "type": "object", + "required": ["kind", "content_hash", "recorded_at", "seq"], + "properties": { + "kind": { + "type": "string", + "enum": ["signoff_cleared", "operator_override"], + }, + "content_hash": string, + "recorded_at": string, + "seq": integer, + "signoff_seq": integer, + }, + }, + }, + }, + ), + _schema( + ["status", "sei", "attestations", "unavailable"], + { + "status": {"type": "string", "enum": ["unavailable"]}, + "sei": string, + "attestations": {"type": "array", "maxItems": 0}, + "unavailable": { + "type": "array", + "items": _schema(["reason"], {"reason": string}), + }, + }, + ), + ] + ), + }, ] @@ -2249,6 +2302,34 @@ def _governance_trail_records(runtime: McpRuntime) -> list[Any]: ) +def _tool_attestation_get(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, Any]: + from legis.service.governance import read_sei_attestations + + sei = _require(args, "sei") + # FAIL-CLOSED: attestation is only possible when the trail is signature- + # verifiable. `verified_records` ONLY runs TrailVerifier.verify when BOTH a + # protected trail_owner AND a trail_verifier are wired (governance.py:199-205); + # with a protected_gate but no verifier it returns engine records UNVERIFIED. + # Gate on BOTH so the invariant holds by construction, not by the convention + # that build_runtime co-locates them under one `if hmac_key:` block. Return the + # discriminated unavailable (NEVER a silent empty 'checked' that reads as + # "never attested", NEVER unverified field values). + if runtime.protected_gate is None or runtime.trail_verifier is None: + return _tool_result( + { + "status": "unavailable", + "sei": sei, + "attestations": [], + "unavailable": [ + {"reason": "trail not signature-verifiable (no protected gate / verifier)"} + ], + } + ) + # _governance_trail_records runs verified_records, which raises + # AuditIntegrityError (-> AUDIT_INTEGRITY_FAILURE) on a tampered protected trail. + return _tool_result(read_sei_attestations(_governance_trail_records(runtime), sei)) + + def _tool_identity_gap_list(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, Any]: return _tool_result( read_identity_gaps(runtime.identity, lambda: _governance_trail_records(runtime)) @@ -2505,6 +2586,7 @@ def _tool_posture_get(runtime: McpRuntime, args: dict[str, Any]) -> dict[str, An "policy_boundary_check": _tool_policy_boundary_check, "posture_get": _tool_posture_get, "warpline_preflight_get": _tool_warpline_preflight_get, + "attestation_get": _tool_attestation_get, } diff --git a/src/legis/service/__init__.py b/src/legis/service/__init__.py index 62b11aa..883a87d 100644 --- a/src/legis/service/__init__.py +++ b/src/legis/service/__init__.py @@ -23,6 +23,7 @@ evaluate_policy, read_identity_gaps, read_lineage_integrity, + read_sei_attestations, request_signoff, resolve_for_record, submit_override, @@ -48,6 +49,7 @@ "compute_override_rate", "read_identity_gaps", "read_lineage_integrity", + "read_sei_attestations", "evaluate_policy", "explain_policy", "request_signoff", diff --git a/src/legis/service/governance.py b/src/legis/service/governance.py index d94af5c..5f9074e 100644 --- a/src/legis/service/governance.py +++ b/src/legis/service/governance.py @@ -219,6 +219,31 @@ def compute_override_rate(records: list): ) +def read_sei_attestations(verified_runtime_records: list, sei: str) -> dict[str, Any]: + """Per-SEI human-cleared attestation facts from the VERIFIED governance trail. + + ASYMMETRIC ERROR RULE: a FALSE "attested" lets warpline skip reverify on + un-cleared code (security hole); an OMITTED attestation only wastes work + (safe). Every ambiguous/failure case therefore resolves toward "not attested" + — omit the record, never surface it. ``verified_runtime_records`` MUST already + have come through ``verified_records`` — the handler guarantees this via the + protected-gate + trail-verifier pre-gate, and a tampered protected trail has + already raised AuditIntegrityError before this function is called. The + parameter is named for that contract: a future caller passing raw + ``_engine(runtime).records`` is then a self-documenting mistake. This function + takes a MATERIALIZED list (not a callable) — a bare list cannot carry the + verified/unverified distinction, so the gate decision lives in the handler. + + BLOCKED — positive admission classifier (Task 8): which records count as an + attestation, and the forge-proof discriminator, await owner ratification. + Until then this surfaces ZERO attestations (fail-closed: omit everything). + """ + records = list(verified_runtime_records) # noqa: F841 Task 8 classifier reads this + attestations: list[dict[str, Any]] = [] + # Task 8: classify `records` for `sei` here once the discriminator is ratified. + return {"status": "checked", "sei": sei, "attestations": attestations} + + def _requires_protected_verification(payload: dict[str, Any], protected_policies) -> bool: """Gate-local protected-detection for the KEYLESS branch of the override-rate gate: would refusing to score this record be right because it genuinely needs diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index a68d864..5efcff7 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -3435,3 +3435,48 @@ def reverify_worklist(self, base, head): assert sc["status"] == "checked" assert sc["impact_radius"] == {"affected": [{"sei": "S1"}], "count": 1} assert sc["reverify_worklist"] == {"entries": [], "count": 0} + + +# Task 5: attestation_get fail-closed scaffolding +# --------------------------------------------------------------------------- + + +def test_attestation_get_unavailable_when_no_protected_gate(tmp_path): + # ENGINE-ONLY DEPLOYMENT: no LEGIS_HMAC_KEY -> runtime.protected_gate is None. + # The trail is not signature-verifiable, so attestation_get MUST return a + # success-envelope unavailable, NOT a silent empty that reads as "never attested". + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) # no protected gate wired + assert runtime.protected_gate is None + result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert not result.get("isError") + sc = result["structuredContent"] + assert sc["status"] == "unavailable" + assert sc["sei"] == "mod.fn#1" + assert sc["attestations"] == [] + assert sc["unavailable"] and "reason" in sc["unavailable"][0] + + +def test_attestation_get_tamper_yields_audit_integrity_failure(tmp_path): + # FAIL-CLOSED: a tampered protected trail -> AUDIT_INTEGRITY_FAILURE, nothing + # surfaced. Build a protected runtime whose trail verifier raises TamperError. + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) + + class _TamperVerifier: + def verify(self, records): + from legis.enforcement.protected import TamperError + + raise TamperError("record 4 hash mismatch") + + class _FakeProtectedGate: + def records(self): + return ["bad-record"] + + runtime.protected_gate = _FakeProtectedGate() + runtime.trail_verifier = _TamperVerifier() + result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert result.get("isError") + assert result["structuredContent"]["error_code"] == "AUDIT_INTEGRITY_FAILURE" diff --git a/tests/service/test_governance.py b/tests/service/test_governance.py index f0ae3d4..cee8260 100644 --- a/tests/service/test_governance.py +++ b/tests/service/test_governance.py @@ -494,3 +494,16 @@ def test_source_binding_status_is_bound_into_the_signature(tmp_path): payload["extensions"]["source_binding"]["status"] = "verified" tampered = signing_fields(payload, seq=rec.seq) assert verify(tampered, result.signature, key) is False + + +# Task 5: read_sei_attestations stub +# --------------------------------------------------------------------------- + + +def test_read_sei_attestations_returns_checked_shape_on_empty_verified_trail(): + from legis.service.governance import read_sei_attestations + + out = read_sei_attestations([], "mod.fn#1") + assert out["status"] == "checked" + assert out["sei"] == "mod.fn#1" + assert out["attestations"] == [] From 3109319ef35648cf7deeb1ef3ec69062b8922991 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:21:01 +1000 Subject: [PATCH 08/16] test(mcp): bump agent surface to 24 tools for warpline + attestation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add warpline_preflight_get and attestation_get to the surface-set literal in test_initialize_and_tools_list_exposes_full_agent_surface (22→24 names), clearing the long-RED test introduced by Tasks 3 and 5. Add test_warpline_tools_introduce_no_new_error_codes to lock that both tools degrade to a success-envelope (status:"unavailable"), confirming no new error code requires _recovery_for / pinned-code list changes. Co-Authored-By: Claude Sonnet 4.6 --- tests/mcp/test_server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index 5efcff7..2b7f6a4 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -302,6 +302,8 @@ def test_initialize_and_tools_list_exposes_full_agent_surface(tmp_path): "doctor_get", "policy_boundary_check", "posture_get", + "warpline_preflight_get", + "attestation_get", } # posture_get is the dedicated read-only posture surface (Phase 8); the # change gate (posture set) stays operator/CLI only — no posture_set tool. @@ -2121,6 +2123,17 @@ def test_c8_no_agent_reachable_enablement_or_signing_surface(): assert forbidden_arg not in props +def test_warpline_tools_introduce_no_new_error_codes(tmp_path): + # warpline_preflight_get / attestation_get degrade to success-envelope + # status:"unavailable"; their only error path is the pre-existing + # AUDIT_INTEGRITY_FAILURE. No new error code => no _recovery_for / pinned-code change. + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) + assert not call_tool(runtime, "warpline_preflight_get", {"base": "x"}).get("isError") + assert not call_tool(runtime, "attestation_get", {"sei": "x#1"}).get("isError") + + def test_git_rename_feed_get_is_listed(): from legis.mcp import tool_definitions From 5833be936d21e6d56f2db99fe3fa9820fbe04f73 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:24:25 +1000 Subject: [PATCH 09/16] test(mcp): outputSchema conformance vectors for warpline + attestation tools Co-Authored-By: Claude Sonnet 4.6 --- tests/mcp/test_output_schema_conformance.py | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/mcp/test_output_schema_conformance.py b/tests/mcp/test_output_schema_conformance.py index 4ce4180..457d7a5 100644 --- a/tests/mcp/test_output_schema_conformance.py +++ b/tests/mcp/test_output_schema_conformance.py @@ -622,3 +622,52 @@ def test_policy_boundary_check_no_root_instead_of_vacuous_pass(tmp_path): scanned = _conformant(runtime, "policy_boundary_check", {"root": "specimen"}) assert scanned["outcome"] == "PASS" assert scanned["scanned_root"].endswith("specimen") + + +def test_warpline_preflight_get_unavailable_conforms(tmp_path): + runtime, _store = _runtime(tmp_path) # warpline None + payload = _conformant(runtime, "warpline_preflight_get", {"base": "aaa"}) + assert payload["status"] == "unavailable" + + +def test_warpline_preflight_get_checked_conforms(tmp_path): + class _FakeWarpline: + def impact_radius(self, base, head): + return {"affected": [], "count": 0} + + def reverify_worklist(self, base, head): + return {"entries": [], "count": 0} + + runtime, _store = _runtime(tmp_path) + runtime.warpline = _FakeWarpline() + payload = _conformant(runtime, "warpline_preflight_get", {"base": "aaa", "head": "bbb"}) + assert payload["status"] == "checked" + + +def test_attestation_get_unavailable_conforms(tmp_path): + runtime, _store = _runtime(tmp_path) # no protected gate + payload = _conformant(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert payload["status"] == "unavailable" + + +def test_attestation_get_tamper_error_conforms_to_envelope(tmp_path): + from legis.mcp import ERROR_ENVELOPE_SCHEMA, call_tool + from jsonschema import Draft202012Validator + + runtime, _store = _runtime(tmp_path) + + class _TamperVerifier: + def verify(self, records): + from legis.enforcement.protected import TamperError + + raise TamperError("mismatch") + + class _FakeProtectedGate: + def records(self): + return ["bad"] + + runtime.protected_gate = _FakeProtectedGate() + runtime.trail_verifier = _TamperVerifier() + result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert result.get("isError") + Draft202012Validator(ERROR_ENVELOPE_SCHEMA).validate(result["structuredContent"]) From 4a843c9984ac0a7aaff8525c5f664be48cad1296 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:35:22 +1000 Subject: [PATCH 10/16] test(warpline): cover attestation_get in advisory-boundary structural guard Adds _tool_attestation_get and read_sei_attestations to the warpline advisory-boundary structural test, and strengthens it by deriving tool-handler coverage from _TOOL_HANDLERS so future handlers are covered automatically. The single legitimate warpline reader (_tool_warpline_preflight_get) is excluded by name; any other handler that starts touching .warpline will cause an immediate test failure. Co-Authored-By: Claude Sonnet 4.6 --- tests/mcp/test_warpline_advisory_boundary.py | 31 +++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/mcp/test_warpline_advisory_boundary.py b/tests/mcp/test_warpline_advisory_boundary.py index ff76690..2864c63 100644 --- a/tests/mcp/test_warpline_advisory_boundary.py +++ b/tests/mcp/test_warpline_advisory_boundary.py @@ -145,19 +145,30 @@ def test_runtime_warpline_referenced_in_no_verdict_path_function(): # verdict-path / honesty-read source. NOTE inspect.getsource is a SHALLOW text # scan — it sees only these named functions, not helpers they call — so this # COMPLEMENTS, never replaces, the byte-identity test above. + # + # Tool-handler coverage is DERIVED from _TOOL_HANDLERS so that any future + # handler is covered by construction. The single legitimate advisory handler + # (_tool_warpline_preflight_get) is excluded by name — any other handler that + # starts reading .warpline will cause this test to fail immediately. import legis.mcp as mcp - - verdict_path_fns = [ - mcp._tool_policy_evaluate, + from legis.service.governance import read_sei_attestations + + # --- derived: every tool handler except the one legitimate advisory handler --- + _WARPLINE_HANDLER = "_tool_warpline_preflight_get" + for name, handler in mcp._TOOL_HANDLERS.items(): + if handler.__name__ == _WARPLINE_HANDLER: + continue + src = inspect.getsource(handler) + assert ".warpline" not in src, ( + f"tool handler {handler.__name__!r} (tool={name!r}) references warpline" + ) + + # --- explicit: non-handler verdict internals not in _TOOL_HANDLERS --- + for fn in [ mcp._engine, mcp._coached_engine, mcp._governance_trail_records, - mcp._tool_identity_gap_list, - mcp._tool_lineage_integrity_get, - mcp._tool_policy_boundary_check, - mcp._tool_signoff_status_get, - mcp._tool_override_submit, - ] - for fn in verdict_path_fns: + read_sei_attestations, + ]: src = inspect.getsource(fn) assert ".warpline" not in src, f"{fn.__name__} references warpline" From 11ab7f8e8ab89051c1c7afd15a3f51a223990ba4 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:43:35 +1000 Subject: [PATCH 11/16] fix(governance): attestation_get returns unavailable while classifier BLOCKED (no false-green) read_sei_attestations previously returned status='checked' with an empty attestations list even when the positive-admission classifier (Task 8) had not run. In a key-wired deployment this passes the pre-gate in _tool_attestation_get and reaches read_sei_attestations, producing a false-green: 'checked: []' asserts "I checked this SEI and found no attestation" when no check actually ran. The honesty surface forbids this verbatim (asymmetric rule: unavailable is safe, false-checked is a security hole letting warpline skip reverify). Fix: return status='unavailable' with reason "attestation classifier pending owner ratification (Task 8)" until Task 8 ratifies the discriminator. Tests: renamed unit test to assert the unavailable-pending shape; added test_attestation_get_wired_deployment_returns_unavailable_not_false_checked to cover the previously-untested key-wired path that was the buggy false-green. Co-Authored-By: Claude Sonnet 4.6 --- src/legis/service/governance.py | 17 +++++++++++++---- tests/mcp/test_server.py | 31 +++++++++++++++++++++++++++++++ tests/service/test_governance.py | 10 ++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/legis/service/governance.py b/src/legis/service/governance.py index 5f9074e..3a3497f 100644 --- a/src/legis/service/governance.py +++ b/src/legis/service/governance.py @@ -237,11 +237,20 @@ def read_sei_attestations(verified_runtime_records: list, sei: str) -> dict[str, BLOCKED — positive admission classifier (Task 8): which records count as an attestation, and the forge-proof discriminator, await owner ratification. Until then this surfaces ZERO attestations (fail-closed: omit everything). + While the positive-admission classifier is BLOCKED (Task 8), this returns + status='unavailable' (reason: classifier pending ratification), NOT an empty + status='checked' — an empty 'checked' would falsely assert "I checked this SEI + and found no attestation" when the classifier did not actually check (the + silent false-green the honesty surface forbids). Unavailable is safe under the + asymmetric rule: warpline reverifies. """ - records = list(verified_runtime_records) # noqa: F841 Task 8 classifier reads this - attestations: list[dict[str, Any]] = [] - # Task 8: classify `records` for `sei` here once the discriminator is ratified. - return {"status": "checked", "sei": sei, "attestations": attestations} + records = list(verified_runtime_records) # noqa: F841 - Task 8 classifier will read this + return { + "status": "unavailable", + "sei": sei, + "attestations": [], + "unavailable": [{"reason": "attestation classifier pending owner ratification (Task 8)"}], + } def _requires_protected_verification(payload: dict[str, Any], protected_policies) -> bool: diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index 2b7f6a4..11c1d5d 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -3493,3 +3493,34 @@ def records(self): result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) assert result.get("isError") assert result["structuredContent"]["error_code"] == "AUDIT_INTEGRITY_FAILURE" + + +def test_attestation_get_wired_deployment_returns_unavailable_not_false_checked(tmp_path): + # KEY-WIRED DEPLOYMENT HONESTY: when BOTH protected_gate AND trail_verifier + # are wired (the deployment that actually has real sign-offs/overrides), the + # pre-gate passes and read_sei_attestations is called. While the classifier + # is BLOCKED (Task 8), this MUST return status='unavailable' with the + # classifier-pending reason — NOT status='checked' with an empty attestations + # list, which would falsely assert "I checked this SEI and found nothing" on + # exactly the deployments that have real governance records. + from legis.mcp import call_tool + + runtime, _store = _runtime(tmp_path) + + class _OkVerifier: + def verify(self, records): + return None + + class _FakeProtectedGate: + def records(self): + return [] + + runtime.protected_gate = _FakeProtectedGate() + runtime.trail_verifier = _OkVerifier() + result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) + assert not result.get("isError") + sc = result["structuredContent"] + assert sc["status"] == "unavailable" + assert sc["sei"] == "mod.fn#1" + assert sc["attestations"] == [] + assert sc["unavailable"] and "pending" in sc["unavailable"][0]["reason"] diff --git a/tests/service/test_governance.py b/tests/service/test_governance.py index cee8260..edf1055 100644 --- a/tests/service/test_governance.py +++ b/tests/service/test_governance.py @@ -500,10 +500,16 @@ def test_source_binding_status_is_bound_into_the_signature(tmp_path): # --------------------------------------------------------------------------- -def test_read_sei_attestations_returns_checked_shape_on_empty_verified_trail(): +def test_read_sei_attestations_unavailable_while_classifier_blocked(): + # While the positive-admission classifier (Task 8) is BLOCKED, the function + # MUST return status='unavailable' with the classifier-pending reason, NOT + # status='checked' with an empty list — an empty 'checked' would falsely + # assert "I checked this SEI and found no attestation" when the classifier + # did not actually check (the silent false-green the honesty surface forbids). from legis.service.governance import read_sei_attestations out = read_sei_attestations([], "mod.fn#1") - assert out["status"] == "checked" + assert out["status"] == "unavailable" assert out["sei"] == "mod.fn#1" assert out["attestations"] == [] + assert out["unavailable"] and "pending" in out["unavailable"][0]["reason"] From 0b229590eac03cb3b6aca8554c1033f3a9158a1e Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:51:59 +1000 Subject: [PATCH 12/16] product: accept warpline bet Tasks 1-7, defer Task 8 (BLOCKED); PDR-0002, PDR-0003 Checkpoint of the warpline-interfaces build session. Records the accept of the advisory preflight consumer + attestation fail-closed scaffolding, the federation-read doctrine (facts-not-verdict), and the Task-8/merge/spec-correction owner escalations. Workspace committed on branch warpline-interfaces; lands on main when the branch merges. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/product/current-state.md | 24 +++++++++++ .../0001-bootstrap-from-observed-state.md | 21 ++++++++++ ...02-accept-warpline-bet-defer-classifier.md | 23 +++++++++++ .../0003-federation-read-doctrine.md | 22 ++++++++++ docs/product/metrics.md | 26 ++++++++++++ docs/product/roadmap.md | 18 ++++++++ docs/product/vision.md | 41 +++++++++++++++++++ 7 files changed, 175 insertions(+) create mode 100644 docs/product/current-state.md create mode 100644 docs/product/decisions/0001-bootstrap-from-observed-state.md create mode 100644 docs/product/decisions/0002-accept-warpline-bet-defer-classifier.md create mode 100644 docs/product/decisions/0003-federation-read-doctrine.md create mode 100644 docs/product/metrics.md create mode 100644 docs/product/roadmap.md create mode 100644 docs/product/vision.md diff --git a/docs/product/current-state.md b/docs/product/current-state.md new file mode 100644 index 0000000..a046f4d --- /dev/null +++ b/docs/product/current-state.md @@ -0,0 +1,24 @@ +# Current State — Legis Checkpoint: 2026-06-25 · committed (PDR-0002, PDR-0003) + +## The bet right now +**Keep the governance-honesty surface true post-gold** (north-star: open governance-honesty defects → 0) — close the three confirmed P2 findings — while the **Warpline federation seam**, now BUILT, awaits owner sign-off to merge and to unblock its attestation classifier. + +## In flight +- **Warpline interfaces** (legis-1734128d34) — **BUILT** on branch `warpline-interfaces` (8 commits, `5a30cd8..11ab7f8`): advisory preflight consumer (`warpline_preflight_get`) + `attestation_get` fail-closed scaffolding + the byte-identical advisory-boundary spine. All CI-equivalent gates green (pytest 1225, mypy clean, coverage 92.14%, ruff). **Gated on owner** (PDR-0002): merge (federation contract) + Task-8 classifier ratification. The classifier is BLOCKED and ships honest `unavailable` (no false-green). +- **Governance-honesty P2 findings** — confirmed, ready, unclaimed: unverified posture tail (legis-476ab6f125); protected batch without HeadAnchor advance (legis-0c310712a7); policy_boundary_check accepts roots outside source root (legis-0186c23a2c). The north-star Now bet; untouched this session. + +## Open questions / blocked-on-owner +- **Task 8 ratification (4 questions)** — to unblock `attestation_get`'s classifier: (1) operator-override = a *verifying* `judge_metadata_signature`, never the bare field; (2) only *signed* sign-offs attest; (3) no-key deployments can't attest → `unavailable`; (4) absent `content_hash` → omit, never `""`. +- **Merge / publish** `warpline-interfaces` — binds a sibling → owner sign-off. `warpline_preflight_get` is independently valuable; merge-now-then-ratify and hold-the-branch are both clean. +- **Spec §4.1 correction** — design spec lines 92/102 assert a fail-closed guarantee that's false in the no-key deployment; code is correct, prose is stale. Recommend correcting (escalated, not done). +- **Warpline wire format** — §6 inferred (TO-CONFIRM); ships shape-validating, degrades to `unavailable`. Gates real integration, not unit work. +- **Inferred vision/metrics** — PDR-0001's reversal trigger fires on the owner's first review (the `(set)` time-to-close TARGET still needs an owner number). + +## Last checkpoint did +- Dispatched → built → **accepted** the warpline bet Tasks 1–7 via subagent-driven TDD from a grounded, adversarially-reviewed plan; all CI-equivalent gates green (PDR-0002). +- Recorded the **federation-read doctrine** — facts-not-verdict, advisory context structurally isolated (PDR-0003). +- Caught + fixed two review-found defects pre-merge: a stale structural guard, and a **false-green** in the BLOCKED attestation stub (`checked: []` → honest `unavailable`). +- Deferred Task 8 (classifier) as BLOCKED; flagged merge + Task-8 + spec-correction for the owner. + +## Next session, start here +Either the owner's answers to the warpline escalations (Task-8 ratification → unblock + implement the classifier; or merge sign-off), **or** pick up the north-star Now bet — the three P2 governance-honesty findings (legis-476ab6f125, -0c310712a7, -0186c23a2c), still unclaimed. diff --git a/docs/product/decisions/0001-bootstrap-from-observed-state.md b/docs/product/decisions/0001-bootstrap-from-observed-state.md new file mode 100644 index 0000000..85b584e --- /dev/null +++ b/docs/product/decisions/0001-bootstrap-from-observed-state.md @@ -0,0 +1,21 @@ +# PDR-0001 — Bootstrap the Legis product workspace from observed state + +Date: 2026-06-24 Status: accepted Author: claude (opus, product-owner) Owner sign-off: partial (grant confirmed live; inferred vision/metrics not yet confirmed) +Supersedes: — Related: vision.md, roadmap.md, metrics.md, current-state.md + +## Context +Legis had no product-ownership workspace (`/product-checkpoint` halted on the missing precondition). Legis is a shipped gold product (v1.1.1) with a rich repo, README, CHANGELOG, a Weft member briefing, and a 145-issue filigree tracker. A workspace had to be constructed from that observed reality rather than fabricated from memory, so the next session can resume cold. + +## Options considered +1. **Bootstrap from observed reality (README + git log + tracker + member briefing), confirm only the authority grant live.** — pro: grounded in fact, fast, the one load-bearing item (the grant) is human-confirmed; con: purpose/audience/metric _numbers_ are inferred and may need correction. +2. **Interrogate the owner for vision/metrics before writing anything.** — pro: maximally accurate; con: slow, and the README + member briefing already state purpose and direction plainly — interrogation would mostly re-elicit what's already written. +3. **Do nothing / stay stateless.** — pro: no risk of a wrong inference; con: forfeits continuity entirely, which is the whole point of ownership. + +## The call +Option 1. Seeded `vision.md`, `roadmap.md`, `metrics.md`, `current-state.md` from the README, the Weft member briefing (`~/weft/members/legis.md`), git history, and the filigree tracker. The **authority grant was proposed and confirmed live** by the owner (Standard variant) during this `/own-product` run, so it is written as authoritative (not draft). Everything else is inferred-from-repo and marked as such. + +## Rationale +The repo states purpose, audience, anti-goals, and recent direction explicitly and consistently with the federation doctrine; inference here is reading, not guessing. The only item that genuinely required a human (the delegation boundary) was confirmed interactively. Metric _numbers_ that the repo doesn't expose are left as `(set)` placeholders rather than invented, keeping every target falsifiable-or-flagged. + +## Reversal trigger +Revisit on the **first owner review of this workspace**: if the owner corrects the purpose/audience framing in `vision.md` or sets real numbers against any `(set)` metric placeholder, supersede the inferred portions with a new PDR. Also revisit if the north-star ("open confirmed governance-honesty defects = 0") proves to be the wrong success measure once the P2 findings close. diff --git a/docs/product/decisions/0002-accept-warpline-bet-defer-classifier.md b/docs/product/decisions/0002-accept-warpline-bet-defer-classifier.md new file mode 100644 index 0000000..047e97a --- /dev/null +++ b/docs/product/decisions/0002-accept-warpline-bet-defer-classifier.md @@ -0,0 +1,23 @@ +# PDR-0002 — Accept the warpline interfaces bet (Tasks 1–7); defer the attestation classifier (Task 8) as BLOCKED + +Date: 2026-06-25 Status: accepted (build/accept within grant; MERGE is owner-gated — see flags) Author: claude (opus, product-owner) +Supersedes: — Related: [[0003-federation-read-doctrine]]; tracker legis-1734128d34; spec `docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md`; plan `docs/superpowers/plans/2026-06-24-legis-warpline-interfaces-plan.md` + +## Context +Warpline (Weft sibling; impact-radius / reverify-worklist analysis) requested two Legis interfaces: an advisory preflight consumer and a per-SEI attestation read (governance-as-verification, Rung 2). This was the Now federation-seam bet (legis-1734128d34). Load-bearing constraint: governance verdicts must be **byte-identical** whether warpline is present or absent; Legis stays the **only** governance/attestation authority. + +## Options considered +1. Build the whole feature including the attestation classifier in one pass. +2. Build the advisory boundary + the attestation **fail-closed scaffolding**, and DEFER the positive-admission classifier pending owner ratification of a forge-proof discriminator. +3. Don't build / defer the whole bet. + +## The call +Option 2. Built Tasks 1–7 via subagent-driven TDD from a grounded, adversarially-reviewed plan: the stdlib `HttpWarplineClient`, `warpline_preflight_get` (advisory), `attestation_get` fail-closed scaffolding, the byte-identical advisory-boundary acceptance spine, and a derived structural guard over all tool handlers. The positive-admission classifier (Task 8) is **BLOCKED** and ships an honest `unavailable` (no false-green) because grounding proved the obvious operator-override discriminator is **forgeable** (the chill engine writes caller `extensions` verbatim) — shipping it unratified risks a false-"attested" → warpline skips reverify on un-cleared code (a security hole). Accepted on all CI-equivalent gates green (pytest 1225 passed, mypy clean, coverage 92.14% ≥88% floor, ruff) + a whole-branch review. + +## Rationale +The advisory boundary is the whole point of the bet and is now proven (structural + behavioral byte-identity), not asserted. The classifier is the single piece that can introduce a false-green on the honesty surface, and its safe discriminator is a **spec-level security decision** that requires the owner. Deferring it (fail-closed, surfaces nothing) is honest and reversible; guessing it is a one-way security risk. Two review-caught defects — a stale structural guard and a false-green BLOCKED stub (`checked: []` in wired deployments) — were found and fixed before merge. + +## Reversal trigger +- If the byte-identical advisory-boundary invariant ever fails (a governance verdict diverges with warpline present vs absent) → **reopen immediately**; that invariant is the guardrail (metrics.md). +- If the owner ratifies the four Task-8 classifier questions → Task 8 unblocks as a **new** decision (new PDR), flipping `attestation_get` from `unavailable` to real attestations. +- If warpline's real wire format contradicts the inferred §6 parser → the client's URL/parse layer reopens (isolated; degrades to `unavailable` until then). diff --git a/docs/product/decisions/0003-federation-read-doctrine.md b/docs/product/decisions/0003-federation-read-doctrine.md new file mode 100644 index 0000000..f036443 --- /dev/null +++ b/docs/product/decisions/0003-federation-read-doctrine.md @@ -0,0 +1,22 @@ +# PDR-0003 — Federation-read doctrine: Legis exposes verified FACTS, never a verdict; advisory context is structurally isolated + +Date: 2026-06-25 Status: accepted (reinforces existing vision anti-goals — no new sign-off) Author: claude (opus, product-owner) +Supersedes: — Related: [[0002-accept-warpline-bet-defer-classifier]]; vision.md anti-goals ("a second judge of trust", "an identity owner") + +## Context +The warpline seam is Legis's first time being an HTTP **client** of a sibling on a path next to governance reads, and the first time it **exposes** a read a sibling treats as proof. This pattern recurs (Clarion SEI consume; future sibling fact-providers), so it needs a durable principle to stop future seams from eroding the authority boundary. + +## Options considered +1. Legis returns a `proven_good` / skip-reverify **verdict** the sibling acts on directly (Legis decides). +2. Legis returns verified **FACTS** (attested content_hash, kind, seq); the sibling does its own Rung-2 commit-match and skip decision (Legis attests, the sibling decides). +3. Embed advisory sibling reads **inside** the governance honesty reads. + +## The call +Option 2 for attestation; option 3 **rejected** — advisory context lives in a DEDICATED sibling tool structurally isolated from every verdict path. `attestation_get` returns facts; warpline makes the skip-reverify call. `warpline_preflight_get` is a sibling tool whose data is never an input to `policy_evaluate` / the gates / sign-off / the honesty reads, enforced by a derived structural test over all tool handlers (covers future tools by construction). + +## Rationale +"Wardline analyses, Legis governs." Legis must never become a second judge of trust, and advisory context must be **structurally incapable** of reaching a verdict — not merely "we didn't wire it." Facts-not-verdict keeps Legis the sole authority while still letting siblings build verification on top. This is the doctrine for every future federation read. + +## Reversal trigger +- If a sibling shows a need Legis cannot meet with facts-only (a genuine case where Legis must *decide*) → revisit as an explicit **vision / authority-grant escalation** (it would change the "not a second judge" anti-goal). +- If any future seam is found feeding sibling data into a verdict path → treat as a **Critical** regression against this doctrine. diff --git a/docs/product/metrics.md b/docs/product/metrics.md new file mode 100644 index 0000000..4052db7 --- /dev/null +++ b/docs/product/metrics.md @@ -0,0 +1,26 @@ +# Metrics — Legis Last read: 2026-06-25 + +> Legis is a governance-_honesty_ tool, not an engagement product. Its north-star +> is integrity of the honesty surface (no provable false-greens, no unverified +> trust on a hot path), not usage. Targets below carry a number and a date so a +> bet can be accepted or a PDR reversal trigger can fire. BASELINE/TARGET +> placeholders marked `(set)` need a real number from the owner. + +## North-star +| Metric | Target (falsifiable) | Current | Read on | Trend | +|--------|----------------------|---------|---------|-------| +| Open **confirmed governance-honesty defects** (security/governance findings that let the honesty surface be bypassed or trust be trusted unverified) | = 0 by 2026-07-15 | 3 (legis-476ab6f125, -0c310712a7, -0186c23a2c) | 2026-06-24 | → | + +## Input metrics (the levers that move the north-star) +| Metric | Target | Current | Read on | +|--------|--------|---------|---------| +| Confirmed P2 security findings remaining open | 0 by 2026-07-15 | 3 | 2026-06-24 | +| Median time-to-close on a confirmed security finding | ≤ 14 days `(set)` | unmeasured | 2026-06-24 | + +## Guardrails (must NOT degrade) +| Metric | Floor / ceiling | Current | Read on | +|--------|-----------------|---------|---------| +| **Advisory-boundary invariant** — governance verdicts byte-identical with a sibling (Warpline) absent vs present | must hold (binary) | **holds — proven** by `tests/mcp/test_warpline_advisory_boundary.py` (byte-identical on real verdicts) + a derived structural guard over all tool handlers; warpline consumed only in its sibling tool | 2026-06-25 | +| CI green (tests + mypy) | 100% | `warpline-interfaces` branch: pytest 1225 passed, mypy clean (78 files) — CI-equivalent gate run locally; `main` green at 1.1.1 | 2026-06-25 | +| Release publish gated on **live Loomweave SEI conformance** | must pass before any PyPI publish | gate in place (PR #8 line) | 2026-06-24 | +| Test coverage vs configured floors (`scripts/check_coverage_floors.py`) | ≥ floors | **92.14% total** (floor 88); all per-package floors hold (mcp.py 92.4%, service/ 95.0%) — read on `warpline-interfaces` | 2026-06-25 | diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md new file mode 100644 index 0000000..65afd84 --- /dev/null +++ b/docs/product/roadmap.md @@ -0,0 +1,18 @@ +# Roadmap — Legis Updated: 2026-06-24 (PDR-0001) + +> Sequencing, WSJF / cost-of-delay, and dated forecasts are produced by +> /axiom-program-management. This file records bets as INTENT, not a delivery +> schedule. Do not compute WSJF here; hand the committed bet over for sequencing. + +## Now (committed, in-flight) +- **Governance-honesty integrity, post-gold** — keep the surface that earns the gold line _true_: close the confirmed P2 codex-security findings that let the honesty surface be bypassed (unverified posture tail, un-anchored protected batch, unbounded policy-boundary root) · tracker: legis-476ab6f125, legis-0c310712a7, legis-0186c23a2c · metric: north-star (open governance-honesty defects → 0) +- **Federation interface readiness — Warpline seam** — publish the advisory preflight consumer + per-SEI attestation read warpline requested, without ever letting advisory context reach a verdict · tracker: legis-1734128d34 · metric: guardrail (advisory-boundary invariant holds) + +## Next (shaped, decreasing certainty) +- **v2: unify keyed signing onto operator elevation sessions** — migrate protected-cell verdict + sign-off signing onto the elevation-session primitive shipped in the posture-ratchet line · tracker: legis-11b3a3dd14, legis-2d0537655d +- **Federation integration hardening (live-fire)** — run the cross-tool seams against real daemons, not stubs: real-Filigree bind/closure (G12), move-aware SEI backfill (G2), backfill failure guards (G8), two-way rename-parser conformance vectors (G16) · tracker: legis-356fe094dd, legis-bc9e5f3e60, legis-b7ce9fdc40, legis-c4cbf78fdb + +## Later (directional bets, no order, no dates) +- **Additional key-custody backends** — 1Password / Vault signer backends beyond the v1 OS-keychain + age-file + env escape hatch. +- **Broader provider seams** — Clarion SEI consume seam and any further sibling fact-provider contracts as the federation grows. +- **Audit-trail retention / compaction** — the honest lever for the O(N)-per-read trail-verification cost if trail size ever becomes latency-bound. diff --git a/docs/product/vision.md b/docs/product/vision.md new file mode 100644 index 0000000..990f5e5 --- /dev/null +++ b/docs/product/vision.md @@ -0,0 +1,41 @@ +# Vision — Legis + +## Purpose +Legis is the **governance surface of the Weft suite**: it turns "did a human authorize this change, and is that authorization still valid for the code as it stands now?" into a cited, durable, machine-readable fact. It records SEI-keyed governance verdicts, overrides, sign-offs, and audit lineage over the 2×2 enforcement cells (chill / coached / structured / protected), and exposes the git/CI provenance surface (branch / commit / PR / CI state) that those verdicts attach to. Its defining property is **governance _honesty_** — it never reports a green it cannot prove, and an unauthorized or unverifiable change is always _detectable_. It serves agents first: humans supervise, approve, and govern from **outside** the operating loop, not inside it. + +## Who it serves +- **Primary:** coding agents operating under governance — the agent-customers of the Legis MCP/HTTP surface that submit overrides, request sign-offs, read attestations, and route Wardline findings. +- **Secondary:** human operators who supervise and authorize from outside the loop (sign-offs, posture floor, key custody, release gates) — served, but never pulled _into_ the operating cycle. +- **Sibling tools** (Loomweave, Wardline, Filigree, Warpline) consuming Legis's git-rename feed and governance attestations across the federation contracts. +- **Explicitly not:** humans who want a config-driven CI dashboard; anyone wanting Legis to be a general-purpose CI runner, a trust/taint analyzer, or an identity authority. + +## Anti-goals (what it refuses to be) +- **A second judge of trust.** "Wardline analyses, Legis governs." Trust vocabulary passes through verbatim; Legis never re-adjudicates a taint/trust verdict. +- **An identity owner.** The SEI is opaque to Legis. It consumes Loomweave's `resolve_sei` / `lineage`; it never parses or mints identity. +- **Tamper-_proof_.** Legis is a governance-_honesty_ tool. The honest claim is "an unauthorized change is detectable," never "impossible." +- **A config-driven security boundary.** Config is not a security boundary; signing keys stay out of agent reach. Governance is promoted into signed records, not editable TOML. +- **Human-in-the-loop by default.** Zero _human_ config; the instruction layer is the configuration mechanism. Humans gate by exception, not by operating the tool. + +## Authority grant +Granted by: john (john@pgpl.net) Last reviewed: 2026-06-24 +Review cadence: monthly, or on any vision change + +Autonomous within strategy — the agent MAY, without asking: + prioritize the backlog, write PRDs, dispatch delivery, accept against + criteria, reprioritize, kill a failing bet per metrics.md. + +Escalate BEFORE acting — the agent MUST get owner sign-off for: + - changing this vision / strategy / authority grant + - a PyPI publish or a GitHub release (outward-facing) + - deprecating a feature siblings or users depend on + - changing a **federation contract that binds a sibling tool** (the + contracts-index seams: SEI consumption, git-rename provider, Filigree + sign-off binding, Wardline routing, Warpline preflight) — these bind + external parties (sibling maintainers), so a contract change escalates + - a pricing / commercial / licensing change + - deleting data or any irreversible data operation + - anything touching an external party (sibling maintainers, users, registries) + (Taxonomy + rationale: product-ownership-operating-model.md.) + +> Grant **confirmed live** by the owner on 2026-06-24 during bootstrap +> (`/own-product`). Status: confirmed, not draft. From da85e162c2ed488f315299cf696f132903f704b0 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:17:23 +1000 Subject: [PATCH 13/16] =?UTF-8?q?feat(governance):=20forge-proof=20per-SEI?= =?UTF-8?q?=20attestation=20classifier=20(Task=208=20ratified)=20+=20spec?= =?UTF-8?q?=20=C2=A74=20correction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement read_sei_attestations: admit operator_override and signoff_cleared attestations from the VERIFIED governance trail, keying only off SIGNED fields. - operator_override: gated on the judge_metadata_signature MARKER (proves the record was in the verified selection), signed judge_verdict == OVERRIDDEN_BY_OPERATOR (FORGE-A closed), signed protected_cell, signed inline loomweave.content_hash (non-empty), and the signed entity_key dict == sei + identity_stable. - signoff_cleared: gated on the signoff_signature MARKER, SIGNED_OFF state, and the FORGE-B integrity join — recompute content_hash over the full stored PENDING payload and require it == the signed request_payload_hash; surface the PENDING's loomweave.content_hash (non-empty). seq = SIGNED_OFF, signoff_seq = request_seq. The function now always returns status='checked' (the handler pre-gate owns the unavailable case). Added forge-negative tests (FORGE-A/B, chill stuffing, unsigned procedural sign-off, cross-SEI, identity_stable False, empty content_hash, BLOCKED) + positive end-to-end MCP test. Spec §4.1/§4.2/§7 corrected: the unconditional "forged attested never returned" claim is false (verifiable only when both gate+verifier wired); fixed lowercase 'signed_off' -> 'SIGNED_OFF'; replaced the "pinned during implementation" hand-wave with the ratified discriminator. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-06-24-legis-warpline-interfaces-design.md | 16 +- src/legis/service/governance.py | 129 ++++++- tests/mcp/test_server.py | 54 ++- tests/service/test_governance.py | 318 +++++++++++++++++- 4 files changed, 475 insertions(+), 42 deletions(-) diff --git a/docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md b/docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md index c7be0ce..60e9a3f 100644 --- a/docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md +++ b/docs/superpowers/specs/2026-06-24-legis-warpline-interfaces-design.md @@ -89,7 +89,7 @@ and held on `McpRuntime` as `warpline: Any | None = None`. ### 4.1 Tool: `attestation_get` - **Input:** `{sei: string (required)}`. Legis does **not** accept a commit. Warpline resolves the SEI's content_hash at commit X via Loomweave (which it already queries for `timeline` / `changed`) and matches it against the `content_hash` Legis returns. The match — and the "skip reverify" decision — is warpline's Rung-2 call, not Legis's. -- **Service function `read_sei_attestations(runtime_records, sei)`** in `service/governance.py` (next to the other honesty reads). It consumes the **verified governance trail** via the existing `verified_records` / `_governance_trail_records` path, so a tampered trail raises `AuditIntegrityError → AUDIT_INTEGRITY_FAILURE` — a forged "attested" is never returned. +- **Service function `read_sei_attestations(runtime_records, sei)`** in `service/governance.py` (next to the other honesty reads). It is reached **only when the trail is signature-verifiable** — the handler pre-gate requires BOTH a protected gate AND a `trail_verifier` (governance.py `verified_records` runs `TrailVerifier.verify` only when both are wired; with neither, it would return engine records UNVERIFIED). When verifiable, a tampered/forged record raises `AuditIntegrityError → AUDIT_INTEGRITY_FAILURE` upstream of the classifier, OR — for a forge the chain signature cannot catch, e.g. a mutated **unsigned** PENDING-request `content_hash` (FORGE-B) — the classifier itself omits it. The classifier admits a record only when every field it keys on is **covered by a signature** (`signing_fields` / `signoff_signing_fields`); a forged "attested" is therefore never returned when the trail is verifiable. - **Honest discriminated output:** `status: "checked"`: @@ -99,7 +99,7 @@ and held on `McpRuntime` as `warpline: Any | None = None`. {"kind": "operator_override", "content_hash": "...", "recorded_at": "...", "seq": 19} ]} ``` - `status: "unavailable"` (no governance trail wired — no `LEGIS_HMAC_KEY`, so no protected/sign-off gate): + `status: "unavailable"` (trail not signature-verifiable — no protected gate / no `trail_verifier`; a no-key deployment still HAS a trail — chill overrides, unsigned procedural sign-offs — it is UNVERIFIABLE, not absent, and reading unverified records would be a false-green): ```json {"status": "unavailable", "sei": "", "attestations": [], "unavailable": [{"reason": "..."}]} ``` @@ -108,12 +108,14 @@ and held on `McpRuntime` as `warpline: Any | None = None`. Only governance records that represent a **human clearance** for the SEI count — the strongest "governed-good" signal warpline can safely skip reverification on: -1. **Cleared sign-offs** — a `SIGNED_OFF` record (`extensions.signoff_state == "signed_off"`) whose `entity_key` is the queried SEI and `identity_stable` is true. Its `content_hash` is joined from the matching `PENDING` request via `extensions.request_seq` (the cleared record itself carries no loomweave content_hash; the request does, at `extensions.loomweave.content_hash`). `kind: "signoff_cleared"`, `signoff_seq` = the request seq. -2. **Protected operator-overrides** — an operator-override verdict record for the SEI (content_hash inline at `extensions.loomweave.content_hash`). `kind: "operator_override"`. +1. **Cleared sign-offs** — a `SIGNED_OFF` record (`extensions.signoff_state == SignoffState.SIGNED_OFF.value == "SIGNED_OFF"`, UPPERCASE — `verdict.py`) carrying a `signoff_signature` (the verified-selection marker), whose `entity_key` is the queried SEI and `identity_stable` is true. Its `content_hash` is joined from the matching `PENDING` request via the **signed** `extensions.request_seq` (the cleared record carries no loomweave content_hash of its own; the request does, at `extensions.loomweave.content_hash`). The join is **integrity-checked, not trusted** (FORGE-B): the classifier recomputes `legis.canonical.content_hash` over the FULL stored PENDING payload and requires it == the SIGNED_OFF record's signed `extensions.request_payload_hash`. `kind: "signoff_cleared"`, `seq` = the SIGNED_OFF record seq, `signoff_seq` = the request seq. Absent/empty content_hash → omit. +2. **Protected operator-overrides** — a record carrying a `judge_metadata_signature` (the verified-selection marker) with the **signed** `extensions.judge_verdict == Verdict.OVERRIDDEN_BY_OPERATOR.value` (`signing_fields["verdict"]` — FORGE-A is closed: mutating it breaks the signature and fails closed upstream), `extensions.protected_cell is True` (signed), entity == SEI with `identity_stable` true (read from the SIGNED `entity_key` dict, never the unsigned top-level `identity_stable` duplicate), and a non-empty inline `extensions.loomweave.content_hash` (signed). `kind: "operator_override"`. -**Explicitly excluded:** chill/coached self-clear overrides and `BLOCKED` verdicts — they are not proof of anything warpline should skip reverification on. (The decision is conservative on purpose; broadening the set later is additive.) +**Discriminator discipline (necessary-but-not-sufficient trap):** admission keys off the **signature MARKER** (`judge_metadata_signature` / `signoff_signature`) present on the candidate, NOT the bare `judge_verdict` / `signoff_state` value — a marker-less injected record can ride through `TrailVerifier._requires_verification` UNVERIFIED, so marker presence is what proves THIS record was in the verified selection. The classifier never re-verifies signatures (the pre-gate did); it only keys off fields inside `signing_fields` / `signoff_signing_fields` and independently integrity-checks the sign-off join. Never key off `judge_advisory_verdict` or the simple-tier engine `judge_verdict` (both unsigned). -The exact record discriminators (the operator-override marker, the SEI/`identity_stable` filter, the request-join) are pinned against the real record shapes in `enforcement/signoff.py` and `enforcement/protected.py` during implementation and covered by unit tests. +**Explicitly excluded (omitted):** chill/coached self-clear overrides, unsigned/procedural sign-offs (no `signoff_signature`), `BLOCKED` verdicts, empty content_hash, cross-SEI, `identity_stable` false — they are not proof of anything warpline should skip reverification on. WHEN IN DOUBT, OMIT. (The decision is conservative on purpose; broadening the set later is additive.) + +**Shipped sound:** BOTH kinds (`operator_override`, `signoff_cleared`) shipped — each distinguishing/content field it keys on is signed (`signing_fields` protected.py / `signoff_signing_fields` signoff.py), so membership in the verified set proves the field authentic and the request-join is integrity-bound via the signed `request_payload_hash`. Neither kind is escalated/blocked. Covered by forge-negative unit tests in `tests/service/test_governance.py` and an end-to-end MCP test in `tests/mcp/test_server.py`. ## 5. Rename feed (part b, item 1) — confirm, don't rebuild @@ -132,7 +134,7 @@ Shape validation is **minimal and tolerant**: the client requires each response - **No silent empties.** Both new reads discriminate `checked` from `unavailable`. An unreachable/unconfigured warpline → `unavailable` with a reason, never an empty affected-set that reads as "nothing impacted". An unwired governance trail → `unavailable`, never an empty attestations list that reads as "never attested". - **Advisory failure is contained.** A `WarplineError` is caught in the service layer and mapped to `unavailable`; it never becomes `INTERNAL_ERROR` and never perturbs a governance read (different tool, different call). -- **Attestation reads are fail-closed.** Tamper → `AUDIT_INTEGRITY_FAILURE` via the shared `verified_records` path. No forged attestation is ever returned. +- **Attestation reads are fail-closed.** A trail that is **not signature-verifiable** (no protected gate / no `trail_verifier` — e.g. no `LEGIS_HMAC_KEY`) → `unavailable`, reading no unverified record (NOT an empty `checked` that reads as "never attested"). When the trail IS verifiable: tamper → `AUDIT_INTEGRITY_FAILURE` via the shared `verified_records` path, and any forge the chain signature cannot catch (a mutated unsigned PENDING `content_hash`, FORGE-B) is omitted by the classifier's integrity-join. The classifier admits only signature-covered fields, so no forged attestation is returned **when the trail is verifiable**. - **Insecure transport is opt-in.** `http` to non-loopback warpline is rejected unless `LEGIS_ALLOW_INSECURE_REMOTE_HTTP=1`, with the same warning Filigree logs. ## 8. Testing diff --git a/src/legis/service/governance.py b/src/legis/service/governance.py index 3a3497f..380d471 100644 --- a/src/legis/service/governance.py +++ b/src/legis/service/governance.py @@ -20,6 +20,7 @@ TrailVerifier, ) from legis.enforcement.signoff import SignoffGate, SignoffResult +from legis.enforcement.verdict import SignoffState, Verdict from legis.governance import params from legis.identity.entity_key import EntityKey from legis.identity.resolver import IdentityResolver @@ -234,23 +235,119 @@ def read_sei_attestations(verified_runtime_records: list, sei: str) -> dict[str, takes a MATERIALIZED list (not a callable) — a bare list cannot carry the verified/unverified distinction, so the gate decision lives in the handler. - BLOCKED — positive admission classifier (Task 8): which records count as an - attestation, and the forge-proof discriminator, await owner ratification. - Until then this surfaces ZERO attestations (fail-closed: omit everything). - While the positive-admission classifier is BLOCKED (Task 8), this returns - status='unavailable' (reason: classifier pending ratification), NOT an empty - status='checked' — an empty 'checked' would falsely assert "I checked this SEI - and found no attestation" when the classifier did not actually check (the - silent false-green the honesty surface forbids). Unavailable is safe under the - asymmetric rule: warpline reverifies. + THE FORGE-PROOF DISCRIMINATOR (Task 8, owner-ratified). Two kinds are admitted, + and ONLY when every distinguishing/content field is COVERED BY A SIGNATURE — so + membership in the verified set actually proves the field is authentic: + + * ``operator_override`` — a protected operator-override verdict. Admit only when + ``judge_metadata_signature`` is present on the candidate (the marker that + proves THIS record was in the verified selection — a marker-less injected + record rides through ``_requires_verification`` UNVERIFIED, so keying on the + bare ``judge_verdict`` is forgeable), ``judge_verdict == OVERRIDDEN_BY_OPERATOR`` + (signed at signing_fields["verdict"], protected.py — FORGE-A is closed: + mutating it breaks the signature and fails closed upstream), ``protected_cell + is True`` (signed), the inline ``loomweave.content_hash`` is non-empty + (signed), and entity_key.value == sei AND entity_key.identity_stable + (the SIGNED entity dict — never the unsigned top-level identity_stable dup). + * ``signoff_cleared`` — a SIGNED_OFF record carrying a ``signoff_signature`` + (the verified-selection marker), whose joined PENDING request (by the signed + ``request_seq``) is INTEGRITY-BOUND: recompute ``content_hash`` over the FULL + stored PENDING payload and require it == the signed ``request_payload_hash`` + (FORGE-B: a pointer is not integrity; mutating the PENDING's content_hash + breaks this match). The surfaced content_hash is the PENDING's + ``loomweave.content_hash`` (the SIGNED_OFF carries none of its own), required + non-empty, and the entity is read from the SIGNED entity dict == sei AND + identity_stable. + + OMITTED: chill/coached self-clears, unsigned/procedural sign-offs, BLOCKED + verdicts, empty content_hash, cross-SEI, identity_stable False. WHEN IN DOUBT, + OMIT. The classifier never re-verifies signatures (the pre-gate already did); + it ONLY keys off fields inside signing_fields/signoff_signing_fields and + independently integrity-checks the sign-off join (which crosses a signature + boundary the SIGNED_OFF's own signature covers only via request_payload_hash). + + Returns status='checked' with the admitted attestations (possibly empty — an + empty result here is HONEST: the trail WAS verified, the SEI simply has no + human clearance). The handler owns the status='unavailable' pre-gate for the + no-key / engine-only case (an unverifiable trail must not be read here). """ - records = list(verified_runtime_records) # noqa: F841 - Task 8 classifier will read this - return { - "status": "unavailable", - "sei": sei, - "attestations": [], - "unavailable": [{"reason": "attestation classifier pending owner ratification (Task 8)"}], - } + from legis.canonical import content_hash as _content_hash + + records = list(verified_runtime_records) + attestations: list[dict[str, Any]] = [] + + for rec in records: + payload = rec.payload + ext = payload.get("extensions", {}) or {} + + # Entity must be the SIGNED entity_key dict (value + identity_stable both + # inside signing_fields["entity"]); the top-level payload["identity_stable"] + # is an unsigned duplicate — never read it. + entity = payload.get("entity_key") + if not isinstance(entity, dict): + continue + if entity.get("value") != sei or entity.get("identity_stable") is not True: + continue + + # --- operator_override ------------------------------------------------- + # PRECONDITION 1: the signature marker proves this record verified. + if "judge_metadata_signature" in ext and "signoff_state" not in ext: + if ext.get("judge_verdict") != Verdict.OVERRIDDEN_BY_OPERATOR.value: + continue # BLOCKED / ACCEPTED protected verdicts are not clearances + if ext.get("protected_cell") is not True: + continue + content = (ext.get("loomweave", {}) or {}).get("content_hash") or "" + if not content: + continue + attestations.append( + { + "kind": "operator_override", + "content_hash": content, + "recorded_at": payload.get("recorded_at"), + "seq": rec.seq, + } + ) + continue + + # --- signoff_cleared --------------------------------------------------- + # PRECONDITION 1: signoff_signature marker proves this record verified. + if "signoff_signature" in ext and ext.get("signoff_state") == SignoffState.SIGNED_OFF.value: + request_seq = ext.get("request_seq") + signed_request_hash = ext.get("request_payload_hash") + if request_seq is None or not signed_request_hash: + continue + # FORGE-B: join the PENDING by its seq COLUMN, then recompute the hash + # over the FULL stored PENDING payload and require it == the signed + # request_payload_hash. A pointer alone is not integrity. + pending_payload = None + for cand in records: + cand_ext = cand.payload.get("extensions", {}) or {} + if ( + cand.seq == request_seq + and cand_ext.get("signoff_state") == SignoffState.PENDING.value + ): + pending_payload = cand.payload + break + if pending_payload is None: + continue + if _content_hash(pending_payload) != signed_request_hash: + continue # PENDING content_hash mutated -> hash no longer matches + content = ( + (pending_payload.get("extensions", {}) or {}).get("loomweave", {}) or {} + ).get("content_hash") or "" + if not content: + continue + attestations.append( + { + "kind": "signoff_cleared", + "content_hash": content, + "recorded_at": payload.get("recorded_at"), + "seq": rec.seq, + "signoff_seq": request_seq, + } + ) + + return {"status": "checked", "sei": sei, "attestations": attestations} def _requires_protected_verification(payload: dict[str, Any], protected_policies) -> bool: diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index 11c1d5d..581430a 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -3495,14 +3495,12 @@ def records(self): assert result["structuredContent"]["error_code"] == "AUDIT_INTEGRITY_FAILURE" -def test_attestation_get_wired_deployment_returns_unavailable_not_false_checked(tmp_path): - # KEY-WIRED DEPLOYMENT HONESTY: when BOTH protected_gate AND trail_verifier - # are wired (the deployment that actually has real sign-offs/overrides), the - # pre-gate passes and read_sei_attestations is called. While the classifier - # is BLOCKED (Task 8), this MUST return status='unavailable' with the - # classifier-pending reason — NOT status='checked' with an empty attestations - # list, which would falsely assert "I checked this SEI and found nothing" on - # exactly the deployments that have real governance records. +def test_attestation_get_wired_deployment_empty_trail_is_checked(tmp_path): + # KEY-WIRED DEPLOYMENT (Task 8 shipped): when BOTH protected_gate AND + # trail_verifier are wired, the pre-gate passes and the classifier runs over + # the VERIFIED trail. An empty verified trail honestly yields status='checked' + # with an empty attestations list — the trail WAS checked and the SEI simply + # has no human clearance. (Unavailable is reserved for the no-key pre-gate.) from legis.mcp import call_tool runtime, _store = _runtime(tmp_path) @@ -3520,7 +3518,43 @@ def records(self): result = call_tool(runtime, "attestation_get", {"sei": "mod.fn#1"}) assert not result.get("isError") sc = result["structuredContent"] - assert sc["status"] == "unavailable" + assert sc["status"] == "checked" assert sc["sei"] == "mod.fn#1" assert sc["attestations"] == [] - assert sc["unavailable"] and "pending" in sc["unavailable"][0]["reason"] + + +def test_attestation_get_wired_deployment_admits_genuine_signoff(tmp_path): + # END-TO-END (Task 8): a real signed sign-off through the MCP handler surfaces + # as a signoff_cleared attestation under the queried SEI. + from legis.clock import FixedClock + from legis.enforcement.signoff import SignoffGate + from legis.mcp import call_tool + + runtime, store = _runtime(tmp_path) + key = b"signoff-key" + gate = SignoffGate(store, FixedClock("2026-06-02T12:00:00+00:00"), signer=True, key=key) + req = gate.request( + policy="protected.x", + entity_key=EntityKey(value="loomweave:eid:cleared", identity_stable=True), + rationale="review", + agent_id="agent-1", + extensions={"loomweave": {"content_hash": "ch:e2e"}}, + ) + cleared = gate.sign_off(request_seq=req.seq, operator_id="op-1", rationale="ok") + + runtime.protected_gate = gate + runtime.trail_verifier = TrailVerifier(key, frozenset({"protected.x"})) + + result = call_tool(runtime, "attestation_get", {"sei": "loomweave:eid:cleared"}) + assert not result.get("isError") + sc = result["structuredContent"] + assert sc["status"] == "checked" + assert sc["attestations"] == [ + { + "kind": "signoff_cleared", + "content_hash": "ch:e2e", + "recorded_at": "2026-06-02T12:00:00+00:00", + "seq": cleared.seq, + "signoff_seq": req.seq, + } + ] diff --git a/tests/service/test_governance.py b/tests/service/test_governance.py index edf1055..24c4b69 100644 --- a/tests/service/test_governance.py +++ b/tests/service/test_governance.py @@ -496,20 +496,320 @@ def test_source_binding_status_is_bound_into_the_signature(tmp_path): assert verify(tampered, result.signature, key) is False -# Task 5: read_sei_attestations stub +# Task 8: read_sei_attestations forge-proof classifier # --------------------------------------------------------------------------- -def test_read_sei_attestations_unavailable_while_classifier_blocked(): - # While the positive-admission classifier (Task 8) is BLOCKED, the function - # MUST return status='unavailable' with the classifier-pending reason, NOT - # status='checked' with an empty list — an empty 'checked' would falsely - # assert "I checked this SEI and found no attestation" when the classifier - # did not actually check (the silent false-green the honesty surface forbids). +def test_read_sei_attestations_empty_trail_is_checked_not_unavailable(): + # The function now ALWAYS returns status='checked' — it only ever sees a + # signature-verified trail (the handler owns the unavailable pre-gate). An + # empty verified trail was genuinely checked and honestly has no attestation. from legis.service.governance import read_sei_attestations out = read_sei_attestations([], "mod.fn#1") - assert out["status"] == "unavailable" + assert out["status"] == "checked" assert out["sei"] == "mod.fn#1" assert out["attestations"] == [] - assert out["unavailable"] and "pending" in out["unavailable"][0]["reason"] + assert "unavailable" not in out + + +_ATTEST_SEI = "loomweave:eid:cleared" + + +class _StableJudge: + def evaluate(self, record): + return JudgeOpinion(verdict=Verdict.ACCEPTED, model="judge@1", rationale="ok") + + +def _signed_signoff_gate(tmp_path): + from legis.clock import FixedClock + from legis.enforcement.signoff import SignoffGate + + store = AuditStore(f"sqlite:///{tmp_path}/gov.db") + gate = SignoffGate( + store, FixedClock("2026-06-02T12:00:00+00:00"), signer=True, key=b"signoff-key" + ) + return store, gate + + +def _stable_entity_key(sei=_ATTEST_SEI): + return EntityKey(value=sei, identity_stable=True) + + +def _request_and_clear(gate, *, sei=_ATTEST_SEI, content_hash_value="ch:request-1"): + req = gate.request( + policy="protected.x", + entity_key=_stable_entity_key(sei), + rationale="please review", + agent_id="agent-1", + extensions={"loomweave": {"content_hash": content_hash_value}}, + ) + cleared = gate.sign_off( + request_seq=req.seq, operator_id="operator-1", rationale="approved" + ) + return req, cleared + + +def test_read_sei_attestations_admits_genuine_signed_signoff(tmp_path): + # POSITIVE / FORGE-B SOUNDNESS PROOF: a real signed SignoffGate request+sign_off. + # If content_hash(stored PENDING) does NOT equal the signed request_payload_hash + # this admission would not happen — the test passing IS the soundness proof. + from legis.service.governance import read_sei_attestations + + store, gate = _signed_signoff_gate(tmp_path) + req, cleared = _request_and_clear(gate, content_hash_value="ch:signoff-content") + + out = read_sei_attestations(store.read_all(), _ATTEST_SEI) + assert out["status"] == "checked" + assert out["attestations"] == [ + { + "kind": "signoff_cleared", + "content_hash": "ch:signoff-content", + "recorded_at": "2026-06-02T12:00:00+00:00", + "seq": cleared.seq, + "signoff_seq": req.seq, + } + ] + + +def test_read_sei_attestations_admits_genuine_operator_override(tmp_path): + # POSITIVE: a real signed protected operator override surfaces as + # operator_override with the inline (signed) content_hash. + from legis.clock import FixedClock + from legis.service.governance import read_sei_attestations + + store = AuditStore(f"sqlite:///{tmp_path}/gov.db") + gate = ProtectedGate( + store, FixedClock("2026-06-02T12:00:00+00:00"), judge=_StableJudge(), key=b"k" + ) + result = gate.operator_override( + policy="protected.x", + entity_key=_stable_entity_key(), + rationale="operator clears", + operator_id="operator-1", + file_fingerprint="sha256:ff", + ast_path="ap", + extensions={"loomweave": {"content_hash": "ch:override-content"}}, + ) + + out = read_sei_attestations(store.read_all(), _ATTEST_SEI) + assert out["status"] == "checked" + assert out["attestations"] == [ + { + "kind": "operator_override", + "content_hash": "ch:override-content", + "recorded_at": "2026-06-02T12:00:00+00:00", + "seq": result.seq, + } + ] + + +def test_read_sei_attestations_forge_b_mutated_pending_content_hash_omitted(tmp_path): + # FORGE-B: real signed SIGNED_OFF; mutate the joined PENDING's content_hash in + # the materialized list. The signed request_payload_hash no longer matches the + # recomputed content_hash(pending) -> the sign-off MUST be omitted (the + # mutated hash is NEVER surfaced). The SIGNED_OFF record is unchanged and still + # "verifies"; only the classifier's recompute-and-compare catches the forge. + from legis.service.governance import read_sei_attestations + + store, gate = _signed_signoff_gate(tmp_path) + req, _cleared = _request_and_clear(gate, content_hash_value="ch:original") + + records = store.read_all() + for rec in records: + if rec.seq == req.seq: + rec.payload["extensions"]["loomweave"]["content_hash"] = "ch:FORGED" + + out = read_sei_attestations(records, _ATTEST_SEI) + assert out["status"] == "checked" + assert out["attestations"] == [] + + +def test_read_sei_attestations_forge_a_mutated_verdict_breaks_signature(tmp_path): + # FORGE-A coverage proof: a real signed BLOCKED protected record whose + # judge_verdict is mutated to OVERRIDDEN_BY_OPERATOR. Because judge_verdict is + # SIGNED (signing_fields["verdict"]), the mutation breaks the v3 signature, so + # TrailVerifier.verify RAISES before the classifier ever runs — the forged + # override never reaches read_sei_attestations. + from legis.clock import FixedClock + from legis.enforcement.protected import TrailVerifier + from legis.enforcement.verdict import JudgeOpinion + + class _BlockingJudge: + def evaluate(self, record): + return JudgeOpinion(verdict=Verdict.BLOCKED, model="judge@1", rationale="no") + + store = AuditStore(f"sqlite:///{tmp_path}/gov.db") + key = b"k" + gate = ProtectedGate( + store, FixedClock("2026-06-02T12:00:00+00:00"), judge=_BlockingJudge(), key=key + ) + gate.submit( + policy="protected.x", + entity_key=_stable_entity_key(), + rationale="x", + agent_id="agent-1", + file_fingerprint="sha256:ff", + ast_path="ap", + extensions={"loomweave": {"content_hash": "ch:blocked"}}, + ) + + records = store.read_all() + assert records[0].payload["extensions"]["judge_verdict"] == "BLOCKED" + records[0].payload["extensions"]["judge_verdict"] = "OVERRIDDEN_BY_OPERATOR" + + verifier = TrailVerifier(key, frozenset({"protected.x"})) + with pytest.raises(TamperError): + verifier.verify(records) + + +def test_read_sei_attestations_chill_self_clear_stuffing_override_omitted(tmp_path): + # A chill/coached self-clear that STUFFS operator-override extensions. With NO + # signature marker it must be omitted (keying on bare judge_verdict would admit + # an unsigned forgery). Even WITH a non-verifying judge_metadata_signature it is + # omitted unless protected_cell + content_hash are present and signed — but the + # marker-present branch is what the verified pre-gate would have rejected; here + # we assert the no-marker stuffing is omitted. + from legis.service.governance import read_sei_attestations + + class _Rec: + def __init__(self, seq, payload): + self.seq = seq + self.payload = payload + + stuffed = _Rec( + 1, + { + "policy": "ordinary", + "entity_key": {"value": _ATTEST_SEI, "identity_stable": True}, + "recorded_at": "2026-06-02T12:00:00+00:00", + "extensions": { + # operator-override value but NO judge_metadata_signature marker + "judge_verdict": "OVERRIDDEN_BY_OPERATOR", + "protected_cell": True, + "loomweave": {"content_hash": "ch:stuffed"}, + }, + }, + ) + + out = read_sei_attestations([stuffed], _ATTEST_SEI) + assert out["attestations"] == [] + + +def test_read_sei_attestations_unsigned_procedural_signoff_omitted(tmp_path): + # A structured (procedural) sign-off built with no signer -> SIGNED_OFF carries + # no signoff_signature marker -> omitted. + from legis.clock import FixedClock + from legis.enforcement.signoff import SignoffGate + from legis.service.governance import read_sei_attestations + + store = AuditStore(f"sqlite:///{tmp_path}/gov.db") + gate = SignoffGate(store, FixedClock("2026-06-02T12:00:00+00:00")) # no signer + _request_and_clear(gate) + + out = read_sei_attestations(store.read_all(), _ATTEST_SEI) + assert out["status"] == "checked" + assert out["attestations"] == [] + + +def test_read_sei_attestations_cross_sei_not_surfaced(tmp_path): + # A genuine signed sign-off for a DIFFERENT SEI must not surface under the query. + from legis.service.governance import read_sei_attestations + + store, gate = _signed_signoff_gate(tmp_path) + _request_and_clear(gate, sei="loomweave:eid:other") + + out = read_sei_attestations(store.read_all(), _ATTEST_SEI) + assert out["attestations"] == [] + + +def test_read_sei_attestations_identity_unstable_omitted(tmp_path): + # identity_stable False in the SIGNED entity dict -> omitted. + from legis.service.governance import read_sei_attestations + + class _Rec: + def __init__(self, seq, payload): + self.seq = seq + self.payload = payload + + unstable = _Rec( + 1, + { + "policy": "protected.x", + "entity_key": {"value": _ATTEST_SEI, "identity_stable": False}, + "recorded_at": "2026-06-02T12:00:00+00:00", + "extensions": { + "judge_metadata_signature": "hmac-sha256:v3:deadbeef", + "judge_verdict": "OVERRIDDEN_BY_OPERATOR", + "protected_cell": True, + "loomweave": {"content_hash": "ch:x"}, + }, + }, + ) + + out = read_sei_attestations([unstable], _ATTEST_SEI) + assert out["attestations"] == [] + + +def test_read_sei_attestations_empty_content_hash_omitted(tmp_path): + # Empty inline content_hash (operator_override) and empty joined content_hash + # (signoff) both omit — never surface content_hash == "". + from legis.service.governance import read_sei_attestations + + # operator_override inline-empty + class _Rec: + def __init__(self, seq, payload): + self.seq = seq + self.payload = payload + + override_empty = _Rec( + 1, + { + "policy": "protected.x", + "entity_key": {"value": _ATTEST_SEI, "identity_stable": True}, + "recorded_at": "2026-06-02T12:00:00+00:00", + "extensions": { + "judge_metadata_signature": "hmac-sha256:v3:deadbeef", + "judge_verdict": "OVERRIDDEN_BY_OPERATOR", + "protected_cell": True, + "loomweave": {"content_hash": ""}, + }, + }, + ) + out = read_sei_attestations([override_empty], _ATTEST_SEI) + assert out["attestations"] == [] + + # signoff join-empty: real signed sign-off whose PENDING has empty content_hash + store, gate = _signed_signoff_gate(tmp_path) + _request_and_clear(gate, content_hash_value="") + out2 = read_sei_attestations(store.read_all(), _ATTEST_SEI) + assert out2["attestations"] == [] + + +def test_read_sei_attestations_blocked_verdict_omitted(tmp_path): + # A genuine signed BLOCKED protected verdict is not a clearance -> omitted. + from legis.clock import FixedClock + from legis.enforcement.verdict import JudgeOpinion + from legis.service.governance import read_sei_attestations + + class _BlockingJudge: + def evaluate(self, record): + return JudgeOpinion(verdict=Verdict.BLOCKED, model="judge@1", rationale="no") + + store = AuditStore(f"sqlite:///{tmp_path}/gov.db") + gate = ProtectedGate( + store, FixedClock("2026-06-02T12:00:00+00:00"), judge=_BlockingJudge(), key=b"k" + ) + gate.submit( + policy="protected.x", + entity_key=_stable_entity_key(), + rationale="x", + agent_id="agent-1", + file_fingerprint="sha256:ff", + ast_path="ap", + extensions={"loomweave": {"content_hash": "ch:blocked"}}, + ) + + out = read_sei_attestations(store.read_all(), _ATTEST_SEI) + assert out["status"] == "checked" + assert out["attestations"] == [] From 1e21418a590bf85613b07b66b86ed9528e72d6c9 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:22:03 +1000 Subject: [PATCH 14/16] test(governance): cover chill-stuffing-with-non-verifying-signature (Task 8 forge-negative) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The asymmetric twin of the no-marker stuffing case: a self-clear stuffing operator-override extensions WITH a garbage judge_metadata_signature marker. The classifier trusts marker presence by design, so the defense is the pre-gate — TrailVerifier selects the marker-bearing record and raises TamperError before the classifier runs (same principle as FORGE-A). Closes the task-enumerated "with and without a non-verifying signature" coverage requirement. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/service/test_governance.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/service/test_governance.py b/tests/service/test_governance.py index 24c4b69..155917a 100644 --- a/tests/service/test_governance.py +++ b/tests/service/test_governance.py @@ -696,6 +696,44 @@ def __init__(self, seq, payload): assert out["attestations"] == [] +def test_chill_stuffing_with_non_verifying_signature_caught_by_pre_gate(tmp_path): + # The asymmetric twin of the no-marker case: a chill/coached self-clear that + # STUFFS operator-override extensions AND a garbage judge_metadata_signature + # marker. The classifier trusts marker presence BY DESIGN (marker == "this was + # in the verified selection"), so a direct classifier call would admit it. The + # defense is the pre-gate: a record carrying the marker IS selected by + # _requires_verification, and its signature does NOT verify -> TrailVerifier + # raises TamperError (-> AUDIT_INTEGRITY_FAILURE) BEFORE the classifier runs. + # Same principle as FORGE-A: marker present + signature invalid -> fail-closed. + from legis.enforcement.protected import TrailVerifier + + class _Rec: + def __init__(self, seq, payload): + self.seq = seq + self.payload = payload + + stuffed_signed = _Rec( + 1, + { + "policy": "ordinary", # non-protected policy + "entity_key": {"value": _ATTEST_SEI, "identity_stable": True}, + "recorded_at": "2026-06-02T12:00:00+00:00", + "rationale": "self-clear", + "agent_id": "agent-1", + "extensions": { + "judge_metadata_signature": "hmac-sha256:v3:garbage", # non-verifying + "judge_verdict": "OVERRIDDEN_BY_OPERATOR", + "protected_cell": True, + "loomweave": {"content_hash": "ch:stuffed"}, + }, + }, + ) + + verifier = TrailVerifier(b"k", frozenset()) + with pytest.raises(TamperError): + verifier.verify([stuffed_signed]) + + def test_read_sei_attestations_unsigned_procedural_signoff_omitted(tmp_path): # A structured (procedural) sign-off built with no signer -> SIGNED_OFF carries # no signoff_signature marker -> omitted. From fb33a9e0624b5f072ba9ada2d0f3d170114a5398 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:33:50 +1000 Subject: [PATCH 15/16] product: ratify + implement Task 8 forge-proof attestation classifier; PDR-0004 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Warpline interfaces now complete (Tasks 1-8). Records the owner ratification + forge-proof classifier implementation (both kinds, 0 forges admitted), the spec §4 correction, and the held-on-merge state. Workspace on warpline-interfaces; lands on main when the branch merges. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/product/current-state.md | 24 ++++++++---------- ...ment-forge-proof-attestation-classifier.md | 25 +++++++++++++++++++ docs/product/metrics.md | 3 ++- 3 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 docs/product/decisions/0004-ratify-implement-forge-proof-attestation-classifier.md diff --git a/docs/product/current-state.md b/docs/product/current-state.md index a046f4d..869630a 100644 --- a/docs/product/current-state.md +++ b/docs/product/current-state.md @@ -1,24 +1,22 @@ -# Current State — Legis Checkpoint: 2026-06-25 · committed (PDR-0002, PDR-0003) +# Current State — Legis Checkpoint: 2026-06-25 (2nd) · committed (PDR-0004) ## The bet right now -**Keep the governance-honesty surface true post-gold** (north-star: open governance-honesty defects → 0) — close the three confirmed P2 findings — while the **Warpline federation seam**, now BUILT, awaits owner sign-off to merge and to unblock its attestation classifier. +**Keep the governance-honesty surface true post-gold** (north-star: open governance-honesty defects → 0, currently **3**) — close the three confirmed P2 findings. The **Warpline federation seam is COMPLETE** (Tasks 1–8 + spec fix), held on merge by the owner until warpline's own body of work lands. ## In flight -- **Warpline interfaces** (legis-1734128d34) — **BUILT** on branch `warpline-interfaces` (8 commits, `5a30cd8..11ab7f8`): advisory preflight consumer (`warpline_preflight_get`) + `attestation_get` fail-closed scaffolding + the byte-identical advisory-boundary spine. All CI-equivalent gates green (pytest 1225, mypy clean, coverage 92.14%, ruff). **Gated on owner** (PDR-0002): merge (federation contract) + Task-8 classifier ratification. The classifier is BLOCKED and ships honest `unavailable` (no false-green). -- **Governance-honesty P2 findings** — confirmed, ready, unclaimed: unverified posture tail (legis-476ab6f125); protected batch without HeadAnchor advance (legis-0c310712a7); policy_boundary_check accepts roots outside source root (legis-0186c23a2c). The north-star Now bet; untouched this session. +- **Warpline interfaces** (legis-1734128d34) — **COMPLETE** on branch `warpline-interfaces` (`5a30cd8..1e21418`): advisory preflight consumer + `attestation_get` with the **forge-proof per-SEI attestation classifier** (Task 8, both kinds shipped) + byte-identical advisory-boundary spine. All CI gates green (pytest 1237, mypy clean, coverage 92.13%, ruff). **Held on merge** (owner: warpline mid-work, will push when done). Adversarial forge phase: zero forges admitted. +- **Governance-honesty P2 findings** — confirmed, ready, unclaimed: unverified posture tail (legis-476ab6f125); protected batch without HeadAnchor advance (legis-0c310712a7); policy_boundary_check accepts roots outside source root (legis-0186c23a2c). The north-star Now bet; untouched. ## Open questions / blocked-on-owner -- **Task 8 ratification (4 questions)** — to unblock `attestation_get`'s classifier: (1) operator-override = a *verifying* `judge_metadata_signature`, never the bare field; (2) only *signed* sign-offs attest; (3) no-key deployments can't attest → `unavailable`; (4) absent `content_hash` → omit, never `""`. -- **Merge / publish** `warpline-interfaces` — binds a sibling → owner sign-off. `warpline_preflight_get` is independently valuable; merge-now-then-ratify and hold-the-branch are both clean. -- **Spec §4.1 correction** — design spec lines 92/102 assert a fail-closed guarantee that's false in the no-key deployment; code is correct, prose is stale. Recommend correcting (escalated, not done). -- **Warpline wire format** — §6 inferred (TO-CONFIRM); ships shape-validating, degrades to `unavailable`. Gates real integration, not unit work. +- **Merge / publish** `warpline-interfaces` — held by owner (warpline's body of work mid-flight; they push when done). This is the only remaining gate; nothing else blocks. +- **Warpline wire format** (§6) — inferred/TO-CONFIRM; ships shape-validating, degrades to `unavailable`. Gates real integration (confirmed when warpline lands), not unit work. - **Inferred vision/metrics** — PDR-0001's reversal trigger fires on the owner's first review (the `(set)` time-to-close TARGET still needs an owner number). +- *(Resolved this session: Task 8 ratification → done (PDR-0004); spec §4.1 correction → done.)* ## Last checkpoint did -- Dispatched → built → **accepted** the warpline bet Tasks 1–7 via subagent-driven TDD from a grounded, adversarially-reviewed plan; all CI-equivalent gates green (PDR-0002). -- Recorded the **federation-read doctrine** — facts-not-verdict, advisory context structurally isolated (PDR-0003). -- Caught + fixed two review-found defects pre-merge: a stale structural guard, and a **false-green** in the BLOCKED attestation stub (`checked: []` → honest `unavailable`). -- Deferred Task 8 (classifier) as BLOCKED; flagged merge + Task-8 + spec-correction for the owner. +- **Ratified + implemented Task 8** — the forge-proof attestation classifier (both `operator_override` + `signoff_cleared`), via a ground→implement→adversarial-forge workflow; zero forges admitted, both positives admit (PDR-0004). +- Corrected design spec §4.1/§4.2/§7 (false unconditional fail-closed claim → conditional; confirmed discriminator). +- Independently re-ran the full CI gate (pytest 1237, mypy clean, coverage 92.13%, ruff) — green. ## Next session, start here -Either the owner's answers to the warpline escalations (Task-8 ratification → unblock + implement the classifier; or merge sign-off), **or** pick up the north-star Now bet — the three P2 governance-honesty findings (legis-476ab6f125, -0c310712a7, -0186c23a2c), still unclaimed. +Either the owner's merge sign-off when warpline's work lands (the branch is complete and green), **or** pick up the north-star Now bet — the three P2 governance-honesty findings (legis-476ab6f125, -0c310712a7, -0186c23a2c), still unclaimed. diff --git a/docs/product/decisions/0004-ratify-implement-forge-proof-attestation-classifier.md b/docs/product/decisions/0004-ratify-implement-forge-proof-attestation-classifier.md new file mode 100644 index 0000000..224db68 --- /dev/null +++ b/docs/product/decisions/0004-ratify-implement-forge-proof-attestation-classifier.md @@ -0,0 +1,25 @@ +# PDR-0004 — Ratify + implement the forge-proof attestation classifier (Task 8); correct spec §4 + +Date: 2026-06-25 Status: accepted (owner ratified the discriminator 2026-06-25; merge still owner-gated) Author: claude (opus, product-owner) +Unblocks the Task-8 deferral in [[0002-accept-warpline-bet-defer-classifier]] Related: [[0003-federation-read-doctrine]]; tracker legis-1734128d34; commits da85e16, 1e21418 + +## Context +PDR-0002 shipped `attestation_get` fail-closed and **deferred** its positive-admission classifier, because the obvious operator-override discriminator is forgeable. The owner **ratified** the four classifier resolutions (2026-06-25, "Done"): (a) operator-override = a *verifying* signature, never the bare field; (b) only *signed* sign-offs attest; (c) no-key deployments → `unavailable`; (d) absent `content_hash` → omit. + +## Options considered +1. Key off the (signed) verdict value, trusting that the record is in the verified set. +2. Key off the **signature MARKER presence** (`judge_metadata_signature` / `signoff_signature`) — a strict subset of `_requires_verification`, so "admitted ⟹ verified" holds — integrity-bind the sign-off `content_hash` join via the signed `request_payload_hash`, and read only the signed `entity_key` dict. +3. Keep it blocked (ship no classifier). + +## The call +Option 2. Grounding confirmed the load-bearing facts against the real code: `judge_verdict`, `protected_cell`, and the inline `content_hash` are inside `signing_fields` (**FORGE-A closed**); the signed `SIGNED_OFF` carries a signed `request_payload_hash` (**FORGE-B closed**). Both kinds (`operator_override`, `signoff_cleared`) shipped forge-proof. The *necessary-but-not-sufficient* trap (membership in the verified set ≠ the field is signed) is closed by gating admission on the signature marker (a subset of the verification predicate) and keying only on signed fields; the sign-off join recomputes the PENDING hash and compares it to the signed `request_payload_hash` rather than trusting the `request_seq` pointer. + +Verified by an **adversarial forge phase** (4 lenses, live-run probes): **zero forges admitted**, both genuine positives admit; full CI gate green (pytest 1237, mypy clean, coverage 92.13%, ruff). Spec §4.1/§4.2/§7 corrected (the false *unconditional* fail-closed claim → conditional on a signature-verifiable trail; lowercase `signed_off` → `SIGNED_OFF`; hand-wave → the confirmed discriminator). + +## Rationale +The forge-proof property is **structural**: admission requires the signature marker → the record was cryptographically verified → the keyed fields are authentic. A mutated unsigned field either breaks the signature (→ `AUDIT_INTEGRITY_FAILURE`) or isn't a field the classifier keys off. The sign-off join is integrity-checked, not pointer-trusted. This is the strongest "governed-good" signal warpline can safely skip reverification on, and it keeps the asymmetric rule (any ambiguity → omit). + +## Reversal trigger +- If any adversarial review or production incident finds a forged / non-human-cleared record **admitted** → reopen immediately (Critical; the asymmetric rule is breached). +- If the protected / sign-off **signing surface** changes (a keyed field moves out of `signing_fields` / `signoff_signing_fields`) → re-audit the discriminator against the new signed-field set. +- Broadening the admitted set beyond the two human-cleared kinds is additive and requires the same signing-coverage proof per new kind. diff --git a/docs/product/metrics.md b/docs/product/metrics.md index 4052db7..ae9cca2 100644 --- a/docs/product/metrics.md +++ b/docs/product/metrics.md @@ -21,6 +21,7 @@ | Metric | Floor / ceiling | Current | Read on | |--------|-----------------|---------|---------| | **Advisory-boundary invariant** — governance verdicts byte-identical with a sibling (Warpline) absent vs present | must hold (binary) | **holds — proven** by `tests/mcp/test_warpline_advisory_boundary.py` (byte-identical on real verdicts) + a derived structural guard over all tool handlers; warpline consumed only in its sibling tool | 2026-06-25 | -| CI green (tests + mypy) | 100% | `warpline-interfaces` branch: pytest 1225 passed, mypy clean (78 files) — CI-equivalent gate run locally; `main` green at 1.1.1 | 2026-06-25 | +| CI green (tests + mypy) | 100% | `warpline-interfaces` branch (Tasks 1–8 complete): pytest 1237 passed, mypy clean (78 files), coverage 92.13% — CI-equivalent gate run locally; `main` green at 1.1.1 | 2026-06-25 | +| **Attestation classifier forge-resistance** — `attestation_get` admits no forged / non-human-cleared record | must hold (binary) | **holds** — adversarial forge phase (4 lenses, live-run probes) admitted **0** forges; admission gates on the signature marker + keys only on signed fields + integrity-bound sign-off join (PDR-0004) | 2026-06-25 | | Release publish gated on **live Loomweave SEI conformance** | must pass before any PyPI publish | gate in place (PR #8 line) | 2026-06-24 | | Test coverage vs configured floors (`scripts/check_coverage_floors.py`) | ≥ floors | **92.14% total** (floor 88); all per-package floors hold (mcp.py 92.4%, service/ 95.0%) — read on `warpline-interfaces` | 2026-06-25 | From 298eded1bab34238181c54f0ad9675ab7da1cee1 Mon Sep 17 00:00:00 2001 From: John Morrissey <544926+tachyon-beep@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:30:14 +1000 Subject: [PATCH 16/16] chore(release): prepare 1.2.0 Warpline federation interfaces release: advisory preflight consumer (warpline_preflight_get) and forge-proof per-SEI attestation read (attestation_get). Agent MCP surface 22 -> 24 tools. Bump version (pyproject + __init__ + lockfile) and add the 1.2.0 CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/legis/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bbdea..a0de0f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,36 @@ versions per [PEP 440](https://peps.python.org/pep-0440/) / _Post-1.0.0 work lands here; legis versions independently from the Weft 1.0 launch on._ +## [1.2.0] — 2026-06-25 + +Warpline federation interfaces: an advisory preflight consumer and a +forge-proof per-SEI attestation read. The agent MCP surface grows from 22 to +24 tools. + +### Added + +- **Advisory Warpline preflight consumer (`warpline_preflight_get`).** A + stdlib-only HTTP client (`HttpWarplineClient`) and a `read_warpline_preflight` + service read surface Warpline's impact-radius and reverify-worklist hints. The + consumer is purely advisory and structurally isolated from every governance + verdict path — an acceptance test asserts governance output is byte-identical + with and without advisory data present, and transport or configuration + failures degrade to a discriminated `unavailable` rather than affecting a + verdict or raising. The client reuses Legis's SSRF/redirect/size-cap gating + (loopback or HTTPS only unless `LEGIS_ALLOW_INSECURE_REMOTE_HTTP=1`), with a + clone-parity guard that fails if those primitives drift from the Filigree + client. +- **Forge-proof per-SEI attestation read (`attestation_get`).** A + `read_sei_attestations` classifier surfaces the operator-override and + cleared-signoff attestations bound to a given SEI. Admission is gated on + cryptographic signature markers drawn from the verified-trail selection: a + sign-off's joined PENDING record is integrity-bound by recomputing and + comparing its content hash against the signed request-payload hash, every + surfaced field comes from the signed set, and any ambiguity resolves to + omission (a false "attested" is a security hole; a missed attestation only + costs wasted reverify work). The adversarial forge phase admitted zero forged + records. + ## [1.1.1] — 2026-06-23 Security and release-readiness hardening after the 1.1.0 dogfood release. diff --git a/pyproject.toml b/pyproject.toml index 4fdf684..de807cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "legis" -version = "1.1.1" +version = "1.2.0" description = "Legis — the git/CI + governance layer of the Weft suite" readme = "README.md" license = "MIT" diff --git a/src/legis/__init__.py b/src/legis/__init__.py index 759daa4..ef0fdfa 100644 --- a/src/legis/__init__.py +++ b/src/legis/__init__.py @@ -1,3 +1,3 @@ """Legis — the git/CI + governance layer of the Weft suite.""" -__version__ = "1.1.1" +__version__ = "1.2.0" diff --git a/uv.lock b/uv.lock index 367cd58..7604c90 100644 --- a/uv.lock +++ b/uv.lock @@ -498,7 +498,7 @@ wheels = [ [[package]] name = "legis" -version = "1.1.1" +version = "1.2.0" source = { editable = "." } dependencies = [ { name = "cryptography" },