Skip to content

Release 1.0.0 — rc5 into main#10

Merged
tachyon-beep merged 98 commits into
mainfrom
rc5
Jun 17, 2026
Merged

Release 1.0.0 — rc5 into main#10
tachyon-beep merged 98 commits into
mainfrom
rc5

Conversation

@tachyon-beep

Copy link
Copy Markdown
Collaborator

rc5 → main

Promotes the rc5 release line into main: 70 commits, the coordinated 1.0.0 Weft launch plus the post-cut honesty and hardening pass.

Governance honesty

  • Never PASS on a zero-file scan; policy-boundary fails-degraded on hostile nesting instead of dying green (Friction D, dogfood-4 A2)
  • G1: reject an absent findings key instead of routing zero under green; map ArtifactStatusReason to canonical weft reason_class with a two-way conformance vector
  • Lineage / identity-gap honesty reads; SEI-on-entry (L1 inline bind) for override + signoff authoring

MCP surface

  • Full 21-tool surface with outputSchema on every tool + conformance vector
  • Added override_list, doctor_get, policy_boundary_check, signoff_bind_issue, check_report
  • pull_request_record deliberately stays off the surface (forge is source of truth)

Install / doctor hardening

  • Resolve own running binary; never clobber operator .mcp.json config (legis-788a85fac1)
  • Retire legis→Filigree transport-HMAC — bind route is transport-open (G11)
  • Doctor MCP-registration + split-brain probes

Site

  • legis.foundryside.dev landing site on @weft/site-kit

Hygiene

  • Keep the repo code-only — untrack local config/dev tooling

🤖 Generated with Claude Code

tachyon-beep and others added 30 commits June 8, 2026 01:32
…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>
@tachyon-beep

Copy link
Copy Markdown
Collaborator Author

Disposition of Codex's third review (1 × P1):

  • release.yml:94 — publish gated on live-Loomweave conformance that hard-fails when unprovisionedfixed (0dafc83). rc5's 31f5dd7 re-added the exact rc4 blocker that f95036b removed. Reworked the conformance job to skip-not-fail: when LOOMWEAVE_URL / LOOMWEAVE_LIVE_ORACLE_LOCATOR / LEGIS_LOOMWEAVE_HMAC_KEY are unset it passes as a fast no-op (notice, not error) so publish proceeds; when they're provisioned it installs and runs the oracle for real, so a genuine conformance failure still blocks publish. The gate bites where it can without blocking an unprovisioned 1.0 release.

Note: this workflow only triggers on release: published, so it doesn't run in PR CI — validated by YAML parse + logic review. The other 5 checks re-run green on the push.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread tests/test_ci_workflow.py Outdated
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

tachyon-beep and others added 20 commits June 16, 2026 11:38
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/legis/install.py
Comment on lines +1289 to +1290
if backend == "env":
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/legis/doctor.py
Comment on lines +708 to +715
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]"
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/legis/api/app.py
Comment on lines +569 to +572
if cell in ("chill", "coached"):
try:
result = _submit_override(
engine(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/legis/api/app.py
Comment on lines +637 to +642
if missing:
response.status_code = 422
return {
"outcome": "need_inputs",
"cell": "protected",
"required_inputs": missing,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/legis/mcp.py
# 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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/legis/doctor.py
Comment on lines +577 to +583
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/legis/mcp.py
Comment on lines +2354 to +2356
"epoch_reset_unacknowledged": bool(
ledger is not None and ledger.epoch_reset_unacknowledged()
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/legis/cli.py
Comment on lines +540 to +545
session = open_session(
ttl=ttl,
operator_id=operator_id,
backend_id=backend_id,
unlock_ref=unlock_ref,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@tachyon-beep tachyon-beep merged commit 8dba661 into main Jun 17, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread .gitignore
Comment on lines +49 to 52
# Developer config / local tooling — not part of the solution
.claude/
.agents/
.weft/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +84 to 85
print(f" [skip] {prefix:28} not yet measured (prefix matched no files)")
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant