Release 1.0.0 — rc5 into main#10
Conversation
…oc; C-8 preserved Dogfood-#2 governance honesty (convention C-10), branch-local — merge/release gated on the filigree-first propagation. Capability confinement (proposed C-8) preserved throughout: operator signing keys stay out of agent reach, nothing is auto-provisioned/relocated, no MCP tool enables a cell or self-grants authority. N3 (weft-df8d2ef454, C-10(c)) — legis no longer ships dark and quiet: - mcp.py _recovery_for: INVALID_CELL_SPEC names LEGIS_WARDLINE_CELL / LEGIS_WARDLINE_CELL_BY_SEVERITY (covers all WardlineRoutingError kinds, incl. those str(exc) misses); CELL_NOT_ENABLED split into the keyless simple tier (policy/cells.toml / LEGIS_POLICY_CELLS / LEGIS_DEV_DEFAULT_CELLS) and the complex tier (LEGIS_HMAC_KEY, operator out-of-band + relaunch). Subsumes Le1. - doctor.py: two report-only checks (check_policy_cells, check_wardline_routing) naming the enablement path when unwired — presence-only, no repair param, write nothing, never render a key value. Fail-closed preserved (no auto-open). N4 (weft-a7a92a40dd, C-10(d)) — honest dirty-tree skip: - WardlineDirtyTreeError.to_payload() is the single source both transports (mcp.py scan_route + api/app.py) serialize: structured reason/posture/cause/ remediation, routed==[] (governs nothing). No scan_route call argument added; the LEGIS_WARDLINE_ALLOW_DIRTY dirty-snapshot opt-in stays an env-only operator switch. C3 (weft-f506e5f845) — charter now documents that legis's OWN audit records carry a self-asserted agent_id/operator_id (launch-bound + HMAC-tamper-evident, not authenticated); verified_author:null maps to those fields. Guards: test_c8_no_agent_reachable_enablement_or_signing_surface (no enable/sign tool; scan_route schema locked) + doctor checks write-nothing/render-no-key test. 762 passed; ruff + mypy clean; coverage 92.30%; per-package floors hold; policy-boundary-check PASS; SEI oracle PASS. Designed + adversarially red-teamed (C-8 verdict: safe) and implementation-reviewed via multi-agent workflows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ntime Acceptance branch 1 of N3 (weft-df8d2ef454) — "a fresh stdio launch CAN reach a configured non-secret surface" — was only proven via injected-engine unit tests; the CHANGELOG and ticket comments assert "chill/coached reachable keyless" as fact. Add a test that exercises the REAL launch path: build_runtime() with no LEGIS_HMAC_KEY + the LEGIS_DEV_DEFAULT_CELLS=1 chill posture, then override_submit -> ACCEPTED_SELF via the lazy keyless _engine. A future change making _engine require a key now fails here instead of silently falsifying the promise. (Scan-route axis already pinned by test_scan_route_uses_server_owned_cell.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…48eb2) Wardline renamed the per-finding output key `suppressed` -> `suppression_state` across all surfaces incl. the SIGNED legis scan artifact, changing the canonical signed bytes and breaking the Wardline->legis hop (wardline's opt-in legis_e2e oracle red by design). legis adopts the new key. - ingest: WardlineFinding.from_wire reads `suppression_state`; the dataclass field, error message, and active_defects branches follow. Values unchanged (active/waived/suppressed/baselined/judged); the `Suppressed` enum (value vocabulary) and SUPPRESSION_PROOF_KEYS are untouched. - clean break: a finding carrying only the legacy `suppressed` key reads as `active` and OVER-gates — fail-safe (never silently drops a real defect), pinned by test_legacy_suppressed_key_is_ignored_clean_break. - NO signing/canonical change: legis's signer already reproduces Wardline's rekeyed golden byte-for-byte. Added the legis-side cross-impl golden MIRROR legis was missing: sign(_GOLDEN_FIELDS, _GOLDEN_KEY) == hmac-sha256:v2:2b2cf09… over `suppression_state`, so the hop self-verifies on both ends. - intake fixtures: ~40 `suppressed` test fixtures across tests/wardline, tests/api, tests/mcp, tests/store renamed to `suppression_state` (a sweep flagged these to avoid vacuously-green suppression-path assertions). Acceptance: legis 767 tests green; golden byte-agreement pinned; the live signed hop verifies — wardline's `-m legis_e2e` test_legis_accepts_signed_artifact PASSES against the reinstalled legis (real build_legis_artifact -> signed suppression_state artifact -> legis verifies + routes). Branch-only; ship via the filigree-gated rc4->main merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… resolution check_policy_cells claimed to "mirror mcp._load_policy_cell_registry" but the root fallback differs: the resolver uses os.getcwd() when LEGIS_SOURCE_ROOT is unset, while doctor uses its passed-in root. The env precedence is faithfully mirrored; the root resolution is a deliberate difference (they coincide when doctor runs from the server's launch CWD). Tighten the docstring to say so. Docstring-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oute root (opp #6) scan_route returned `{outcome: ROUTED, routed:[...]}` with no top-level posture field, so an agent relaying "governance passed" could not tell a keyless dev-grade pass (unverified/dirty) from a CI-signed `verified` pass — the posture was only buried in each routed record's provenance, and absent entirely when nothing routed. Same vacuous-green fidelity gap as wardline W2. - `route_wardline_scan` now returns `RoutedScan(routed, artifact_status)` instead of a bare list, surfacing the scan-level `artifact_status` that `verify_wardline_artifact` already computes - both surfaces echo it at the response root: the MCP `scan_route` tool and the HTTP `/scan-route` adapter (identical contract) - new MCP test asserts a keyless unsigned scan echoes `artifact_status: "unverified"` at the top level; the exact-shape routing test gains the field Closes gap-analysis opp #6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…filigree-scope check (N1) Close two release-1.0 risk-audit gaps: POLICY-1 — a pinned, running evidence test could be disabled after the fact with @pytest.mark.skip / skipif / xfail. The fingerprint is blind to decorators (Q-L5 parity), so the drift check is byte-identical and cannot see the disablement. Add a highest-priority disabled-evidence judgement in the shared evaluate_test_evidence so both the runtime gate and the static boundary scanner reject it identically (new POLICY_BOUNDARY_TEST_DISABLED). Marker match is terminal-name based, so it catches the import-alias form (`from pytest import mark; @mark.skip`) whose only tell lives outside the function source the fingerprint sees. N1 — add report-only check_filigree_binding_scope to doctor: an unscoped federation-write binding in .mcp.json (/api/weft/… etc.) is fail-closed with HTTP 400 by a filigree server-mode daemon, so scans silently non-emit. Warn (not error — harmless against single-project/stdio) and name the offending URL + the scoped form to use.
/governance/lineage-integrity computed status as "unverified" if unavailable else "verified", ignoring integrity.divergences. A confirmed external tamper (divergence list populated) reported status="verified" — a false green at the top-level posture while the same payload carried the divergence. Three-way precedence: any divergence -> "diverged" (most severe, confirmed tamper) over "unverified" (can't check) over "verified". The existing divergence test pinned the divergences list but pointedly omitted the status assertion; pin status="diverged" so the false green cannot regress. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… anchor (AUD-1) An attacker with DB-file write access could delete an audit record and re-chain the survivors undetectably: the hash chain is plain SHA (keyless, recomputable) and the HMAC bound record *content* but never its chain *position*, so every surviving signature still verified and the chain stayed internally consistent. service/governance.py already documented that whole- trail verify catches mutation but not deletion. Two complementary, isolated mechanisms now close it: * seq-binding (v3) + contiguity — interior delete and reorder. verify_integrity gains an expected-seq counter (a re-chained gap is now a tamper), and protected + sign-off verdicts sign at v3, folding the chain seq into the HMAC. A renumber-to-hide-a-deletion then fails to verify at the new position. seq is taken from the column at verify time, never a payload field. Resolved the sign-before-seq ordering with a store-mediated append_signed: the store reserves seq + prev_hash under its BEGIN IMMEDIATE lock and hands them to a signer callback, so the bound seq is provably the row's seq with no race. The store stays key-agnostic (the callback closes over the gate's key). * HeadAnchor (opt-in) — tail-truncation, the one thing seq-binding structurally cannot catch (a truncated head is legitimately last). A small HMAC-signed sidecar remembers the last (seq, chain_hash); a missing anchor on an anchored store fails closed. Wired as optional gate/verifier params, off by default — conceded-capability hardening that does not touch the 1.0 core. The shared sign()/verify() primitive keeps its v2 default, so the cross-tool Wardline artifact contract and the binding ledger are byte-for-byte untouched. Binding ledger stays v2 (separate, homogeneous store) but is covered by the new contiguity check; renumber-within that store is a documented residual, as is the inherent renumber-vulnerability of an all-unsigned (chill/coached) run. Tests: three attack PoCs, each isolating one mechanism (interior-delete-gap → contiguity; delete-and-renumber → v3 seq-HMAC; tail-truncate → anchor), plus HeadAnchor unit coverage (forged/missing/reappend/no-op) and a v3 signing pin. Full suite 793 passed, 2 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ail-loss (AUD-3) The audit store ran synchronous=NORMAL under WAL. NORMAL only fsyncs the WAL at a checkpoint, so a committed-but-not-yet-checkpointed append is lost on a power-cut while the database stays consistent. The survivors form a contiguous, fully-signed hash chain — a valid-looking SHORTENED trail indistinguishable from "nothing more was ever written". For an audit-integrity store that silent tail-loss is precisely the harm. Set synchronous=FULL: each commit is fsynced, so a committed governance record survives power loss; throughput is the correct thing to trade here. The floor is intentionally not configurable — an audit store's durability must not be lowerable back to the bug. SQLite's default wal_autocheckpoint still bounds WAL growth, so no separate checkpoint lifecycle is needed. This is the prevention half of the shortened-trail problem; AUD-1's out-of-band head anchor is the detection half (it flags a trail that shrank below its recorded head, whether by malice or by lost-tail). Pinned by reading PRAGMA synchronous (==2 FULL) on a listener connection, mirroring the existing WAL/busy_timeout pragma tests. Full suite 795 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed limit (AUD-1 red-team) An adversarial review of the AUD-1 anchor (5 red-team lanes, executed PoCs) refuted every interior-delete / reorder / renumber / version-downgrade / seq-soundness attack and confirmed the Wardline v2 contract is byte-for-byte intact (201-test regression sweep green). It found one genuine residual: the anchor's HMAC stops forgery but not REPLAY. The anchor is a single mutable sidecar, so a snapshotting attacker can save a genuinely-signed early anchor (head=1), let the trail grow, truncate the DB back to seq=1, and restore the saved anchor — it verifies (real signature, consistent seq + chain_hash) and the rollback goes undetected. This is inherent to local same-filesystem storage: nothing on disk is beyond a file-write attacker's rollback, so no purely-local check (counter, timestamp, extra copy) closes it — that would be honesty theatre. The fix is a deployment property: store the anchor on append-only/WORM or remote storage, or run an external monitor on the anchored head's monotonicity. The prior docstring over-claimed it detects "a rollback to an earlier consistent prefix" — false under replay. Corrected to state precisely what it catches (forgery; truncation by a late/non-snapshotting attacker) and the replay limitation + its real mitigation. Pinned the boundary with an executable known-limitation test so the over-claim cannot silently drift back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st-marker-only (INSTALL-1) The injector deliberately tolerates a split brain: when a second legis instruction block sits beyond a sibling tool's block, it cannot canonicalise across the foreign block, so it rewrites the first block fresh, warns, and leaves the stale second copy in place (foreign-safety wins over own-dedup). The doctor's freshness probe, though, read the token off the FIRST marker only (_MARKER_TOKEN_RE.search → first match) — so a fresh first block masked a stale second block and the doctor reported "healthy" on exactly the conflicting- guidance state it exists to catch. Freshness now requires EXACTLY ONE legis block at the current token, via a new foreign-aware walk (_own_open_marker_tokens) that reuses the injector's own fence-tracking — a legis marker quoted inside a sibling block is not counted, so the probe never miscounts a documented example as a real block. check_instruction _block surfaces a split brain (>1 block) with an actionable hand-resolution message and, since the injector cannot collapse it, does not falsely claim repair fixed it. This is the same honesty discipline as GOV-1/POLICY-1: a gate must not report green on the condition it exists to detect. RED test pinned the false-"ok" first; both CLAUDE.md and AGENTS.md get the fix via the shared check. Full suite 797 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HttpLoomweaveIdentity.capability() probed GET /api/v1/_capabilities with an
explicit signed=False, so the request went out unsigned even when an HMAC key
was provisioned — the lone unsigned exception among the SEI routes, and the very
one that establishes whether legis trusts the provider as SEI-capable. On a
keyed deployment that left the trust-establishing handshake unauthenticated,
spoofable to capability=supported.
Sign it like every other route (the default path already no-ops signing when no
key is set, so loopback/trusted deployments are unchanged). Removed the per-call
`signed` knob from _request entirely: an unsigned opt-out is exactly the
affordance that caused this, and no other caller used it — so it cannot
reintroduce the gap. Wire confidentiality against an on-path response rewrite
remains TLS's job, which _validate_base_url already enforces for any non-loopback
(keyed) host.
RED-pinned the unsigned probe ({} headers when keyed) before the fix; added a
companion test that the keyless probe stays bare. Full suite 799 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… guard (JUDGE-1)
In the coached cell a model ACCEPTED maps straight to accepted=True, and the
agent-controlled rationale (and, on the degraded-to-locator branch, the entity
locator) flowed into the judge prompt with no length bound — so a prompt-stuffing
payload could bury the instruction or smuggle an injection into the model.
LLMJudge.evaluate now bounds the SERIALIZED request — {policy, entity, rationale}
exactly as build_prompt embeds it — at MAX_JUDGE_REQUEST_CHARS (8192) before the
model is consulted; over-cap is rejected as BLOCKED by a deterministic guard that
never calls the model (stamped with a self-documenting sentinel model id, not an
LLM identity). Measuring the serialized request (not the raw rationale) bounds
every agent-settable field in one check — rationale, entity locator, and the
ensure_ascii unicode-expansion variant (each non-ASCII char → 6-char \uXXXX, so a
raw-char cap would be 6x loose). Reject, never truncate: truncation would mutate
the rationale that is recorded and (protected cell) signed, and could pass a
front-loaded injection. The full over-cap rationale is still written to the
BLOCKED record, so the attempt stays attributable.
build_prompt's serialization (the structural-escape defense — a forged sibling
{"verdict":"ACCEPTED"} survives only as an escaped string value) is now pinned by
a round-trip test covering rationale AND entity injection (JUDGE-2). The module
docstring documents the residual honestly: a SEMANTIC injection that persuades
the model is a model-robustness property, not a code fail-open — mitigated by
attribution and, in the protected cell, by Q-H3's deterministic validator.
TDD: RED-pinned both stuffing vectors (rationale + entity reaching an accepting
model) and the model-never-consulted property before the guard; added an
in-cap boundary test so a thorough justification is not falsely blocked. Full
suite 803 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-2, CRYPTO-THRESHOLD-001) Closes the last three low/post-1.0 items from docs/release-1.0-risk-audit.md. POLICY-2 (this session) — remove the exemption-rescue mechanism outright. PolicyGrammar had a VIOLATION->CLEAR exemption-rescue branch wired to an agent-writable YAML loader (ExemptionAllowlist.from_file) with zero src consumers — the latent bypass trap the finding names. Full removal: delete policy/exemptions.py + tests/policy/test_exemptions.py, drop the exemptions ctor param / _exemptions / rescue branch from grammar.py, and remove the 3 rescue-branch tests. New regression guard test_grammar_has_no_exemption_rescue _mechanism pins that no exemption seam can be re-introduced by accident. This supersedes the earlier conservative document-only closure of legis-e512e97bfc (see ticket history): documenting around the loader left the trap in the tree. AUTH-1 (doc) — app.py comment telegraphs that LEGIS_ALLOW_UNSCOPED_API_TOKENS=1 grants unscoped tokens operator authority (not renamed: the var already fits the LEGIS_ALLOW_<bad-thing> family; audit remedy was "rename OR document"). CRYPTO-THRESHOLD-001 (doc) — README scopes the "cryptographic layer" to intra-suite HMAC tamper-evidence with a self-asserted actor, not third-party cryptographic proof; names RFC-8785 as the upgrade path. Full suite green (792 passed, 2 skipped), ruff clean on changed files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the 6 standing lint errors (default ruff E4/E7/E9/F ruleset): - test_doctor.py: 5x E402 (module-level imports placed under mid-file section headers) — consolidated into the top import block; section comments kept. - test_install.py: 1x F401 — dropped the unused `_legis_mcp_entry` import. No behaviour change. Full suite green (792 passed, 2 skipped), ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second adversarial pre-ship review (docs/release-1.0-pre-ship-review.md)
re-attacked the prior audit's self-verified fixes. Crypto-threshold held;
these gaps it surfaced are now closed, each independently re-verified.
- JUDGE-3 (protected-cell fail-open): the Q-H3 advisory-downgrade was gated on
exact-match `protected_policies`, which diverges from the glob-capable cell
routing — a protected-cell policy outside the set (incl. any glob route and
the empty-set default) had its model ACCEPTED signed authoritative. The cell
is now fail-closed UNCONDITIONALLY: it clears only on a validator-confirmed
ACCEPTED. Independent re-attack then caught a second variant — a fooled model
emitting the operator-only OVERRIDDEN_BY_OPERATOR (which _record_signed also
counts as accepted) cleared the gate even for a declared protected policy.
Closed at two layers: the judge JSON parser now restricts verdicts to
{ACCEPTED, BLOCKED}, and submit() downgrades the whole accepted-set.
Behavior change: with no validator wired (default prod), protected overrides
now require operator sign-off. Regression tests at parser and gate levels.
- GOV-2: /governance/identity-gaps now returns a {status, gaps} envelope
("unavailable" vs "checked") so a can't-check state is not a false all-clear,
matching the GOV-1 fix on the sibling lineage-integrity endpoint.
- F1: TrailVerifier docstring corrected — no longer claims modify-to-unsigned is
caught; the modify-to-unsigned / tail-truncation residuals of the conceded
raw-file-write tier are documented honestly (code hardening tracked post-1.0).
- POLICY-1: aliased-marker (`skipper = pytest.mark.skip; @skipper`) and
fixture-skip vectors documented as residuals in _disabling_marker (zero live
@policy_boundary sites; name-heuristic hardening tracked post-1.0).
- ID-SEI-1: LEGIS_ALLOW_INSECURE_REMOTE_HTTP now warns on a remote-plaintext
bypass (loomweave + filigree clients); documented in README + federation doc.
- ID-SEI-2: resolver `alive` is now strict-bool; a non-bool truthy value
degrades fail-closed instead of promoting to a stable SEI identity.
- README "Known security limitations" section + CHANGELOG entries.
Suite 801 passed / 2 skipped; ruff + mypy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doctor: - `--fix` is now the canonical repair flag; `--repair` stays a working alias (argparse dest `fix`), so no script breaks. - DoctorCheck gains a `repairable` bit; text view tags each problem `[fixed]` / `[auto-fixable]` / `[operator]` with footers that point auto-fixable items at `legis doctor --fix` and tell the operator that `[operator]` items need out-of-band config + a relaunch. JSON checks carry `repairable` additively. - `install.filigree_scope` is gated on filigree actually being installed (file-existence probe, no filigree import): the unscoped-binding warning only fail-closes against a server-mode filigree daemon, so it is noise when filigree is absent. When it fires, the message names it operator- owned (the `--filigree-url` is operator-pinned in wardline's `.mcp.json`) and stays repairable=False. tidy for 1.0 (version held at rc4 per the live-e2e gate): - README + doctor docstring use the canonical `--fix` spelling. - CHANGELOG [Unreleased] records the above. - .gitignore ignores `.claude/*.lock` (transient scheduled-tasks lock). - removed stray build artifacts (.coverage, coverage.json). Full suite green (813 passed, 2 skipped), ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The README covers the *why* (the 2×2 concept) and the legis-workflow skill covers the *agent-call* surface, but there was no human-operator guide for "how do I configure this" and "what am I seeing when an agent does X". Adds docs/guide/: - configuration.md — the operator's governance-control reference: reconciles "zero human config" (the agent's experience) with the operator's two acts (choose the cell, hold the key); per-cell cost/buys table; the fail-closed routing default + resolution order; full LEGIS_* / OPENROUTER_* env-var reference grouped by purpose; and a separate, warning-carrying "dev-only / escape hatches" section for the LEGIS_UNSAFE_* / LEGIS_ALLOW_* flags. - reading-legis-output.md — organized by "where it surfaces / what it means / do I act": keeps the recorded Verdict (ACCEPTED/BLOCKED/OVERRIDDEN_BY_OPERATOR) distinct from the override_submit outcome envelope (ACCEPTED_SELF / ACCEPTED_BY_JUDGE / BLOCKED / ESCALATED_PENDING / NEED_INPUTS); covers scan outcomes, artifact/identity/lineage statuses, the override-rate gate, CI exit codes, doctor tags, and flags the only signals that need a human in real time. - README.md (index) + links from the top-level README. Every flag/enum/command cited was verified against source (e.g. dropped a spurious OPENROUTER_BASE_URL row that was a grep artifact of the DEFAULT_OPENROUTER_BASE_URL constant, not a real env var). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reference tables answer "what does signal Y mean / do I act"; a single compact narrative (agent hits a coached policy → BLOCKED → revise → ACCEPTED_BY_JUDGE → async review, with the structured ESCALATED_PENDING contrast) converts the reference into the mental model behind the user's literal question, "what am I seeing when an agent does X". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gree's install predicate Two corrections to the doctor checks landed in 84a8047: - **Split-brain instruction block is not auto-fixable.** `--fix` returns before the repair branch for the >1-block split-brain case (the injector won't splice across a sibling tool's block), so tagging it `repairable=True` rendered a false `[auto-fixable]` signal that re-creates the very --fix loop the design eliminates. Now `repairable=False` → `[operator]`, matching the check's own "resolve it by hand" message. (Corrects the tag shipped in 84a8047.) - **`_filigree_installed` now mirrors filigree's real install predicate.** It was an AND requiring `.filigree.conf` AND a `config.json`; filigree's `find_filigree_anchor` (core.py:1046-1064) treats a project as installed if ANY of three markers is present: `.filigree.conf` (file), `.weft/filigree/` (dir), or `.filigree/` (dir) — never AND, and the store/legacy checks are `.is_dir()`, not a `config.json` `.is_file()`. The old AND would return "not installed" for confless / legacy / conf-only installs and SILENTLY DROP a real unscoped-binding warning where filigree genuinely is installed — the false-green the governance honesty discipline forbids. Tests updated to cover conf-only, confless-weft, and confless-legacy installs (the last is the live federation-legacy-path case). Full suite green (815 passed, 2 skipped), ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te cell trap, envelope next_action LEG-1: add the policy_list tool (routing table + each cell's honest enabled state, computed via a shared explain_cell so it can never disagree with policy_explain) and an additive matched_rule field on policy_explain (a configured policy reports its rule pattern; an unconfigured/hallucinated name reports null). cell_for now delegates to a new rule_for() so routing and discovery cannot drift. LEG-2: the error envelope already carries next_action/recoverable for every code (_recovery_for); reconcile the SKILL.md error table to it verbatim and add one drift-lock test asserting every emitted code yields a non-empty next_action. No new abstraction. LEG-3: scan_route's server-owned rejection now names the rejected request-side arg(s) (cell/severity_map/fail_on) while retaining the literal 'server-owned' substring; the cell/severity_map/fail_on schema descriptions state the LEGIS_UNSAFE_WARDLINE_REQUEST_ROUTING gating. Additive only; no routing/enablement/tiering semantics changed. ruff + mypy clean; full suite 825 passed, 2 skipped (+10 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Version 1.0.0rc4 -> 1.0.0 across pyproject, legis.__version__ (feeds the MCP serverInfo, /health, and `legis --version`), and uv.lock. CHANGELOG [Unreleased] -> [1.0.0] (2026-06-09) with refreshed compare links. 1.0 release-prep hygiene (same pass): - README points to the now-public adversarial threat model — the risk audit and the independent pre-ship review, attack recipes and all — framed as the "forced me to do the right thing" discipline it is. - Dropped the rc1 "Known limitations" list from the changelog: the MCP item was superseded at rc2; the live sibling-gated items moved to the Filigree tracker (outstanding work belongs in the tracker, not the log). No code behavior change — version strings + docs only. Full suite green (825 passed, 2 skipped; ruff + mypy clean). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes three bugs from the 2026-06-10 code review (filigree legis-c9a4d67542, legis-517aa65e37, legis-02978d839d). - doctor: check_filigree_binding_scope now triggers on the presence of an unscoped filigree scan-results binding URL, not on a local filigree install. The install gate false-greened the federation-consumer case (no local marker + unscoped REMOTE --filigree-url): the remote server-mode daemon fail-closes the unscoped write (N1) while doctor read all-clear. Binding-presence strictly subsumes the old gate; dropped the now-dead _filigree_installed helper. Reverses a11378e (install-parity was the false-green). Deliberately-baked suppression test rewritten to assert the warn. - doctor: render_text now includes repaired checks (status "ok" + fixed=True) in the rendered set and adds a "fixed N item(s)" banner, so `--fix` reports what it repaired in text mode and the [fixed] tag branch is reachable. Prior test used a contrived status="warn" input that masked the bug. - enforcement: ProtectedGate.submit gates the validator on the ACCEPTED path and wraps it in try/except — a raising operator-supplied validator is now a veto (-> BLOCKED) instead of an unhandled fail-open-shaped 500, and no longer runs on already-BLOCKED submits. Verification: pytest 827 passed / 2 skipped; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ies (JUDGE-3 hygiene) Pre-existing governance-honesty pass, carried into rc5 ahead of the G1 fix: - Verdict.model_emittable() / Verdict.accepting() become the single source of truth for "an LLM judge may emit this" and "this verdict cleared a gate". judge.py, lifecycle.py, and protected.py consume them instead of re-inlining the member tuples, so the JUDGE-3 guard (a model must never emit OVERRIDDEN_BY_OPERATOR) and the accepting set cannot drift apart. - CELL_TIER_ORDER is promoted to the canonical ordered cell membership in policy/cells.py; VALID_CELLS is derived from it, and mcp.py policy_list iterates it — a new governance cell can no longer be silently omitted from the policy_list cells block. - Flatten the server-owned Wardline routing branch (no behaviour change). - Sync loomweave-workflow skill docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…under green (G1)
Weft federation G1 (seam S8, GS-1+GS-7): producer (Wardline core/legis.py) and
consumer (legis ingest) agree the scan batch carries defects under the key
"findings", but nothing asserted that key's PRESENCE. active_defects did
`scan.get("findings", [])`, so a silent producer rename ("findings" ->
"findings_list"), re-signed HMAC-clean, verified cleanly, read as ZERO active
defects, and route_wardline_scan returned routed=[] with artifact_status
"verified" — the entire Wardline->legis defect flow breaking silently under a
green status (the vacuous-green class the RoutedScan docstring names as opp #6).
The signature does NOT protect against this: it is contract-drift, not tamper.
The producer re-signs the renamed dict, so HMAC proves authenticity, not schema
conformance. The only structural defense is asserting the key's presence
independent of the signature.
Fix:
- FINDINGS_KEY = "findings" module constant — the cross-impl contract anchor,
not a bare string scattered across producer + consumer.
- active_defects() raises WardlinePayloadError when FINDINGS_KEY is absent,
distinguishing "key absent" (drift/tamper -> red) from "key present, empty
list" (a genuinely clean scan -> []). A clean scan carries findings: [].
Placement is active_defects(), not verify_wardline_artifact() as the report
suggested: verify returns early in the keyless posture (the lacuna showcase
default tier) before any field check, so a guard there would leave keyless
exposed. active_defects() is the single choke every posture's route passes
through — verified closed across keyed + keyless by adversarial replay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A P0 false-green (G1, weft S8) was found after 1.0.0 final was cut. A silent Wardline producer rename of the findings key routed zero defects under a green verified status. That is a must-close honesty blocker, so we go back to a release candidate rather than ship final with it open. rc5 carries the G1 fix plus the JUDGE-3 vocabulary-hygiene pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A standalone, dependency-free static landing page at www/, modeled on the federation hub site (~/weft/www/) but focused on the single product: what Legis is, the governance 2×2 (chill/coached/structured/protected), how it engages each sibling (Loomweave §6, Filigree §7, Wardline §8, Charter §9), and the published security-honesty posture. Design system reused verbatim — colors_and_type.css and the bundled fonts are byte-for-byte copies of the hub's; violet legis thread for identity, amber stays the interactive accent. No build step, GitHub-Pages-deployable (.nojekyll). Content-complete with JS off; main.js only adds the 2×2 filter. Honesty discipline preserved: "tamper-evident" never "tamper-proof", all residual threat tiers named, both adversarial reviews linked, version shown as a dated snapshot at the 1.0.0 line pointing to CHANGELOG for live state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d guard; G11/G12 honesty Closes the legis half of the Weft cross-member contract hardening (incident 2026-06-10). The dangerous G1 false-green (absent `findings` key routing zero defects under a green `verified` status) was fixed in rc5 (a9bb827) but had only a local test — root cause #2 of the incident is precisely "hand-transcribed contracts with no shared test", so the fix was not yet real. G1 — shared conformance vector (the fix is real now): - tests/contract/weft/vectors/wardline_scan_artifact.v1.json: the canonical, cross-member wire-contract vector. The PRODUCER (wardline core/legis.py) and every CONSUMER (legis ingest) load the SAME bytes. The byte-exact expected_signature doubles as the canonical-JSON+HMAC drift detector. - tests/contract/weft/test_wardline_scan_artifact_contract.py: drives every valid/invalid case through legis's real ingest + signer. - test_ingest.py: the inline _GOLDEN_FIELDS/_GOLDEN_SIG (a second hand-copied literal) are now SINGLE-SOURCED from the vector — no more legis-side duplicate. - vectors/README.md documents the vendor-both-sides contract + schema. G1 twin (value axis, legis-b69949740b) — the `kind` token must be a KNOWN value: - active_defects selected the gate population with a bare `kind == "defect"`; a defect whose kind token drifted out of Wardline's vocabulary (re-signed HMAC-clean) silently fell through the skip and vanished under a green status — the same false-green class as G1, on the value axis. - KNOWN_KINDS / DEFECT_KIND constants, carried verbatim from Wardline core/finding.py::Kind {defect,fact,classification,metric,suggestion} like TRUST_TIERS. An unknown kind is rejected loudly; known non-defect kinds stay legitimately excluded. Negative + over-correction cases in the shared vector. G11 (legis-f2bd35f88a, in-repo half) — verification posture stated plainly: - weft_signing docstring now names the transport-open reality: legis EMITS the X-Weft-* HMAC + app-level binding_signature, the classic Filigree route stores them without verifying. Integrity rests on the loopback transport + legis's own BindingLedger, not on a sibling checking the signature. The headers are kept (shared seam, cheap, forward-compatible); verify-or-declare is Filigree's call. G12 (legis-356fe094dd, scaffold) — real-Filigree bind + closure-gate: - tests/governance/test_signoff_binding_real_filigree.py: skipped unless LEGIS_FILIGREE_TEST_URL + LEGIS_FILIGREE_TEST_ISSUE name a live daemon. Asserts the bind PERSISTS (read the association back — the assertion FakeFiligree's []-returning echo structurally cannot make), all bound fields round-trip, the closure-gate clears over real HTTP, and the keyless bind is accepted (the live evidence behind the G11 transport-open posture). 847 passed, 4 skipped; ruff + mypy clean. Local only — merge stays gated on filigree-merges-to-main-first (the one rule). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…G-2, N-1
- N-9/LEG-1: policy_explain carries an explicit policy_known boolean
(true iff a registry rule matched; omitted from explain_cell payloads
so policy_list rows never carry it); tool description documents the
signal. Unknown names still default-route — no POLICY_NOT_FOUND.
- LEG-2: _tool_error appends "next_action: ..." to the error TEXT
content (the "{code}: {message}" first line stays parse-stable;
structuredContent unchanged) so clients that only surface text see
remediation on every error. Terse NotEnabledError messages now name
LEGIS_HMAC_KEY as the operator-set, out-of-band knob (C-8: phrased as
operator actions, keys stay out of agent reach).
- N-1: legis session-context always prints a one-line posture banner
(instructions / skill pack / cells-config posture, honest unreadable/
stale/not-installed variants, failure line on the exception path) —
never exits 0 silently; never claims MCP-server runtime posture the
hook process cannot see. Also fixes a LEGIS_POLICY_CELLS env-leak
order dependency from cli.py's mcp path in the test suite.
Verified: full suite 862 passed / 4 skipped; live stdio MCP probe
replayed the dogfood scenarios against the tree; per-finding
adversarial review found no blockers. Filigree: legis-965174efe6,
legis-4234a2e8b3, legis-e837e8068d (closed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ock publish rc5 (31f5dd7) re-added a hard gate making PyPI `publish` depend on `live-loomweave-conformance`, whose config check exits 1 when LOOMWEAVE_URL / LOOMWEAVE_LIVE_ORACLE_LOCATOR / LEGIS_LOOMWEAVE_HMAC_KEY are unset — reinstating exactly the rc4 release blocker that f95036b removed when those values were not provisioned. Make the job skip-not-fail: detect the live oracle config and, when absent, pass as a fast no-op (notice, no error) so publish proceeds; when present, install and run the oracle for real so a genuine conformance failure still blocks publish. The gate bites where it can without blocking an unprovisioned release. Codex PR #10 review (P1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Disposition of Codex's third review (1 × P1):
Note: this workflow only triggers on |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0dafc8349a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| assert env["LEGIS_LOOMWEAVE_HMAC_KEY"] == "${{ secrets.LEGIS_LOOMWEAVE_HMAC_KEY }}" | ||
|
|
||
| commands = "\n".join(str(step.get("run", "")) for step in live_job["steps"]) | ||
| assert "Missing required release conformance environment" in commands |
There was a problem hiding this comment.
Update stale release-conformance assertion
This assertion still expects the old fail-closed release check text, but .github/workflows/release.yml now implements the skip-not-fail path in the “Detect live oracle configuration” step by emitting a notice and configured=false. Once dev dependencies are installed, the normal uv run pytest CI/release test gate will fail here even though the workflow no longer contains that string; update the test to assert the new skip behavior instead.
Useful? React with 👍 / 👎.
| # ``_validate_base_url`` enforces for any non-loopback (keyed) host. | ||
| body = _require_dict( | ||
| self._request("GET", "/api/v1/_capabilities", None, signed=False), | ||
| self._request("GET", "/api/v1/_capabilities", None), |
There was a problem hiding this comment.
Reject Loomweave redirects before signing probes
With LEGIS_LOOMWEAVE_HMAC_KEY configured, the capability probe is now signed, but the default transport still uses urllib.request.urlopen, whose redirect handler follows 30x GET responses while preserving non-content headers. If LOOMWEAVE_URL or an intermediary redirects to another origin, the X-Weft-* HMAC headers are forwarded to that target despite the new redirect-custody claim; use a no-redirect opener like the Filigree client before emitting signed Loomweave requests.
Useful? React with 👍 / 👎.
v1: signed chill-baseline posture floor under the per-policy registry, keychain-custodied operator key minted at install, sudo-style elevation sessions for signing, embarrassing-not-catastrophic keyless rekey. v2 unification (protected-cell/commit signing onto sessions) tracked separately in Filigree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssions Ultracode workflow output (12 agents): grounded against codebase reality (9 spec inaccuracies caught), drafted phased TDD plan, adversarially reviewed across reality/architecture/quality/systems (25 critical/high findings resolved), synthesized. 5 open questions flagged for operator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote FlooredRegistry to the cross-surface chokepoint (MCP+API+CLI); collapse the HTTP API's cell-addressed submit routes into one policy-routed POST /overrides; operator-clear routes stay distinct; per-request floor read; cryptography mandatory; age-file re-prompt accepted; doctor non-zero on KEY_RESET; posture_get per-policy; SEI conformance contract updated in-release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-run of the ultracode planning workflow (13 agents) against the revised spec: 13 spec inaccuracies caught, 27 critical/high findings resolved. Adds Decision D0 (floor at every agent-visible cell-resolution site, not just routing), FlooredRegistry as PolicyCellRegistry subclass, phased API route collapse (add-alongside -> green -> delete, never an all-red window), per-request tail-read floor, and SEI conformance oracle update. 6 open questions flagged for operator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
D4 -> warning-discriminant variant (not silent floor-exempt); posture coverage floor 93% (match enforcement/); cryptography>=42 provisional (P3 follow-up legis-ea02d6c6a8); FlooredRegistry subclass + wrapper fallback pre-approved; single active session; explicit CI operator-enable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add cryptography>=42 as a hard dependency (age-file custody backend: scrypt KDF + AES-GCM); refresh uv.lock. - config: add posture_db_url() + LEGIS_POSTURE_DB to STORE_DB_SPECS, operator_session_path() and operator_age_path() resolvers under the federated .weft/legis subtree; amend the keys-out-of-scope doctrine block for the narrow operator-authority-key carve-out (spec §5/§6). - check_coverage_floors: register src/legis/posture/ at 93% (matching enforcement/, the most security-sensitive tier) and make an unmeasured prefix skip-not-fail so the floor lands ahead of Phase 1 records.py while staying fail-closed the moment statements exist. - Tests (TDD, written first): tests/posture/test_deps.py + tests/posture/test_config.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the signed posture-floor ledger (design 2026-06-16, plan Phase 1):
- records.py: PostureRecord frozen dataclass + kind constants
(GENESIS/TRANSITION/KEY_RESET/OPERATOR_SESSION_OPENED). to_payload()
emits exactly the eight domain fields and excludes seq/prev_hash/
chain_hash (the store owns chain fields).
- ledger.py: PostureLedger domain wrapper over AuditStore.
- read_floor(): O(1) tail read (get_latest_sequence_and_hash +
read_by_seq), NOT read_all. Absent DB / empty store -> None
(fail-closed: callers map None -> structured, never chill).
- genesis(): writes the keyless chill GENESIS once; idempotent and
rekey-safe (no-op if any record exists, incl. a KEY_RESET tail).
- transition(): signed TRANSITION binding chain_seq (v3); verifies
signer fingerprint == current epoch before signing; fail-closed
(raise in build -> no half-write); no fresh-connection read inside
the append_signed batch callback (Q-M5).
- session_opened()/rekey(): Phase 3.2 / Phase 11 signatures (stubbed).
Posture package coverage 97.3% (floor 93.0%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ad test) Rewrite test_no_read_inside_transition_batch to bind to the path append_signed actually uses. The prior guard wrapped reads with `if store.in_batch(): raise`, but append_signed acquires its connection via self._engine.begin() and never sets the thread-local batch handle, so in_batch() is False throughout the build() callback — the guard could never fire (structural false-green; the Q-M5 invariant went unverified). The revised test monkeypatches read_all/read_by_seq/ get_latest_sequence_and_hash to raise unconditionally for the duration of transition() and asserts transition() still succeeds (call-count==0), proving the build callback issues no fresh-connection read. Mutation verified: injecting read_all() inside build() now turns the test RED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PostureSigner seam + custody backends (design §6/§7, plan Phase 2). Task 2.1: mint_key()/key_fingerprint() install-time key primitives, the PostureSigner protocol, and _RawKeySigner. The key is held by the backend, never passed by the caller; sign() returns a v3-prefixed HMAC bound to chain_seq; fingerprint() is sha256 of the held key. Fail-closed: no public attribute or zero-arg method surfaces the key bytes/hex (test-pinned). Task 2.2: three custody backends + age crypto in signing.py (consolidated, no separate custody.py): KeychainSigner (injectable secure-store seam), AgeFileSigner (real scrypt + AES-GCM wrap/unwrap, no age CLI shell-out, re-prompt-per-sign passphrase callback), EnvSigner (LEGIS_OPERATOR_KEY escape hatch behind explicit insecure_env=True, emits InsecureEnvKeyWarning). select_backend() defaults to keychain else age-file; env only on opt-in. The real-keychain round-trip is @pytest.mark.integration (excluded from CI); the marker is registered in pyproject. Fail-closed: signer error → refuse; key bytes never returned to the caller. Posture pkg coverage 98% (signing.py 99%), above the 93% floor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/D1/D2) Floor applied at every agent-visible cell-resolution site (override routing, policy_explain, policy_list, hooks banner, service/explain) via a FlooredRegistry subclass; floor read per-invocation from the ledger handle (initialize=False — no local state on launch). Absent/empty ledger -> the identity floor chill (no-op), deferring to the registry's own fail-closed default (structured in prod, chill under the dev opt-in): preserves the N3 keyless-chill acceptance and the build_runtime no-local-state invariant. Idempotency made floor-insensitive with a floor_warning discriminant (D4 warning variant). Spec section 4 reconciled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add set_floor() change gate in posture/ledger.py: an open elevation session is required (D3), the signer's fingerprint is checked against the LEDGER current-epoch fingerprint (last GENESIS/KEY_RESET, not the session's recorded field), and exactly one signed TRANSITION is appended or the call refuses fail-closed with the floor unchanged. Adds current_epoch_fingerprint() (a tail read resolved before append_signed, Q-M5) and a PostureSetResult outcome with stable refusal discriminants. Re-exports set_floor/PostureSetResult. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
install_posture() mints the operator-authority key once, hands it to a custody backend via an injectable KeySink (never the ledger, never .mcp.json), and writes a single keyless GENESIS at floor=chill carrying only the key fingerprint. Idempotent / re-key-safe: a second install over an existing GENESIS or KEY_RESET tail re-mints nothing and appends nothing. - choose_install_backend() feeds select_backend() the keychain probe (_keychain_available, conservative False until a live adapter ships -> age-file fallback; env only behind --insecure-key-in-env). - _default_key_sink routes by backend: env no-op, age-file wraps under LEGIS_OPERATOR_KEY_AGE_PASSPHRASE -> operator.age (no plaintext), keychain raises until an adapter ships. Custody runs BEFORE the genesis append so a custody failure leaves no fingerprint the operator cannot sign against. - OperatorKeyCustodyError lets a bare `legis install` defer the posture step (non-fatal) rather than hard-fail when custody is unconfigured. - _REJECTED_MCP_ENV_KEYS gains LEGIS_OPERATOR_KEY; _safe_mcp_env scrubs the LEGIS_OPERATOR_KEY* family by prefix so the key never lands in .mcp.json. - .gitignore gains root-anchored /.weft/legis/operator_session.json and /.weft/legis/operator.age. - CLI: `legis install [--posture] [--insecure-key-in-env]` wires the step in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the `legis posture` (show/set) and `legis operator` (enable/disable) subcommand groups. `posture show` reads the floor via an initialize=False ledger handle (no local state on launch), mapping an absent ledger to "structured (no ledger)". `posture set <cell>` runs the fail-closed change gate: per D3 it refuses without an open elevation session, builds the session's custody signer (env/age-file; keychain not shipped), and delegates to set_floor so signer/fingerprint failures refuse with no half-write. `operator enable [--ttl] [--insecure-key-in-env]` resolves + verifies custody up front, opens the single elevation session, and appends OPERATOR_SESSION_OPENED; the env CI/headless path STILL opens a session (D3) so every TRANSITION carries a non-null session_id. `operator disable` ends the session. Keychain is the only backend with a non-null unlock_ref (D5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the read-only posture_get MCP tool: returns the governing posture floor and, for a named policy, the floored effective cell (max(floor, registry cell)). Reads the floor fresh per-invocation off the held ledger handle (D2); an absent/empty ledger reports the floor as structured, never chill (cross-cutting checklist #1). Surfaces epoch_reset_unacknowledged so the agent sees the same pending-operator signal doctor exits non-zero on, via a new PostureLedger.epoch_reset_unacknowledged() structural check (latest epoch opener is a KEY_RESET with no acknowledging TRANSITION). The change gate stays operator/CLI only — there is NO posture_set over MCP (C-8). Tool surface, _AGENT_TOOLS, and _TOOL_HANDLERS updated in lockstep; output-schema conformance vector and the full-surface pin extended. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the three cell-addressed submit routes (/overrides chill/coached,
/protected/overrides, /signoff/request) into one policy-routed POST /overrides
that resolves the governing cell through a per-request FlooredRegistry (floor
read fresh on a shared, initialize=False ledger handle; D0/D2). Discriminated
outcome mirrors MCP override_submit: accepted/blocked (201/409), structured
escalation_requested (202), protected need_inputs (422). The legacy env-var
protected_set 403 guard is removed — the floored cell, not a config-era set,
owns protected routing. The operator-clear routes (/protected/operator-override,
/signoff/{seq}/sign) keep their distinct verify_operator authority.
Phased to avoid an all-tests-red window: wire ledger+registry at create_app
(9.0), add the unified route and new tests green (9.1-9.4a), then delete the old
routes + legacy request models (9.4b). SEI keying is preserved on every dispatch
(service functions call resolve_for_entry internally); conformance doc updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the posture-ledger doctor checks (Phase 10, Tasks 10.1–10.3):
- check_posture_chain: report-only hash-chain integrity; missing/zero-byte
store is ok ("no ledger yet"), tampered chain is error. No-leak (never
creates the DB).
- check_posture_ledger: distinguishes no-file (ok), GENESIS-present
(ok, reports floor), and file-but-no-GENESIS (warn) — the empty-store
signal verify_integrity() would otherwise hide.
- check_posture_key_reset: non-zero exit on an unacknowledged KEY_RESET.
Per D6, acknowledgment requires a later TRANSITION whose operator_sig
*verifies* (signing.verify) under the new epoch key, not merely a later
record of the right kind (record-kind presence is replayable). Message
names the reset date + agent_id; never renders key material.
- check_operator_key_accessible: report-only key reachability — warns when
no backend can produce the epoch fingerprint (posture set will refuse) or
when LEGIS_OPERATOR_KEY is set (plaintext-in-env honesty note).
Wired into collect_checks so run_doctor returns non-zero on an
unacknowledged rekey. All checks fail-closed on a missing/empty ledger
(report-only ok) and never raise from doctor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lost-key recovery path (design §8): `PostureLedger.rekey()` mints a fresh key epoch, hands the new key to custody before writing, resets the floor to chill, and chains a single keyless KEY_RESET onto preserved history (append, not a fresh DB) — needs no old key and no open session. `legis posture rekey` [--backend] dispatches it via the install key-sink. Doctor's Phase-10.2 check then keeps `legis doctor` non-zero until a signed TRANSITION verifies under the new epoch. Supersedes the Phase-1 rekey-not-implemented stub test with a real-behavior assertion, mirroring the Phase-3 session_opened supersession. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/posture/test_security_honesty.py pinning the published honesty guarantees of the posture-ratchet feature (design §6/§8/§9/§10): TTY session expiry refuses a post-expiry posture set; the operator key never returns to the caller, never appears in a signature/public attr, and never lands in logs (deterministic caplog behavioral test across keychain/age-file/env backends); rekey can never land above chill; every TRANSITION carries its session_id (including the env-backend D3 path); the env escape hatch is explicit + loud; the age-file backend fails closed on a wrong/absent passphrase. Task 12.1: publish the operator-session-file residual in README 'Known security limitations' (same tier as raw-DB-write; mitigation is OS keychain access control, not session-file encryption). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tract test_release_publish_requires_live_loomweave_conformance asserted the old hard-fail guard string that 0dafc83 deliberately removed (skip-not-fail, never block publish). Re-pin to the current contract: the unprovisioned-env skip branch (configured=false, 'not blocking publish'), the provisioned run branch (configured=true), the old hard-fail string's ABSENCE, and that the real oracle run is gated on steps.oracle_config.outputs.configured. Stale test predating the posture work; no posture files touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8415a0286c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| """ | ||
| self.store.append( | ||
| { | ||
| "kind": KIND_SESSION_OPENED, |
There was a problem hiding this comment.
Keep session-open events from clearing the floor
When an operator opens a session after the floor has been raised, this OPERATOR_SESSION_OPENED append becomes the tail record but carries no floor; read_floor() reads only the tail payload's floor, so it returns None and the routing layer drops back to the registry/default posture until a transition is appended. Preserve the current floor on this event or have read_floor() skip non-floor records so enabling a session cannot temporarily downgrade governance.
Useful? React with 👍 / 👎.
| if backend == "env": | ||
| return |
There was a problem hiding this comment.
Use the env key for the recorded epoch
When legis install --posture --insecure-key-in-env or legis posture rekey --backend env selects the env backend, the caller mints a fresh key_hex and then this branch discards it while the ledger records that discarded key's fingerprint. Later EnvSigner signs with LEGIS_OPERATOR_KEY, whose fingerprint will not match the recorded epoch, so every posture set refuses with fingerprint_mismatch; make the env backend record the actual env key's fingerprint or otherwise hand the minted key to the operator.
Useful? React with 👍 / 👎.
| def build(seq: int, prev_hash: str) -> dict[str, Any]: | ||
| record = PostureRecord( | ||
| kind=KIND_TRANSITION, | ||
| floor=new_cell, |
There was a problem hiding this comment.
Validate the target floor before signing it
With an open operator session, legis posture set structred reaches this path and records a signed TRANSITION with an unknown floor because new_cell is never validated before the append. Subsequent posture reads/routing call _validate_cell/_max_tier on that stored value and fail, effectively corrupting the posture ledger until another valid transition is written; reject unknown cells before building the record.
Useful? React with 👍 / 👎.
| if os.environ.get(_OPERATOR_KEY_ENV): | ||
| return DoctorCheck( | ||
| cid, | ||
| "warn", | ||
| message=( | ||
| "operator key present in LEGIS_OPERATOR_KEY (plaintext-in-env) — usable " | ||
| "but a residual: prefer the keychain/age backend. [operator]" | ||
| ), |
There was a problem hiding this comment.
Check the env key against the ledger epoch
If LEGIS_OPERATOR_KEY is present but stale, malformed, or from a different epoch, this returns a warning saying the key is usable without comparing it to epoch_fp; posture set will still refuse with fingerprint_mismatch. Probe key_provider(epoch_fp) before reporting the env backend usable so doctor surfaces the real recovery action instead of a false healthy signal.
Useful? React with 👍 / 👎.
| if cell in ("chill", "coached"): | ||
| try: | ||
| result = _submit_override( | ||
| engine(), |
There was a problem hiding this comment.
Refuse coached HTTP overrides without a judge
When policy/cells.toml routes a policy to coached but the app was built normally (no injected EnforcementEngine with a judge), this branch still calls engine(), which constructs a judge-less simple engine and records an accepted override. That silently turns a coached policy into chill/self-clear while returning cell: "coached"; mirror the MCP enablement check (engine.has_judge) or build a judged engine before accepting coached submissions.
Useful? React with 👍 / 👎.
| if missing: | ||
| response.status_code = 422 | ||
| return { | ||
| "outcome": "need_inputs", | ||
| "cell": "protected", | ||
| "required_inputs": missing, |
There was a problem hiding this comment.
Report disabled protected cells before requesting inputs
When a policy is routed to protected but LEGIS_HMAC_KEY is absent, a request missing file_fingerprint/ast_path returns need_inputs here before checking that protected_gate is actually enabled; after the caller supplies those inputs, the same route then fails with protected cell not enabled. Check explanation.enabled first (as the MCP path does) so disabled protected deployments return the operator action immediately instead of asking for unusable inputs.
Useful? React with 👍 / 👎.
| # posture_get can never disagree with the cell an override would route | ||
| # to. _floored_registry is fail-closed structured on a missing ledger | ||
| # via _registry()'s fail_closed default, matching the reported floor. | ||
| payload["effective_cell"] = _max_tier(floor, _registry(runtime).cell_for(policy)) |
There was a problem hiding this comment.
Match posture_get to the actual floored routing
When the posture ledger is missing/empty and the registry would otherwise route a policy to chill (for example the dev default or an injected chill registry), this reports effective_cell: "structured", but override_submit uses _floored_registry, where a None floor is a no-op and the same policy can self-clear as chill. Compute this field through the same floored registry/fallback as dispatch so agents are not told a fresh submit will escalate when it will actually self-clear.
Useful? React with 👍 / 👎.
| env_key = os.environ.get(_OPERATOR_KEY_ENV) | ||
| if env_key: | ||
| try: | ||
| from legis.posture.signing import key_fingerprint | ||
|
|
||
| if key_fingerprint(env_key) == fingerprint: | ||
| return env_key |
There was a problem hiding this comment.
Let doctor verify the default age-file epoch
The default custody backend is age-file when keychain is unavailable, and posture rekey writes the new epoch key to operator.age; after the operator re-raises the floor, check_posture_key_reset still calls this provider, which only probes LEGIS_OPERATOR_KEY. As a result age-file/keychain deployments can never acknowledge a reset in doctor even though posture set can sign successfully; add the same age-file/keychain unlock path (or a pluggable provider) before treating the reset as permanently unacknowledged.
Useful? React with 👍 / 👎.
| "epoch_reset_unacknowledged": bool( | ||
| ledger is not None and ledger.epoch_reset_unacknowledged() | ||
| ), |
There was a problem hiding this comment.
Avoid reading the missing posture DB in posture_get
On a fresh MCP runtime build_runtime() always supplies a PostureLedger handle opened with initialize=False, even when .weft/legis/legis-posture.db does not exist. read_floor() handles that as None, but this follow-up call immediately does read_all() without the same missing-file guard, so posture_get can return INTERNAL_ERROR instead of the documented floor: structured/epoch_reset_unacknowledged:false on an uninstalled project; skip the epoch scan when the floor read showed no ledger.
Useful? React with 👍 / 👎.
| session = open_session( | ||
| ttl=ttl, | ||
| operator_id=operator_id, | ||
| backend_id=backend_id, | ||
| unlock_ref=unlock_ref, | ||
| ) |
There was a problem hiding this comment.
Verify the epoch before publishing a session
If the custody backend can be opened but holds a stale key (or the posture ledger has no current epoch), this writes an active operator_session.json before checking that the signer fingerprint matches the ledger epoch; the next posture set then refuses with fingerprint_mismatch even though enable claimed a usable window. Compare the signer fingerprint to current_epoch_fingerprint() and only persist the session after the ledger-side session_opened record can be written.
Useful? React with 👍 / 👎.
The override endpoint reused one `result` variable across the chill/coached, structured, and protected branches, binding it to EnforcementResult and making mypy reject the SignoffResult / ProtectedResult reassignments. Give each branch its correctly-typed name (signoff_result / protected_result) and narrow the protected inputs (always required via _PROTECTED_INPUTS, guaranteed by the preceding need-inputs guard). No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95be7eb2d9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Developer config / local tooling — not part of the solution | ||
| .claude/ | ||
| .agents/ | ||
| .weft/ |
There was a problem hiding this comment.
Stop ignoring the whole Weft namespace
This new local-tooling block still leaves the catch-all .weft/ rule immediately below in force, so the narrower .weft/legis/ rule and the README claim that only Legis-owned runtime state is ignored do not actually take effect. In a repo that needs to track or review another Weft member's project metadata under .weft/..., Git will continue to suppress it; remove the catch-all or replace it with member-specific ignores.
Useful? React with 👍 / 👎.
| print(f" [skip] {prefix:28} not yet measured (prefix matched no files)") | ||
| continue |
There was a problem hiding this comment.
Fail missing coverage prefixes
This skip now applies to every configured floor, including long-existing packages like src/legis/enforcement/ and src/legis/service/, so a coverage report that stops matching a prefix because coverage paths changed or a package was accidentally omitted will still exit 0. The previous behavior correctly failed statements == 0; keep that for prefixes that should already exist, or explicitly whitelist only genuinely not-yet-landed packages.
Useful? React with 👍 / 👎.
rc5 → main
Promotes the
rc5release line intomain: 70 commits, the coordinated 1.0.0 Weft launch plus the post-cut honesty and hardening pass.Governance honesty
ArtifactStatusReasonto canonical weftreason_classwith a two-way conformance vectorMCP surface
outputSchemaon every tool + conformance vectoroverride_list,doctor_get,policy_boundary_check,signoff_bind_issue,check_reportpull_request_recorddeliberately stays off the surface (forge is source of truth)Install / doctor hardening
.mcp.jsonconfig (legis-788a85fac1)Site
legis.foundryside.devlanding site on@weft/site-kitHygiene
🤖 Generated with Claude Code