Skip to content

wardline 1.0.1 — first complete post-reboot release (SEI set-merge)#40

Merged
tachyon-beep merged 188 commits into
mainfrom
rc5
Jun 16, 2026
Merged

wardline 1.0.1 — first complete post-reboot release (SEI set-merge)#40
tachyon-beep merged 188 commits into
mainfrom
rc5

Conversation

@tachyon-beep

Copy link
Copy Markdown
Collaborator

wardline 1.0.1 — first complete post-reboot release

This is the cut of rc5main for wardline 1.0.1. 1.0.0 is already on PyPI
(a partial publish from before the mid-project reboot), so the first complete
reboot release ships as 1.0.1 — it is both "the full release" and the point bump.

Important

Do not merge in isolation. This merges as a coordinated set with the
sibling packages (Loomweave / Filigree / legis) because they must all speak
SEI together. Hold until the siblings' PRs are merge-ready, then merge the
set in lockstep.

What's in 1.0.1 (high level)

  • Python taint frontend — the full trust-boundary / semantic-tainting engine
    (soundness redesign, 6 sink families, boundary partition, fail-closed parse gate).
  • Rust preview frontend behind wardline[rust] — Tier-A command-injection slice
    (RS-WL-108/RS-WL-112), pre-built tree-sitter abi3 wheels (no compiler).
  • Agent/MCP surfacewardline mcp (dep-free stdlib JSON-RPC), structured tool
    output on all tools, doctor/rekey/assure/attest/dossier/finding-lifecycle
    tools, pollable file-backed scan jobs.
  • Weft federation — config in weft.toml [wardline], state in .weft/wardline/;
    enrich-only emit to Filigree (native emitter) and Loomweave taint store; SEI-native
    addressing across the federation.
  • Release hygiene — all rc references dropped from version/changelog/docs;
    Development Status :: 5 - Production/Stable.

Release mechanics

  • __version__ = 1.0.1; wheel + sdist build clean as wardline-1.0.1.
  • CHANGELOG [1.0.1] - 2026-06-15 (rc1/rc2 sections folded in, no content lost).
  • Full suite 4001 passed; ruff + mypy clean.
  • Activation path documented (README, docs index extras table, CLI ref, rust-preview
    guide, agents guide): pip install 'wardline[rust]'.

Held for the human owner (post-merge, with the set)

  • v1.0.1 git tag and PyPI publish (Trusted Publishing) — the irreversible steps.

🤖 Generated with Claude Code

John Morrissey and others added 30 commits June 7, 2026 19:26
Stop tracking per-clone tooling state (.filigree.conf, loomweave-workflow
skill fingerprints/SKILL.md) and add ignore rules (incl. .weft/). Files
remain on disk; only removed from the index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wardline doctor gains a filigree.auth check: probe the configured daemon
(URL sourced from .mcp.json) with the token wardline would emit, detect
401/403 token-mismatch, and under --repair recover the accepted token from
local mints and pin it as WEFT_FEDERATION_TOKEN in .env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7-task TDD plan: verify_token probe, .env rewriter, probe-URL resolution
(flag>env>.mcp.json arg; no published-port rung), detection (probe-always;
absent token only errors on a real 401), repair-from-local-mints, CLI wiring.
Reconciles the spec with two planning refinements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A read-only probe that POSTs a sentinel body ({}) so the daemon's auth
middleware runs before body validation: a good token yields 400 (nothing
recorded), 401/403 means rejected. Returns ProbeResult(reachable, accepted,
status). Does not reuse emit() (which would register an empty scan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… on repair

merge_mcp_entry normalized the wardline .mcp.json entry to the canonical local
form, stripping a fixed-port/server-mode --filigree-url the published-port rung
cannot reconstruct -- silently disabling runtime emit. Preserve those flag pairs
in the operator's original order (idempotent); _check_project_mcp accepts them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wardline doctor probes the configured Filigree daemon (URL resolved flag > env
> .mcp.json --filigree-url arg; loopback-only via strict ipaddress parsing) with
the token wardline would emit. Reports filigree.auth; under --repair, recovers
the daemon-accepted token from local mints and pins it as WEFT_FEDERATION_TOKEN
in .env (surgical, byte-preserving, 0600 from creation). Distinguishes token
absent from token-present-but-rejected. Wired into machine_readable_doctor and
the CLI (--filigree-url flag, human + JSON + --repair surfaces).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…7 token msg

Resolve the four wardline-owned Dogfood-2 federation tickets (weft tracker):

- W1 (weft-439d09fc8d): MCP `scan` default is now BOUNDED — agent_summary is the
  single finding carrier (top-level `findings` array dropped), default page ≤25
  bodies, `full=true` lifts the cap, `offset` paginates via
  agent_summary.truncation.next_offset; `explain=true` inlines provenance into the
  active_defects entries (capped/announced). Kills the ~123KB single-line dump.
- W2 (weft-b937e53854): GateDecision gains `verdict`
  (NOT_EVALUATED/PASSED/FAILED) + `would_trip_at`; a bare scan (no --fail-on) is
  NOT_EVALUATED, never a vacuous green. Invariants re-keyed (not removed) so a
  tripped gate still cannot read as passed. Threaded to all 4 gate surfaces.
- W3 (weft-f506e5f845): per-finding key suppressed -> suppression_state
  (to_jsonl, filigree metadata, legis artifact); summary gains `informational`
  so active+baselined+waived+judged+informational==total (unanalyzed = overlay).
  Identity corpus + grammar golden regenerated (rename-only, fingerprints
  unchanged). legis-side ingest adoption tracked in weft-ef79348eb2; the signed
  legis golden was rekeyed and legis_e2e stays red until legis adopts the key.
- C-7 (weft-23574069a1/F1): filigree emit distinguishes no-token-sent from
  token-sent-but-rejected (401) and names the URL it tried.

Suite 2589 green; ruff/mypy/mkdocs --strict clean; live-verified against a
freshly spawned `wardline mcp`. Glossary (the vocabulary SoT) updated for the
rename + new informational/verdict/would_trip_at vocabulary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (N1 / C-10a)

Every Filigree emit status block (MCP scan, agent-summary, CLI) now carries a
destination {url, project, project_pinned}, and the CLI success line names the
destination project. An unpinned URL reports project_pinned:false — surfacing
that Filigree resolves the project server-side (the silent-misroute shape behind
the lacuna->filigree contamination) so a misroute no longer reads as success.
Pairs with filigree's fail-closed (weft-7a399b8124); filigree's response carries
no project echo yet, so the cross-check half remains gated there.

Reconciled the finding-lifecycle glossary anchors shifted by the scan.py/server.py
insertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…906d19b29a)

block.py was the only non-conforming writer of the weft C-4 multi-owner
managed-block contract: in a shared CLAUDE.md/AGENTS.md co-resident with a
sibling tool's block (e.g. filigree in ~/lacuna) it could swallow a foreign
block, delete an own duplicate sitting beyond a foreign block, or truncate the
shared doc on a mid-write crash.

Mirrors the legis/filigree reference algorithm:
 - _INSTR_FENCE_RE: generic namespaced-fence detector (open/close), foreign =
   ns.lower() != "wardline", case-insensitive per (h).
 - _first_own_open_fence_pos walks fences tracking the enclosing foreign block,
   so an own marker quoted inside a sibling block is never claimed (foreign-safe).
 - Three explicit branches: own_end==-1 -> append-preserve (d); own_end<foreign
   -> well-formed replace + canonicalise own dups up to the first foreign fence
   (e); own_end>=foreign -> bounded recovery, cut at foreign (c). Never spans a
   foreign fence.
 - _canonicalise_tail collapses own dups only before the next foreign fence;
   an own duplicate beyond a foreign block is preserved and surfaced (warning).
 - All writes routed through _atomic_write_text (mkstemp + os.replace, mode
   preserved) with a refuse-to-empty guard (g).

Goes one better than the references: a convergence guard in the append branch
declines to re-append when the exact current block already exists but is
shielded inside an unclosed foreign block — the references grow the file
unboundedly across repeated SessionStart hook runs on such input; wardline
converges. Foreign-safety preserved (the guard is read-only).

Preserves (b) namespaced match, (d) append-on-missing-end, idempotent
convergence. 11 new tests cover (c)/(d)/(e)/(f)/(g)/(h) + convergence; full
suite 2604 green, ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review panel (6 reviewers) against the C-4 contract surfaced two
real, reachable defects in the prior commit:

HIGH (b,c): a foreign marker merely QUOTED inside wardline's own block body was
mistaken for a real foreign boundary by _first_foreign_fence_pos (no balance
tracking), forcing spurious bounded recovery — the rewrite truncated at the
quoted marker, leaving a dangling own close + a stray foreign open that read as
a real foreign block on the next run (sticky corruption). Replaced with
_first_real_foreign_block_pos: a foreign OPEN counts as a boundary only when it
has a matching foreign CLOSE (a genuine sibling block). A lone quoted marker is
our own content -> the block is replaced in place; a genuinely nested balanced
foreign block still triggers bounded recovery and is preserved (NOT swallowed).

MEDIUM (e,h): _FENCE_RE was case-sensitive while boundary detection compares
case-insensitively, so an uppercase-namespaced own duplicate (WARDLINE) was
neither collapsed nor surfaced. Added re.IGNORECASE so own-duplicate
canonicalisation matches the case-insensitive namespace rule.

+4 tests (quoted-foreign-in-body replace-in-place + converge; genuinely nested
foreign preserved; uppercase own dup canonicalised). Full suite 2607 green,
ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mit path (F1, weft-23574069a1)

Closes the last rung of C-3: wardline's outbound token resolver now reads
filigree's auto-minted <root>/.weft/filigree/federation_token, so a fresh
same-host install with NO env/.env/.mcp.json ceremony authenticates against the
per-project filigree daemon.

load_filigree_token resolution order (mirrors filigree/federation_token.py):
  1. env WEFT_FEDERATION_TOKEN                      (operator override / cross-host)
  2. <root>/.env WEFT_FEDERATION_TOKEN             (wardline's portable convention)
  3. NEW <root>/.weft/filigree/federation_token    (filigree's auto-minted 0600 file)
  4. legacy WARDLINE_FILIGREE_TOKEN (env then .env) (deprecated transition fallback)
  5. None                                           (auth off)

Rung 3 is the same project-store file filigree mints and validates against (C-9e
same-host cross-member read; conventions.md C-3 + conflict-register §A-15). It is
read-only — wardline never mints it — and a missing/unreadable/empty file falls
through cleanly to legacy/off, so emit stays soft-fail (never hard-fails the
scan). This token is loopback deconfliction plumbing, not a secret: 0600 is
filigree's to set; no client-side hardening added (per spec).

The dossier_client Bearer attach and the 401-vs-unreachable message are
unchanged. Tier-1/2 env precedence preserved (env still overrides the file).

Tests: 7 new resolver cases (mint read; env overrides mint; .env overrides mint;
mint beats legacy; missing/empty/unreadable fall-through). Updated one doctor
repair test whose premise (stale legacy token needing repair to the project mint)
is now resolved directly by rung 3 — it drives the repair iteration via a stale
CANONICAL token, which still outranks the mint. Full suite 2614 green; ruff +
mypy clean. Live-verified: zero-ceremony emit against the per-project daemon
returns auth_rejected=False (created=1); no-token returns 401.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(wardline-383f83fafe)

The variable-level taint engine tracked lambda bindings in a single-slot
`dict[str, ast.Lambda]`. At a branch merge `_merge_branch_bindings` kept only
the last-arm-in-source-order binding per name, so a sink-lambda bound in a
NON-last arm was overwritten by a later arm's benign rebinding — and a tainted
call against that name after the branch resolved the wrong body, missing the
taint→sink flow (a false negative).

Replace the single slot with a candidate-set model `dict[str, list[ast.Lambda]]`:
- linear rebind REPLACES the candidate set (never accumulates);
- `_branch_copy` deep-copies the per-arm lists (no cross-arm aliasing);
- `_merge_branch_bindings` rebuilds the parent as the UNION over arms
  (dedup by identity), which subsumes the wardline-36016d26f3 FN-closing guard
  (an untouched fall-through arm can no longer revert a sibling's rebinding) and
  drops a name only when EVERY arm rebound it to a non-lambda (sound);
- `_resolve_call` resolves a call against EVERY candidate body.

Resolving an extra body only records arg-taints of that body's own distinct
sink-call nodes, so the union is FP-safe (over-approximate, never mask a sink).
Loops keep the in-place replace path; rebinds reuse the same AST node each
iteration and the merge dedups by identity, so the candidate set cannot grow
unboundedly (documented at the loop fixpoint).

Tests: sink-lambda in a non-last arm now flags PY-WL-107 across if/else,
try/except, match (unit + e2e); plus FP guards (clean-arg post-branch,
replace-after-branch, all-arms-removed drop, benign-rebind-after-loop) and
coverage for elif-middle and loop-body shapes. Suite 2625 green; ruff/mypy
clean; identity golden corpus + gate unchanged.

A pre-existing, orthogonal loop zero-trip FN (a name rebound INSIDE a loop
loses its pre-loop binding) is filed separately as wardline-d6af917bde; this
change neither introduces nor fixes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…83f83fafe)

User-visible under --fail-on: closing the false negative surfaces new findings
on code where a sink-lambda is bound in a non-last if/try/match arm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oundLocalError)

_wait_for_capabilities referenced `url` in its timeout RuntimeError, but `url`
is assigned only inside the poll loop once a bind address is parsed. When
loomweave spawns yet never reports an HTTP bind address within 20s, the loop
times out with `url` unbound, so the diagnostic raise itself crashed with
UnboundLocalError — masking the real "did not become ready" reason and turning
five clean skips into collection ERRORS. Initialize `url` to a placeholder so
the timeout path reports its diagnostic and the fixture skips cleanly.

Surfaced by the pre-1.0 scrub's live-oracle pass (loomweave_e2e: 5 ERROR -> 5 skip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…W3 residual)

The agent_summary pagination union was active_defects + suppressed_defects +
engine_facts, but engine_facts filters Kind.FACT AND rule_id WLN-ENGINE-*, so
non-defect findings of other kinds (METRIC / CLASSIFICATION / SUGGESTION, and
non-engine FACT) were counted in summary.informational yet occupied no slot in
any display array. An agent paginating offset → next_offset believed it had
covered everything while never seeing those findings, and truncation.findings_total
did not equal summary.total_findings.

The earlier W3 fix (weft-f506e5f845) added the informational summary count but
left this display-layer remainder open. Fix it additively: keep engine_facts as
engine facts only (via a shared _is_engine_fact predicate so the post-pagination
re-split can't double-count a non-engine FACT) and add a new `informational`
display bucket for the complement. The four arrays now partition the union
exactly: active_defects + suppressed_findings + engine_facts + informational ==
findings_total == total_findings. No existing output keys renamed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Incidental, pre-existing format drift normalized by a repo-wide `ruff format`
run (collapses implicit multi-line string literals that fit on one line).
Unrelated to the agent_summary change; split out so that commit stays clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All six empirically reproduced + regression-tested. Adds a shared fail-closed
argument resolver (`_sink_helpers.resolved_arg_taints`) as the single
fail-closed implementation; `worst_arg_taint` is now a thin selector over it.

- PY-WL-118 FP: gate SQLi on the SQL-string position only, ignore bound
  parameters (a tainted SQL string still fires; fail-closed on splat).
  (wardline-e0e44852e7)
- PY-WL-118 FN: apply the family-wide `.<locals>.` tier-strip so a tainted
  execute() in a nested def inherits its trusted tier. (wardline-9b88ec5419)
- PY-WL-105 FN: fire when ANY arg is provably untrusted (the predicate is not
  upward-closed, so worst-arg let an UNKNOWN_RAW co-arg mask it).
  (wardline-836dcef5b4)
- PY-WL-114 FN+FP: gate on the resolved builtin decorator FQN (via the alias
  map + builtin-prefix check, mirroring PY-WL-110), not the trailing
  identifier — fixes aliased-builtin miss and foreign-same-name fire.
  (wardline-0267c31cd8)
- Engine FN: clear a stale `var_types` entry on every imprecisely-typed
  reassignment so a raw receiver can't launder past the RAW_ZONE guard
  (soundness). (wardline-5ba7ce0f98)
- Engine FN: bind attest.ruleset_hash(config) into the summary cache key so a
  policy change (untrusted_sources/sanitisers/provenance_clash) can't serve a
  stale-CLEAN summary; one policy identity shared with attest.
  (wardline-9d6a81b9e7)

Suite 2640 green; ruff/mypy/format clean; src/ gate exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ough analysis

_can_fall_through modeled ast.If/Try/Match as potentially-terminal but had
no case for ast.With/ast.AsyncWith (transparent — terminal iff body terminal)
nor for a constant-true `while True` loop with no break (always terminal).
Both fell through to the final `return True`, falsely setting has_none=True
and firing PY-WL-109 on trusted producers that provably cannot return None.

Adds both cases plus a scope-aware _has_loop_break helper that prunes nested
loop bodies and def/class/lambda scopes, and recurses into a nested loop's
orelse (where a break binds to the enclosing loop). The change is monotonic —
it only ever makes the rule fire less, so it cannot introduce a new FP.

Closes wardline-786a4ec647.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Program-level design for a second (Rust) language frontend that scans .rs
for trust-boundary/taint findings, reusing the lattice/grammar/L3/cache/Finding
contract. Decision (user): interim Python tree-sitter frontend on today's engine,
re-ported at the native-core cutover.

Grounded in a 6-agent seam recon: tier-A/B/C semantic ceiling, the id(ast_node)
cross-stage correlation hazard (typed NodeId fix), the dotted Rust qualname
dialect, verified tree-sitter pins (ABI-15 floor), and the two command-injection
findings (RS-WL-108 tainted-program / RS-WL-112 shell-injection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-1 adversarial panel (reality/design/architecture/systems/quality/
rule-designer/security). Citation fixes (FunctionSeed->function_level.py,
NodeId mint->callgraph.build_call_edges, cache-key 6th input scan_policy_hash,
_grammar_digest co_code|co_consts, cp39-abi3 wheel). Resolved deferred
decisions: RS-WL-108=ERROR (new threat class, not a PY-WL-108 port);
module root crate-rooted; CLI explicit --lang rust; identity provisional/
baseline-ineligible until SP2 (dissolves the provisional-vs-frozen contradiction).
Added: RustAnalyzer satisfies the full Analyzer protocol (run_scan wiring);
NodeIdMap threading + cross-pass agreement test; local_string_taints for
format!-through-let; format! FP/FN both directions; 108/112 de-confliction +
forward-guard; Windows case-fold + pwsh; arg0/concat/FFI/args-flag FNs;
coverage-posture disclosure; dense corpus + clean-fixture hard gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the P2 tier of the 2026-06-08 correctness scrub (scrub-2026-06-08).
Five fixes make the engine fire MORE (4 FNs + 1 soundness + 1 coverage), so each
carries a CONTROL (near-identical safe shape that must stay silent) alongside its
repro; one is fire-less. Hardened across three adversarial review panels — the
flow-sensitive candidate pass below was rewritten after panel-1 found the first
implementation was flow-INSENSITIVE, and its loop backstop was re-keyed after
panel-3 found it bounded by the wrong dimension.

- PY-WL-113 FN (wardline-c314a7140b): handler_substitutes_on_failure now also
  matches an in-handler ASSIGNMENT of a non-falsy value to a name the function
  returns by fall-through (new returned_var_names). Gated on the handler having no
  UNCONDITIONAL top-level return (a conditional nested return does not stop
  fall-through) + excludes idempotent self-assignment.
- PY-WL-110 FP (wardline-09c09f14df, fire-less): _marker_canonical_name now keys on
  the engine's exact-export predicate _is_builtin_decorator_fqn instead of a loose
  names+prefix heuristic, so a nested path the engine never seeds is no longer
  counted as a contradictory-marker clash.
- Engine soundness FN (wardline-e04db6e656): _handle_for/_handle_while replace the
  range(8) fixpoint cap with iterate-to-convergence + a num_vars*lattice_height
  backstop — loop-carried rebind chains deeper than 8 links now propagate. AUDIT.md
  WLN-MED-09 rationale corrected (propagation depth != lattice height).
- Engine FN (wardline-499c22bbdd): callgraph resolves a branch-conditional receiver
  to the SET of candidate callees via a new flow-sensitive reaching-definitions pass
  (_candidate_receiver_classes: branch arms unioned at joins, straight-line
  reassignment REPLACES, loop-carried fixpoint, walrus REPLACE) — the single source
  for var-type dispatch, replacing the AST-order-dependent flat last-write-wins.
  Additive call_site_candidate_callees threaded resolver->metadata->context->analyzer;
  PY-WL-105 and PY-WL-120 fire on any anchored trusted-sink candidate regardless of
  AST order and emit ONE finding per call site.
- PY-WL-120 dead branch (wardline-e7c7cda31a): seed cursor.fetch{one,all,many}()
  EXTERNAL_RAW (curated _STORAGE_READ_METHODS) so DB-cursor fetches fire like file
  reads already do; bare .read NOT seeded (FP explosion). _RESOLVER_VERSION bumped
  sp1d->sp1e to invalidate stale-CLEAN summary caches across the engine changes.
- Engine FN (wardline-67c7498931): model container mutators
  {append,add,extend,update,insert} — a tainted arg writes its taint back onto the
  receiver; list.insert's index arg is excluded (position metadata, not content).

Suite 2675 green, ruff + mypy clean, src/ self-scan clean. 18 regression tests
added (repro + control per fix, incl. all panel-found FP shapes as silent controls).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n set

Surfaced by adversarial verification of the six P1 scrub fixes (24b0a3e); all
three empirically reproduced and regression-tested (9 new tests; suite 2688
green, ruff/mypy/format clean, src/ gate exit 0). A 5-lens adversarial panel
(FP-preservation, kwargs edge-space, nesting, family-wide blast radius, lambda)
otherwise confirmed both regression fixes sound — bound-parameter FP preservation
(e0e44852e7) holds across every shape, and the family-wide tier walk is correct.

- **`**kwargs` dict-unpacking FN** (wardline-8c31463f9f). The engine collapses a
  `**` unpack to one taint under the `None` arg-key, which the narrowed
  `_SQL_STRING_KEYS` gate ignored. `_sql_string_taint` now treats `None` as the
  SQL-string slot when a `**` could supply the operation, inspecting literal-dict
  keys (`**{"operation": ...}` fires; `**{"parameters": ...}` stays silent,
  preserving e0e44852e7) and failing closed on any opaque/non-static `**`. The
  per-`**`-key taint collapse means a literal dict mixing a clean operation with a
  tainted parameter over-approximates (fires) — a deliberate fail-closed choice,
  never an FN.

- **Nested-def own-decorator FN, family-wide** (wardline-bb8396f96e). The
  unconditional `.<locals>.` strip made a nested `@trusted` def inherit its
  parent's (suppressed) tier. New shared `_sink_helpers.enclosing_declared_tier`
  walks outward and uses the nearest scope carrying an explicit declaration
  (`declared_qualnames`), so a nested def's own decorator governs while a
  genuinely undeclared nested def still inherits its enclosing trusted tier
  (preserving wardline-9b88ec5419). Applied to PY-WL-118 and the shared
  TaintedSinkRule base (PY-WL-106/107/108/115/116/117).

- **Lambda-body sinks FN** (wardline-b8a94cf0ac). PY-WL-118 walked `own_nodes`,
  which treats `ast.Lambda` as a scope boundary, so a tainted `execute()` in a
  lambda escaped while its sink-family siblings descend into lambdas. It now uses
  the shared lambda-descending `sink_method_calls`, attributing the finding to the
  enclosing entity as the siblings do.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… dialect FIXED)

Loomweave fixed the Rust entity-ID dialect (ADR-049) and published an
extractor-generated corpus. Wardline is the SECOND PRODUCER: it mints the
identical locator string and never parses it (ADR-003/038 opacity).

Adopt the normative forms (replacing my proposal):
- trait impl  Foo.impl[Display].fmt  (concrete generics kept: impl[From<i32>])
- inherent    Foo.impl#<>#0.bar      (positional $0 generics, per-module ordinal)
- cfg twins   f@cfg(unix) / S@cfg(windows)  (all item kinds)
- closures + nested fns are NOT entities  (dropped .<locals>.{closure#N};
  attribute findings to the enclosing named fn)
- id-kind is always "function"; ":" is reserved/invalid (killed :trait=/:setter)

Corpus INVERTED: Loomweave hosts fixtures/qualnames_rust.json (oracle), Wardline
vendors + reproduces byte-for-byte under the documented comparison rule
(qualname-only on function rows). Reproducibility tiers: slice-1 = file-module
suffix; SP2 = crate prefix + cross-file route + path-attr. R-2 downgraded to Med.
SEI fold resolved: keep folding the qualname, never Loomweave's SEI token.
Blocker (was Q1) DROPPED. Reply committed under docs/integration/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w + over-narrows tier

The rule's METADATA.description (and module docstring + docs table) enumerated
an exhaustive-looking swallow set 'only pass/.../continue/break', but
is_silent_handler also treats a bare constant expression (string literal or
number) as a silent swallow via _is_ellipsis_or_constant — the most insidious
case (a docstring-lookalike string). It also said 'in a trusted-tier function'
though modulate() fires it at INFO on partial (GUARDED/UNKNOWN_*) tiers too.

Doc-only fix across the 3 sites; no engine/behaviour change. Closes wardline-a444d607d0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (drift-gate live)

Take up Loomweave's offer: vendor the cross-tool Rust qualname conformance corpus
and add the parity gate now, before slice-1 implementation, so dialect drift is
caught from the first commit.

- tests/conformance/qualnames_rust.json: byte-identical copy of
  loomweave feat/rust-plugin-spec@a3227ad:fixtures/qualnames_rust.json
  (extractor-generated oracle; Loomweave is authoritative for the Rust dialect).
- tests/conformance/test_loomweave_rust_qualname_parity.py: 4 structural self-tests
  run NOW (catch a malformed/stale re-vendor); 22 producer-parity rows skip until
  the Rust frontend lands. Comparison uses function-qualname set-equality per the
  corpus _consumer_comparison contract (handles the zero-callable struct_cfg_twin /
  macro cases; non-vacuous across the 14 function-bearing cases). sp2 rows xfail.
  Dynamic importlib resolution so mypy does not statically chase the not-yet-real
  wardline.rust module.
- plan WP0/WP2/Verification: mark the vendor DONE; pin the producer API the test
  calls (discover_rust_entities(source, *, module) / rust_module_route(*, crate,
  src_root, file)); make "un-skip the 22 producer rows -> GREEN" a WP2 done-criterion.

Verified: ruff clean, mypy Success (362 files), pytest 4 passed / 22 skipped,
conformance dir 58 passed / 22 skipped (no regressions).

4-agent ultracode verification panel: fidelity PASS (byte-identical, SHA match),
contract PASS (set-equality correct + non-vacuous), run PASS-with-fixes (mypy +
ruff-format, now fixed), reality PASS-with-fixes (WP2 API pin, now folded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ying authority

Slice-1 WP0 ("slice 0"): the dependency + scaffold + the NodeId/NodeIdMap keying
authority the rest of the Rust frontend builds on. No engine code is touched
(the identity oracle stays byte-green); base stays zero-dependency.

- pyproject: opt-in `rust` extra (tree-sitter>=0.25,<0.26 + tree-sitter-rust
  ==0.24.2; ABI-15 pin rationale in-line — the grammar's self-declared
  `tree-sitter~=0.22` is stale).
- core/node_id.py: `NodeId` NewType in a neutral module — the shared spec-§5
  contract. Annotating the id()-keyed callgraph maps on the Python side is
  deferred to SP1: NewType is invariant, so annotating the write sites alone
  would cascade type errors through 10+ readers (Mapping[int, str]).
- rust/_tree_sitter.py: lazy `require_rust()` loader + `RustToolingError`,
  mirroring loomweave.require_blake3 — importing wardline.rust never pulls
  tree-sitter (verified: absent from sys.modules after import).
- rust/nodeid.py: `NodeIdMap` + `mint_node_ids` — a deterministic 0-based
  pre-order index over the full CST, keyed on the per-parse-stable node.id. The
  map PINS its source Tree, so tree-sitter cannot reuse a freed node.id pointer
  out from under it: a map outliving its tree raises KeyError, never a silent
  wrong NodeId (the spec-§5 fail-quiet hazard).
- tests: TDD, importorskip-first (proven skip-clean under the extra-free venv);
  ordering + cross-parse determinism pinned against an INDEPENDENT TreeCursor
  oracle (not a clone of mint's own walk); freed-tree false-hit regression;
  empty / malformed-ERROR / deeper-than-recursion-limit edges.

Built TDD then hardened by a 4-lens adversarial panel; the Tree-pin and the
circular-test fixes are panel findings, empirically reproduced before fixing.
The vendored Loomweave qualname corpus + parity gate (the other half of WP0)
landed earlier in 043adf9; its 22 producer rows un-skip at WP2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olution drift

The `fingerprint` is the cross-tool JOIN KEY into the baseline/waiver/judged
stores and the Filigree tracker. It folded engine-RESOLUTION outputs (resolved
TaintState tiers and via_callee) into its `taint_path`, so it moved across
builds for byte-identical source as the rule suite was extended — a baselined
lacuna escaped its baseline, tripped the gate, and minted a federation
duplicate. Empirically reproduced: PY-WL-101 on the lacuna specimen keyed on
"UNKNOWN_RAW->ASSURED|" (baseline fc1bc079) then "EXTERNAL_RAW->ASSURED|"
(8f68f390) for unchanged source — the resolved actual-return tier drifted.

Strip every resolved-tier/via_callee component from taint_path. Rules emitting
<=1 finding per (rule_id,path,line_start,qualname) pass taint_path=None;
call-site-anchored rules (PY-WL-105/106/108/115/116/117/118/120) discriminate
by the source-only sink/callee spelling plus the call's full lexical span
(col_offset:end_col_offset) — collision-free even for chained calls
(`a.execute(x).execute(y)`), which share a start column under CPython's anchor.
PY-WL-114 is unchanged (name:token is already source-derived AND load-bearing).

The invariant is documented at compute_finding_fingerprint and in the
frozen-contract ADR, and enforced by:
- an identity-corpus collision gate (distinct-fp-count == active-finding-count)
  exercised by a new `sinks` fixture with same-line and chained sinks;
- a PY-WL-101 resolved-tier-swap invariance unit test reproducing the bug.

Intentional fingerprint rekey: identity corpus corpus_version 1->2, grammar
golden rekeyed (only fingerprints change; finding set identical). Migration is
one-time and documented in CHANGELOG — it stabilises a key that was already
drifting every build. Full suite 2695 green, ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…RAW for DB-cursor reads

Investigated the cur.fetch*() tier-classification observation: NOT a bug. The
storage-read seed (variable_level._resolve_call._STORAGE_READ_METHODS) does an
unconditional `return EXTERNAL_RAW` (does not join receiver taint), so a @trusted
producer returning cursor.fetch{one,all,many}() resolves actual_return to
EXTERNAL_RAW deterministically across receiver shapes and runs. The historical
UNKNOWN_RAW difference was the wardline-e7c7cda31a dead-branch fix landing, not
ongoing drift; the join key is already immune (weft-4a9d0f863c).

End-to-end coverage previously stopped at PY-WL-120 firing (variable-level seed)
and at the @external_boundary call path for actual_return; this pins the
PY-WL-101 actual_return tier on the fetch* path so the observation cannot recur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
John Morrissey and others added 9 commits June 14, 2026 12:14
Consumes @weft/site-kit by sparse-fetching packages/site-kit from the weft repo into a gitignored vendor/site-kit/ (file: dep); the deploy workflow does the same before build. Content sourced from this repo's docs; roster/matrix/cross-links from kit data. Light theme, astro build passes. CNAME + Pages enablement are operator steps. Teaches the honesty mechanic (silent until you decorate; absent != clean).

NOTE: site/ was the mkdocs default build dir; redirected mkdocs site_dir to
mkdocs-build/ and updated .gitignore so site/ is committable Astro source. Legacy
www/ deploy + mkdocs both targeted this subdomain — retire them at launch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mkdocs.yml)

Superseded by the Astro site (deploy-site.yml). docs/ and www/ stay as reference/
content; the combined www-root + /docs/ gh-pages deploy that claimed
wardline.foundryside.dev is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Developer/runtime config is not part of the solution: ignore and drop the
committed copies of .claude/, .agents/, .weft/, and (where present)
.mcp.json / loomweave.yaml / weft.toml. Toolchain/lint/hook configs and
test fixtures stay tracked — they are code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tracking

PDR-0023 honesty invariant, strike B. On the wardline->filigree emit seam the
"failed" field was a bare integer count: a PARTIAL ingest (Filigree rejected K
findings) was byte-indistinguishable from a clean emit minus a silent number,
and the per-finding reject reasons Filigree returned were discarded. A
confident, low-cardinality failed:0 read as a true-negative.

Implement real per-finding failure tracking. EmitResult now carries a
failures tuple of FailedFinding records, each with a machine-readable reason
(rejected / validation_error / scheme_mismatch / partial), a detail string,
and the wardline join-key fingerprint. The scalar "failed" becomes a property
derived from len(failures), so a hardwired failed=0 while findings failed is
now unrepresentable -- the same construction-time-consistency idiom as
auth_rejected over status. A clean run still reports failures==()/failed==0,
but that emptiness is earned from real records, not assumed.

The 2xx parser preserves Filigree's per-finding reject reasons (normalizing an
unknown/absent reason to a loud "rejected" rather than dropping it, and
recognizing fingerprint-scheme drift as scheme_mismatch -- the join-miss the
seam-health map flags). The fail-soft protocol-reject path records each still-
pending finding as a "partial" failure carrying the status, instead of adding
an opaque remaining-count. The new failures array is surfaced through the MCP
raw/normalized emit blocks, the CLI status, the scan-job and scan-file status
builders, and the agent-summary, with output-schema entries and a shared
$def. An agent can now distinguish "all N emitted" from "M of N emitted, K
failed because R".

Tests: partial-ingest golden vector (per-finding reasons + fingerprints),
earned-empty clean run, fail-soft chunk-reject -> partial records, count-
derived-from-failures invariant, unknown-reason rejection, and an MCP-surface
partial-ingest assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…urface

_resolve_probe_url deliberately excluded the published-port rung, so a project
with a live ephemeral/per-project Filigree daemon (publishing .weft/filigree/
ephemeral.port) but no pinned --filigree-url resolved to None -> "filigree not
configured; nothing to verify". But the emit path (resolve_filigree_url) DOES
auto-discover that port, so a scan emits there and needs a valid token. Doctor
was blind to the exact 401-token gap it exists to catch.

Align the probe with the emit target: fall back to _filigree_published_url after
the .mcp.json arg (flag > env > mcp-arg > published port, mirroring the emit
resolver). Detection is now automatic; --repair's token recovery handles the fix.
Loopback guard + bound-daemon-implies-port keep it read-only and leak-safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ason vocabulary (G1)

Map the shipped per-finding emit-failure reason {rejected, validation_error,
scheme_mismatch, partial} onto the closed 11-class weft-reason vocabulary
(contracts/weft-reason-vocab.json) ADDITIVELY: the on-wire reason/detail fields are
preserved verbatim, and each FailedFinding now also carries the canonical carrier triple
reason_class + cause + fix. validation_error (the only non-canonical domain term) maps to
the canonical rejected (peer reached, refused the malformed body); the domain term survives
in reason/cause. rejected/scheme_mismatch/partial map to their canonical namesakes.

Carrier rule holds: a FailedFinding is always non-clean, so it always carries
reason_class+cause+fix (fix mandatory, cause falls back to the domain reason). MCP emit
output schema (.filigree_emit_failures and the inline scan block) declares the three
new keys so structured output stays valid.

Add tests/conformance/test_weft_reason_vocab_conformance.py: introspects the member's actual
reason vocabulary and asserts the emitted reason_class set is a SUBSET of the canonical 11,
that every shipped reason declares a mapping and a fix, and that the wire carries the carrier
triple on every non-clean failure. Fails on drift (a new non-canonical reason_class, or a
dropped cause/fix), and pins the vendored canonical set to the hub contract when present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ine)

A hand-filed waiver names a specific defect-in-a-function but wrote to
waivers.yaml keyed only by fingerprint, with zero rename-surviving entity
identity — off the spine, invisible to federation joins. Add an optional
inline entity binding, additively and non-breaking:

- entity_id (L1): an opaque SEI/locator the caller already holds, carried
  verbatim, no transport touched.
- entity_symbol (L2): a qualname resolved to a rename-stable SEI through
  Loomweave at entry time via the existing SeiResolver transport.

A supplied entity_symbol that does not resolve returns an unresolved_input
weft-reason carrier {reason_class, cause, fix} and writes NOTHING — never an
unbound-but-looks-bound record. Waiver model + parse/serialize gain optional
entity_sei / entity_locator fields (absent when unused; existing waiver files
and callers unaffected). New resolve_entity_binding_input helper centralises
the L1/L2 + honesty logic.

Tests: core round-trip + malformed-binding rejection; resolver L1/L2/
unresolved paths; MCP no-entity / L1 / L2-resolves / L2-unresolved-writes-
nothing / existing-reports-binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1.0.0 is already on PyPI (a partial publish from before the mid-project
reboot), so the first complete reboot release ships as 1.0.1.

- _version.py: 1.0.0rc4 -> 1.0.1; pyproject Development Status Alpha -> 5/Stable.
- CHANGELOG: [Unreleased] -> [1.0.1] - 2026-06-15; fold the 1.0.0rc1/rc2
  pre-release sections into the final entry (no content lost); compare link
  v0.3.0...v1.0.1 (no v1.0.0 tag exists in-repo). No rc strings remain in any
  shipped surface.
- docs: version displays in getting-started + CLI reference, and the
  legis-handoff example scanner_identity, now read 1.0.1.
- test_package: pin the 1.0.x line (startswith "1.0.") instead of an exact
  patch, so future point releases don't break it.
- doc-discipline tests (pre-existing red, unrelated to the release cut): retire
  test_glossary_in_nav (mkdocs was dropped in 192462e; docs/ is now agent-first
  reference content with no strict nav to orphan from) and re-sync the
  glossary<->code file:line anchors that drifted when the scan-jobs commit grew
  server.py/agent_summary.py. Full suite 4001 green; wheel builds as 1.0.1.

The legis golden HMAC vector (test_legis_artifact.py) keeps wardline@1.0.0rc1 —
it is frozen signed input the golden hex pins and legis co-signs; changing it
would break the cross-tool contract, not a version display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI on the rc5->main PR surfaced pre-existing red that tree-wide `ruff` and the
3.12 runner caught but local per-file checks did not:

- ruff check: 3 E501 long lines in the FailedFinding `fix`-text surface
  (filigree_emit.py + its two tests). Wrapped with value-preserving implicit
  string concatenation — the runtime strings (which the tests assert on) are
  byte-identical.
- ruff format --check: 8 files had format drift (committed unformatted before
  the tree-wide gate caught them). `ruff format` applied.
- Tests 3.12: test_l2_fixed_point_scales_linearly_with_function_count flaked on
  a wall-clock backstop (3200-fn scan clocked 4.05s vs a tight `< 4.0`; 3.13
  passed at the same coverage). The ratio check (`large < small*10`) is the real
  linear-scaling guard; loosened the absolute backstop to `< 8.0`, which still
  sits well below any quadratic path (~13s+ at this N) but tolerates shared-runner
  timer noise.

Full suite 4001 passed; ruff check + format --check clean; mypy clean.

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: 59b71a622e

ℹ️ 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".

- name: Record artifact hashes
run: |
cd dist
sha256sum * > SHA256SUMS

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 checksum files out of PyPI upload dir

On tag releases this writes SHA256SUMS into dist/ and the next step uploads the entire dist/ directory as the artifact that pypa/gh-action-pypi-publish later publishes; PyPA's workflow docs state that action uploads the contents of dist/ unconditionally, so the publish job will try to upload SHA256SUMS as if it were a distribution file and block the release. Put the checksum outside dist/, or delete/exclude it before the publish action runs.

Useful? React with 👍 / 👎.

Comment on lines +137 to +141
has_const = any(isinstance(leaf, ast.Constant) for leaf in leaves)
has_quote = any(_is_shlex_quote_call(leaf, alias_map) for leaf in leaves)
if not (has_const and has_quote):
return False
return all(isinstance(leaf, ast.Constant) or _is_shlex_quote_call(leaf, alias_map) for leaf in leaves)

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 Do not treat quoted command words as sanitized

When the quoted value is the first command word, e.g. os.system(shlex.quote(read_raw(p)) + ' --version'), this returns true because it only requires any constant fragment plus quoted leaves. That suppresses PY-WL-108 even though the attacker still chooses the executable name; shlex.quote prevents shell metacharacter injection, but it does not make an attacker-controlled program safe. The guard needs to distinguish constant-command-with-quoted-argument from quoted-command-with-constant-args.

Useful? React with 👍 / 👎.

Comment thread src/wardline/cli/scan.py
@@ -277,7 +352,7 @@ def confirm_cb(rel_path: str, orig: str, replacement: str, f: Finding) -> bool:
from wardline.core.agent_summary import build_agent_summary

decision = gate_decision(result, Severity(fail_on)) if fail_on is not None else gate_decision(result, 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 Pass the unanalyzed gate into agent-summary

When wardline scan --format agent-summary --fail-on-unanalyzed finds an under-scan diagnostic, this summary decision omits fail_on_unanalyzed, so the JSON file is written with a non-tripped/NOT_EVALUATED (or PASSED) gate even though the final CLI gate below exits 1 for the same run. Agents that consume the generated summary artifact will see a false gate result for the exact enforcement knob they requested.

Useful? React with 👍 / 👎.

192462e retired mkdocs (deleted mkdocs.yml + the www/ gh-pages deploy) but left
the docs-build and docs-deploy jobs in ci.yml still running `mkdocs build
--strict` / force-pushing the www+/docs/ tree to gh-pages. With mkdocs.yml gone
they fail on every PR ("Config file 'mkdocs.yml' does not exist"). The Astro site
(deploy-site.yml) supersedes both. Remove them — completing the retire-mkdocs
cleanup. CI jobs now: lint, typecheck, test, self-hosting scan/sarif, network,
live-oracles.

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: ad3fd30932

ℹ️ 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 on lines +30 to +31
"build",
"dist",

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 Do not hard-skip legitimate source directories

When a configured Python source root contains a real package named build or dist (for example src/build/api.py), this unconditional floor causes discover() to prune that subtree before any analyzer or WLN-ENGINE diagnostic can see it, producing a silent false-green under-scan. Generated-output directories should be skipped via .gitignore/config or only at the project-root level, not by every path segment name.

Useful? React with 👍 / 👎.

Comment thread src/wardline/core/scan_jobs.py Outdated
Comment on lines +264 to +267
if fmt == "agent-summary":
decision = gate_decision(result, Severity(fail_on)) if fail_on is not None else gate_decision(result, None)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(build_agent_summary(result, decision).to_dict(), sort_keys=True) + "\n")

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 Pass the unanalyzed gate into scan-job summaries

For scan-job start --format agent-summary --fail-on-unanalyzed, the worker writes the artifact using a decision that ignores request['fail_on_unanalyzed'], while the terminal job status later evaluates that knob. The result is a completed job whose status gate can be FAILED for unanalyzed files but whose findings.agent-summary.json says the gate passed or was not evaluated, which misleads agents consuming the artifact.

Useful? React with 👍 / 👎.

"fingerprint": finding.fingerprint,
"fingerprint": format_fingerprint(FINGERPRINT_SCHEME, finding.fingerprint),
"metadata": to_filigree_metadata(finding),
"language": "python",

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 Do not label Rust findings as Python

With the new --lang rust path, Filigree emission still serializes every finding with language: "python", so RS-WL findings emitted from .rs scans are stored/displayed under the wrong language. This should either be passed through from the selected frontend or derived from the finding/path before posting.

Useful? React with 👍 / 👎.

Comment thread src/wardline/install/doctor.py Outdated
kept.append(raw)
kept.append(b"WEFT_FEDERATION_TOKEN=" + value.encode("utf-8"))
payload = b"\n".join(kept) + b"\n"
fd = os.open(env_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)

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 Refuse symlinked .env during auth repair

When wardline doctor --repair reaches Filigree auth repair in a checkout where .env is a symlink, this os.open(..., O_TRUNC) follows the link and then chmods it, so an untrusted project can clobber an arbitrary user-writable file outside the repo. Use the existing project path/no-follow guard (or O_NOFOLLOW) before rewriting the token file.

Useful? React with 👍 / 👎.

Comment thread src/wardline/core/config.py Outdated
Comment on lines +341 to +343
for base in (sibling_state_dir(root, "filigree"), legacy_sibling_dir(root, "filigree")):
try:
candidates.add(base.resolve())

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 Do not trust symlinked Filigree store scopes

In server-mode discovery, a repository-controlled .weft/filigree symlink is resolved and compared against the global Filigree registry; if it points at another registered project store, resolve_filigree_url can auto-select that other project's scoped URL and later scans emit findings there. Match only an in-root, non-symlink sibling store (as the published-port path does) so a checkout cannot spoof its project scope.

Useful? React with 👍 / 👎.

boot / install / doctor). Missing or unreadable falls through cleanly to None so
the emit path degrades to the legacy/off rungs rather than crashing the scan.
"""
path = safe_project_file(root, root.joinpath(*_FILIGREE_MINT_RELPATH), label="federation_token")

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 Treat unsafe minted token paths as absent

If a checkout contains .weft/filigree/federation_token as a symlink, safe_project_file raises WardlineError before the fail-soft read block, so any scan with Filigree emission aborts instead of falling through to the lower token rungs/no-token behavior. This token source is project-local sibling state and the docstring promises missing or unreadable mints degrade to None, so use a safe regular-file read and ignore symlinks.

Useful? React with 👍 / 👎.

Comment thread src/wardline/core/rekey.py Outdated
# Atomic write: a crash mid-write must leave the OLD journal intact (or none) — never
# a truncated doc that load_journal rejects, which would brick --resume.
tmp = path.with_name(path.name + ".tmp")
tmp.write_text(yaml.safe_dump(journal_to_doc(journal), sort_keys=False, allow_unicode=True), encoding="utf-8")

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 Write rekey journals without following temp symlinks

When wardline rekey runs in an untrusted checkout, a pre-existing .weft/wardline/migration_journal.yaml.tmp symlink is not checked by the preceding safe_project_file call on the final journal path, so tmp.write_text(...) follows it and can truncate any user-writable target outside the repo before os.replace fails or replaces the link. Use the no-follow writer for the temp file as well, or create it with O_NOFOLLOW|O_EXCL inside the confined state directory.

Useful? React with 👍 / 👎.

Comment thread src/wardline/core/filigree_emit.py Outdated
Comment on lines +757 to +758
accepted = resp.status not in (401, 403)
return ProbeResult(reachable=True, accepted=accepted, status=resp.status)

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 Filigree auth only on validation responses

During wardline doctor --repair, any probe response other than 401/403 is treated as an accepted token, so a transient Filigree 500 or a wrong-route 404 can make the repair path pin the first local token candidate into .env even though auth was never verified. The sentinel {} probe should only mark acceptance on the expected post-auth validation/success statuses (for example 400/2xx) and treat other statuses as inconclusive/unreachable.

Useful? React with 👍 / 👎.

issue_id=issue_id,
filer=filer,
loomweave_client=loomweave_client,
plugin=plugin_for_finding(finding),

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 Scan Rust before attaching Rust finding identities

The new plugin switch only happens after _finding_for_fingerprint finds the finding, but that helper still performs a default Python scan; after a scan --lang rust, file_finding can promote an RS-WL fingerprint while attach_loomweave_identity reports finding fingerprint not present in current scan because the Rust frontend is never searched. Pass the originating frontend through, or try the Rust scan when the Python lookup misses, before choosing plugin_for_finding.

Useful? React with 👍 / 👎.

John Morrissey and others added 2 commits June 16, 2026 00:12
The shlex.quote concat/f-string guard (_quote_guarded_concat) accepted
`shlex.quote(raw) + " --version"` and `f"{shlex.quote(raw)} --version"` as guarded:
it only required a constant fragment plus all-leaves-quoted-or-constant. But when
the quoted leaf is the COMMAND WORD, the attacker still chooses the executable —
shlex.quote sanitizes a shell argument, not the program identity — so PY-WL-108
was suppressed on a real command injection (false negative).

Require the first non-whitespace text to come from a string constant (the leading
command word). `shlex.quote(raw) + " --version"` now fires; the blessed
`"echo " + shlex.quote(raw)` remediation stays clean; leading whitespace-only
constants are skipped so `" " + "echo " + shlex.quote(raw)` is not a false positive.
Regression tests for both concat and f-string forms; full suite 4003 green.

Found by the Codex PR review bot on #40 (P1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The build job writes SHA256SUMS into dist/ and the publish job twine-uploads the
whole dir via pypa/gh-action-pypi-publish, which uploads dist/ unconditionally —
twine rejects SHA256SUMS as an unknown distribution format and blocks the release.
Remove it after the hash verification consumes it, before the publish action.

Found by the Codex PR review bot on #40 (P1).

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: dc74594f91

ℹ️ 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 src/wardline/core/rekey.py Outdated
if dest.exists():
continue # never clobber the pre-migration snapshot
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(live.read_bytes())

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 Copy rekey snapshots without following store symlinks

When wardline rekey is run in an untrusted checkout where a live store such as .weft/wardline/baseline.yaml is a symlink to a user-readable YAML mapping outside the repo, is_file() and this read_bytes() both follow the link and copy the target into .weft/wardline/.rekey_snapshot/ before any no-follow check. That turns the snapshot step into an arbitrary file disclosure into the project; use a safe regular-file/no-follow read for the live store before snapshotting.

Useful? React with 👍 / 👎.

Comment thread src/wardline/core/scan_jobs.py Outdated
with lock:
_write_status(root, job_id, status)
fmt = str(request.get("format", "jsonl"))
_write_scan_artifact(root, output, fmt, result, str(request["fail_on"]) if request.get("fail_on") else 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 Build scan-job summaries after enrichment runs

For scan-job start --format agent-summary with a Filigree URL, this writes the summary artifact before the Filigree emit happens, and _write_scan_artifact therefore serializes the default integration block (configured: false) regardless of whether the later upload succeeds or fails. Agents consuming findings.agent-summary.json will see a false integration state while the job status has the real one; defer the agent-summary write until after enrichment or pass the emit result into it.

Useful? React with 👍 / 👎.

def _gitignored_dir(child: Path, root: Path, ignore: GitignoreMatcher) -> bool:
if not child.is_relative_to(root):
return False
return ignore.match(child.relative_to(root).as_posix(), is_dir=True)

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 nested gitignore patterns relative to their file

When a nested .gitignore contains an anchored directory pattern such as /generated/, the matcher is extended but this passes the child path relative to the project root (vendor/generated) rather than relative to the .gitignore directory (generated). Anchored nested patterns therefore never prune, so scans still walk large generated/vendor trees that the repo explicitly ignored; track each matcher’s base/prefix or match nested patterns against a path relative to their own directory.

Useful? React with 👍 / 👎.

Comment on lines +88 to +91
pid = _status_pid(status)
if pid is not None and _pid_alive(pid):
try:
os.killpg(pid, signal.SIGTERM)

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 Do not trust persisted scan-job PIDs for cancellation

When scan-job cancel is run for a job id whose status file was pre-created or modified in the checkout, the pid field is trusted as a live worker and passed directly to killpg. A malicious or stale .weft/wardline/jobs/<id>/status.json can therefore terminate an unrelated same-user process group if that pgid exists; bind cancellation to a worker this process actually spawned or validate the recorded process before signaling it.

Useful? React with 👍 / 👎.

Comment thread src/wardline/cli/scan.py Outdated
sarif_sink.write(findings, result.context)
elif fmt == "jsonl":
jsonl_sink = JsonlSink(output)
jsonl_sink = JsonlSink(output, root=path if output_is_default else 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 Keep default scan artifacts at the reported path

When PATH is a relative subdirectory, for example wardline scan pkg with the default JSONL output, output is already pkg/findings.jsonl, but passing root=path makes the safe writer resolve that relative target under pkg again and write pkg/pkg/findings.jsonl. The CLI then reports pkg/findings.jsonl even though that artifact was not created, so downstream consumers looking at the printed/default path miss the scan output; resolve the scan root/output once or pass only the default filename to the root-confined writer.

Useful? React with 👍 / 👎.

)
path.write_text(text, encoding="utf-8")
if root is not None:
safe_write_text(root, path, text, label=path.name)

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 Keep generated baselines under the requested root

When wardline baseline create pkg is run with a relative scan root, baseline_file(root) has already produced pkg/.weft/wardline/baseline.yaml, and this safe write treats that relative path as relative to pkg again, creating pkg/pkg/.weft/wardline/baseline.yaml while the CLI reports the original path. Subsequent scans of pkg will not load the baseline that was supposedly created, leaving existing findings unsuppressed; resolve the root before deriving the store path or pass a root-relative target.

Useful? React with 👍 / 👎.

shown_facts: list[Finding] = []
window: list[Finding] = []
elif self.max_findings is not None:
window = union[self.offset : self.offset + self.max_findings]

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 zero-sized agent-summary pages

When an MCP scan requests max_findings: 0 (currently accepted as non-negative), this slice returns an empty window while end remains equal to the current offset, so truncation.next_offset is also the same value whenever findings exist. Agents following next_offset will loop forever without making progress; reject zero or mark the response untruncated with no next page.

Useful? React with 👍 / 👎.

John Morrissey and others added 4 commits June 16, 2026 01:49
…lative root

`wardline scan pkg` / `wardline baseline create pkg` (relative subdir) reported
`pkg/findings.jsonl` / `pkg/.weft/wardline/baseline.yaml` but the root-confined
safe writer resolved the already-root-prefixed target under `pkg` AGAIN, writing
`pkg/pkg/...` — so the reported artifact never existed and a later scan of `pkg`
silently never loaded the baseline.

scan.py: hand the confined writer the bare root-relative default name (output stays
the reported path). baseline.py: pass the store path to write_baseline as absolute
so safe_write_text does not re-join it. Explicit -o / absolute roots unaffected.
Regression tests drive both via a relative subdir.

Found by the Codex PR review bot on #40 (P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five sites where a planted symlink or forged state file in an untrusted checkout
could make wardline follow a link off-box, clobber/disclose an arbitrary file, or
signal an unrelated process group (Codex PR #40 review, P2):

- config.py: server-mode scope discovery resolved `.weft/filigree` THROUGH a
  symlink and matched it against the global registry, letting a checkout spoof
  another registered project's scope and emit findings there. Only an in-root,
  non-symlink sibling store may claim a scope now.
- doctor.py: `doctor --repair` opened `.env` with O_TRUNC, following a symlink to
  clobber an arbitrary user-writable file. Refuse a symlinked .env (+ O_NOFOLLOW
  for the check->open race); the repair reports a refusal instead.
- rekey.py: snapshot copied a store's symlink TARGET into the in-project snapshot
  (arbitrary file disclosure) — now reads stores no-follow (new
  safe_paths.read_bytes_no_follow) and skips symlinked/non-regular ones; the
  journal `.tmp` was unguarded (safe_project_file covered only the final path) and
  is now written no-follow.
- scan_jobs.py: `scan-job cancel` trusted a persisted `pid` and killpg'd it, so a
  forged status.json could terminate an unrelated same-user process group. Validate
  the pid is genuinely this project's worker (own process-group leader + Linux
  /proc cmdline names the worker module and job id) before signaling.

NOT changed: the filigree federation-token/.env READ raising on a symlink
(filigree/config.py) is a deliberate, tested refuse-loudly decision
(wardline-db67828599) — the reviewer's fail-soft suggestion conflicts with it;
surfaced for the owner rather than silently reversed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… non-401/403

verify_token marked every status except 401/403 as an accepted bearer, so a
transient Filigree 5xx or a wrong-route 404 read as "auth verified" — and
doctor --repair would then pin the first local token candidate into .env without
auth ever being exercised. Accept only 2xx or the sentinel-body 400 (auth runs in
middleware before body validation, so both prove the bearer passed); treat any
other status as INCONCLUSIVE (reachable=False) so repair never pins on an
unverified probe. 401/403 stay reachable-but-rejected.

Found by the Codex PR review bot on #40 (P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three agent-facing honesty gaps (Codex PR #40 review, P2):

- scan-job agent-summary artifact ignored fail_on_unanalyzed: it built its gate
  from fail_on alone, so a job whose terminal status FAILED on unanalyzed files
  wrote an artifact saying the gate passed. The artifact now embeds the same
  gate_decision (fail_on + fail_on_unanalyzed) as the terminal status.
- scan-job agent-summary artifact was written BEFORE Filigree enrichment, so its
  integrations.filigree_emit always serialized configured:false even when the
  upload then succeeded. The worker now enriches and computes the decision/emit
  block first, then writes the artifact (and reuses the one migration_hint), so
  artifact and terminal status agree.
- agent_summary max_findings=0 produced an empty window whose truncation cursor
  (next_offset) equalled offset, looping a paging agent forever. The MCP scan tool
  now rejects max_findings < 1 (summary_only covers counts-without-findings), and
  the builder never claims truncation on a zero-progress window.

Glossary file:line anchors re-synced (agent_summary/scan/server grew).

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: fa1ca06397

ℹ️ 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 src/wardline/mcp/server.py Outdated
@@ -206,10 +772,15 @@ def _scan(
# A bad enum value is agent-actionable — give it the valid set rather than
# letting it surface as an opaque generic JSON-RPC -32603.
raise ToolError("fail_on must be one of CRITICAL/ERROR/WARN/INFO") from exc
# A4 (wardline-7fd0f3a82c): the CLI's --fail-on-unanalyzed knob, same default (off).
fail_on_unanalyzed = _bool_arg(args, "fail_on_unanalyzed", False)
new_since = args.get("new_since")
trusted_packs = _trusted_packs_arg(args)
cache_dir = _cache_dir_arg(args, root)
trust_suppressions = bool(args.get("trust_suppressions") or False)

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 non-boolean trust_suppressions

When jsonschema is not installed (the server explicitly falls back to running handlers), an MCP client that sends "trust_suppressions": "false" hits this bool(...) coercion and enables trusted-local suppression anyway. With fail_on set in an untrusted checkout, a repository baseline/waiver can then clear the gate even though the caller attempted the default secure behavior; use _bool_arg here as the scan-job path already does.

Useful? React with 👍 / 👎.

Comment thread src/wardline/install/doctor.py Outdated
out: list[str] = []
for p in paths:
try:
value = p.read_text(encoding="utf-8").strip()

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 Do not follow project token symlinks during repair

When doctor --repair is triggered after an env/.env token is rejected, this candidate loop also reads <root>/.weft/filigree/federation_token. In an untrusted checkout with a planted loopback ephemeral.port, making that file a symlink to a user-readable file causes its contents to be read and sent as a Bearer candidate to the probed local service; skip project-store symlinks/non-regular files (or use a safe no-follow read) before probing.

Useful? React with 👍 / 👎.

Comment thread src/wardline/install/mcp_json.py Outdated
return False
if pa.hostname not in _LOOPBACK_HOSTS or pb.hostname not in _LOOPBACK_HOSTS:
return False
return (pa.port, pa.path) == (pb.port, pb.path)

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 Handle malformed preserved Filigree ports

If an existing .mcp.json has a malformed loopback --filigree-url such as http://localhost:notaport/... and server-mode discovery returns a scoped URL, accessing pa.port here raises ValueError outside the try block. That makes wardline doctor --repair / MCP repair crash instead of replacing the stale local URL; catch port parsing errors or treat them as non-matching.

Useful? React with 👍 / 👎.

entity = context.entities.get(qualname)
if entity is None:
return None
sink_calls = [n for n in ast.walk(entity.node) if isinstance(n, ast.Call) and getattr(n, "lineno", None) == line]

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 the exact sink call when explaining taint

When two sink findings share a physical line, for example semicolon-separated calls, this collects every call on that line and returns the first raw-returning argument it finds. A fingerprint-selected explanation for the later sink can therefore name the wrong source/remediation because the finding's column/span discriminator is ignored; carry the sink call span or parse it from the fingerprint/properties and restrict source attribution to that exact call.

Useful? React with 👍 / 👎.

Comment thread src/wardline/cli/scan.py Outdated
if output_is_default:
safe_write_text(path, confined_name, agent_summary_json, label=default_name)
else:
output.write_text(agent_summary_json, encoding="utf-8")

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 Write custom agent summaries without following symlinks

With wardline scan --format agent-summary --output <path> in an untrusted checkout, this explicit-output branch follows a repository-controlled final symlink and can truncate any user-writable target, whereas the default agent-summary path and the JSONL/SARIF sinks use the no-follow safe writer. Use write_text_no_follow/safe_write_text here too so choosing an explicit summary filename does not re-enable symlink clobbering.

Useful? React with 👍 / 👎.

… fixes

Codex PR #40 second review pass (P2):

- mcp/server.py: scan `trust_suppressions` used bool(arg), and bool("false") is
  True — without jsonschema a client sending the STRING "false" would enable
  trusted-local suppression and let a repo baseline/waiver clear the gate. Use
  _bool_arg (rejects a non-boolean), matching the scan-job path.
- install/doctor.py: doctor --repair enumerated token candidates by read_text on
  <root>/.weft/filigree/federation_token and SENDS them as a Bearer to the probed
  service; a symlinked project mint would exfil its target's bytes. Read the
  project store regular-only/no-follow (safe_read_text_if_regular); the home store
  (operator's own config) is read normally.
- install/mcp_json.py: _same_scope_target accessed urlsplit().port outside the try,
  so a malformed preserved loopback URL (localhost:notaport) crashed doctor
  --repair. Parse ports inside the guard -> non-matching (URL gets replaced).
- cli/scan.py: the explicit `-o` agent-summary branch used a raw write_text that
  follows a repo-controlled symlink and truncates an arbitrary target; use
  write_text_no_follow like the default + JSONL/SARIF sinks.

Deferred to 1.0.2: explain.py exact-call span disambiguation (needs the finding to
carry a column span the engine does not populate today).

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

output.write_text(json.dumps(artifact, indent=2, sort_keys=True) + "\n", encoding="utf-8")

P2 Badge Write legis artifacts without following symlinks

When wardline scan --format legis writes either the default scan.legis.json in an untrusted checkout or an explicit --output, this raw write_text follows a repository-controlled final symlink; the JSONL/SARIF and hardened summary sinks use no-follow writers, but this new branch can still truncate any user-writable target. Route legis artifacts through the same safe/no-follow writer.

ℹ️ 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".

# provisioned key must not auto-attach a ~56KB verbatim artifact into it
# (dogfood-4 B6 blew the MCP token cap exactly this way). An explicit
# legis_artifact:true still wins when the caller asks for both.
return

cfg = config_mod.load(
_cfg(args, path) or weft_config_path(path),

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 Resolve legis config from the MCP root

When an MCP scan call uses a subdirectory path plus an explicit config relative to the server/project root, the scan itself used _cfg(args, root), but the legis artifact path re-resolves the same argument relative to the subdirectory. If legis_artifact: true is set (or this block is otherwise activated), a valid config such as weft.toml at the project root becomes <path>/weft.toml here, causing the whole scan response to fail or sign a ruleset derived from a different config than the findings were scanned with.

Useful? React with 👍 / 👎.

if resp.status >= 500:
# Server-side outage (5xx) — the sibling is degraded, not a Wardline
# payload bug. Treat like absent (warn + continue), carrying the status.
return EmitResult(reachable=False, status=resp.status, token_sent=token_sent, url=self._url)

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 Preserve partial Filigree chunk state

When a scan is split into multiple Filigree chunks and an earlier chunk succeeds, a later 5xx takes this return and discards the accumulated created/updated counts without marking the remaining chunks as failed. Callers then see reachable: false with zero failures even though some findings were already ingested and the rest were not; this should be reported as a partial emit (similar to the fail-soft 4xx branch) or otherwise carry the accumulated stats and pending failures.

Useful? React with 👍 / 👎.

Comment thread src/wardline/core/scan_jobs.py Outdated
# terminal job status instead of an unanalyzed-blind, pre-enrichment snapshot.
output.parent.mkdir(parents=True, exist_ok=True)
summary = build_agent_summary(result, decision, filigree_emit=filigree_emit, migration_hint=migration_hint)
output.write_text(json.dumps(summary.to_dict(), sort_keys=True) + "\n")

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 Write scan-job summaries without following symlinks

For scan-job start --format agent-summary --output <path> inside an untrusted checkout, this raw write_text follows a repository-controlled final symlink, unlike the JSONL/SARIF sinks and the CLI summary path that use no-follow writers. A planted output symlink can therefore truncate any user-writable target when the worker writes the summary; use the same safe/no-follow writer for this artifact branch.

Useful? React with 👍 / 👎.

# orphan every healthy verdict (the destructive twin of the A7 probe misread,
# weft-dda1a6d8dd). Checked BEFORE the snapshot — a refused run writes nothing.
populated_schemes = [scheme for scheme, _fps in _store_fingerprints(root).values()]
if populated_schemes and all(s == FINGERPRINT_SCHEME for s in populated_schemes):

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 incomplete rekeys retry the Filigree leg

When the first apply finishes the YAML legs but Filigree returns a soft failure, the journal remains incomplete and the CLI/MCP tell the user to re-run rekey. On that normal retry the live YAML stores are already stamped with the current scheme, so this guard raises no migration pending before apply_pending_legs can retry the pending Filigree leg; --resume also defers because it supplies no findings. Resume/retry the incomplete journal before this no-op guard, or let the Filigree leg re-emit from the fresh scan.

Useful? React with 👍 / 👎.

Comment thread src/wardline/cli/rekey.py
# scan must cover every frontend too: a python-only scan misreads each healthy
# Rust verdict as orphaned/stale (A7, weft-dda1a6d8dd).
findings = []
for lang in ("python", "rust"):

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 Skip Rust scans when Rust support is unavailable

When a scanner-only install without the optional wardline[rust] extra runs wardline rekey on a mostly Python repo that also contains any .rs file, this unconditional second pass reaches parse_rust() and raises the optional tree-sitter tooling error before the Python stores are migrated. Requiring the Rust extra only because source files exist blocks the normal Python rekey path; skip the Rust pass when tooling is unavailable/no Rust store entries exist, or make it opt-in.

Useful? React with 👍 / 👎.

Regression I introduced in fa1ca06: restructuring _write_scan_artifact to embed
the gate/emit block switched the agent-summary branch to a raw output.write_text,
which follows a repo-controlled --output symlink and can truncate an arbitrary
user-writable target — unlike the JSONL/SARIF sinks and the CLI summary path.
Write through safe_write_text (in-root) / write_text_no_follow (explicit) so the
worker artifact matches the no-follow discipline everywhere else.

Found by the Codex PR review bot on #40 (third pass, P2).

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: 274ad9fd5c

ℹ️ 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".

},
"max_findings": {
"type": "integer",
"minimum": 0,

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 Require positive max_findings in the schema

When an MCP client follows this advertised inputSchema, max_findings: 0 is still valid because the schema says minimum: 0 and the description calls it non-negative, but the handler above now rejects any value below 1 with a ToolError. Schema-driven agents can therefore make a request the server declares valid and receive an isError instead of a scan; update the schema/description to require 1 or allow the documented zero case only where intended.

Useful? React with 👍 / 👎.

for name in snap_files:
live = safe_project_file(root, state / name, label=name)
live.parent.mkdir(parents=True, exist_ok=True)
live.write_bytes((sdir / name).read_bytes())

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 Read rollback snapshots without following symlinks

When wardline rekey --rollback runs in a checkout where .weft/wardline/.rekey_snapshot/baseline.yaml (or judged/waivers) is a symlink, is_file() above follows it and this read_bytes() copies the symlink target into the live store. That lets an untrusted repo disclose any user-readable file into .weft/wardline; read snapshot files with the same no-follow regular-file helper used by snapshot_stores before restoring.

Useful? React with 👍 / 👎.

…owing live discovery

A sibling URL frozen in .mcp.json (e.g. derived from a legacy
.filigree/ephemeral.port rung that outlived a port rotation) becomes an
explicit flag that outranks published-port discovery — so emit silently
targets a dead port while the live daemon sits on a new one. Found dogfooding
elspeth: pinned :9229 (dead) while Filigree was live on the published :9397.

Repair (merge_mcp_entry / new _desired_sibling_url): a loopback pin that a
live published-port rung can reconstruct is DROPPED (both siblings), so runtime
discovery owns the always-current port. Remote (non-loopback) pins and loopback
pins with no live daemon are preserved verbatim. Filigree server mode keeps
repairing a loopback pin to the live project scope (unscoped writes fail-close).

Detection (_check_filigree_auth): when the pinned probe URL is unreachable but a
DIFFERENT published-port URL is live, plain `wardline doctor` now reports an
actionable error pointing at `--repair`, instead of masking it as a soft
"daemon not reachable".

Also corrects a stale _check_config comment that claimed an absent weft.toml is
treated as "ok" — the code (correctly) flags it as a repairable error tied to the
broad-scan warning.

Tests: 5 new regressions (3 mcp_json drop/preserve cases, 2 doctor detection
cases). Full suite 4024 passed; mypy strict + ruff clean; dogfood gate exit 0.

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: caed35dd04

ℹ️ 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 on lines +355 to +356
if resolved == root_resolved or resolved.is_relative_to(root_resolved):
candidates.add(resolved)

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 Require a real Filigree store before matching scope

Fresh evidence beyond the symlink case: a missing .weft/filigree still passes this branch because Path.resolve() defaults to strict=False, so a stale global Filigree registry entry for a reused checkout path is treated as this project's live scope even when no sibling store exists. In that scenario a bare scan/doctor can auto-select the old project-scoped URL and emit findings into the stale Filigree project; only add the candidate when the sibling store is an existing non-symlink directory inside the root.

Useful? React with 👍 / 👎.

Comment on lines +490 to +493
population = [f for f in findings if is_join_population(f)]
scanned = sorted({f.location.path for f in population})
try:
result = filigree.emit(population, scanned_paths=scanned)

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 Preserve clean scanned files during Filigree rekey

When a file had old Filigree findings but the current scan is clean for that file, deriving scanned_paths from only the current defect population omits the file entirely. Filigree's mark_unseen reconciliation is scoped to scanned_paths, so a rekey can report the Filigree leg as reconciled while old wlfp1 associations in now-clean files remain latest/visible; pass through the scan's actual ScanResult.scanned_paths instead of reconstructing it from current findings.

Useful? React with 👍 / 👎.

Comment on lines 795 to +798
trusted_packs=trusted_packs,
strict_defaults=strict_defaults,
trust_suppressions=trust_suppressions,
lang=lang,

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 Strip Rust locators before applying SEI filters

With the new lang: "rust" scan path, an MCP where.qualname SEI filter can resolve to a Loomweave locator like rust:function:demo.m.leaky, but locator_to_qualname still strips only python:* prefixes while Rust findings store the bare qualname (demo.m.leaky). The subsequent filter therefore compares against the full locator and returns an empty page even though the Rust finding is present; extend the SEI locator normalization for Rust id-kinds before filtering Rust scan results.

Useful? React with 👍 / 👎.

) -> None:
"""Start a scan job and print its status JSON."""
request = {
"config": str(config_path) if config_path else 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 Resolve scan-job config before changing cwd

When scan-job is started on a subdirectory with an explicit config relative to the caller's cwd, e.g. wardline scan-job start pkg --config weft.toml, Click validates the project-root file here but the background worker is later launched with cwd=root (the scan path). The serialized weft.toml is then read as pkg/weft.toml, so the job fails or runs without the intended policy while a foreground run behaves differently; resolve these CLI paths to absolute or root-relative before writing the request.

Useful? React with 👍 / 👎.

root = root.resolve()
path = status_path(root, job_id)
try:
parsed = json.loads(path.read_text(encoding="utf-8"))

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 Read scan-job status without following symlinks

When status is requested in an untrusted checkout, only the job directory is confined; the final status.json may still be a symlink. A planted .weft/wardline/jobs/<valid-id>/status.json pointing to another user-readable JSON file is followed here and its contents are returned by scan-job status or MCP status, so status reads should use the same no-follow regular-file discipline as other project-local state.

Useful? React with 👍 / 👎.

Release-engineering hygiene, no engine/source changes:

- pyproject: add explicit [tool.hatch.build.targets.sdist] include
  allowlist so the published sdist ships only the package, tests, and
  canonical metadata instead of every non-ignored repo-root path
  (docs/, site/, www/, packages/, internal audits, uv.lock).
- release.yml: guard that the pushed vX.Y.Z tag matches the version
  hatchling stamped into the built distributions before publishing.
- CHANGELOG: bump [1.0.1] date to 2026-06-17.
- Relocate AUDIT.md -> docs/audits/2026-06-08-comprehensive-audit.md
  and fix its inbound reference in the weft-rename integration doc.
- README: refresh stale "four policy rules" rule-table description.
- gitignore: ignore transient output/ scratch dir.

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: 02f533b112

ℹ️ 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".

resolved_root = root.resolve()
# SP2 whole-tree pass: discover Cargo crate roots ONCE per scan; every file's
# module route resolves against this map (longest-prefix, symlink-safe walk).
crate_roots = discover_crate_roots(resolved_root)

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 Bound Rust crate discovery to the requested scan scope

For wardline scan --lang rust in a large repo where weft.toml narrows source_roots (or even a repo with no discovered .rs files), this walks the entire project root before analyzing the already-discovered file list. That bypasses the same pruning/scope that discover() just applied and can hang on unrelated build/vendor trees; derive crate roots from the discovered files' ancestor chain or pass the same bounded roots/pruning into crate discovery.

Useful? React with 👍 / 👎.

Comment on lines +62 to 64
"fingerprint": format_fingerprint(FINGERPRINT_SCHEME, finding.fingerprint),
"metadata": to_filigree_metadata(finding),
"language": "python",

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 Emit Rust findings with a Rust language tag

When --lang rust findings are emitted to Filigree, this shared wire builder still tags every finding as language: "python" just below the fingerprint. RS-WL findings will therefore be stored and filtered as Python findings in Filigree even though their rule ids/qualnames come from the Rust frontend; derive the language from the finding family (e.g. RS-WL-rust) before posting.

Useful? React with 👍 / 👎.

Comment thread .pre-commit-config.yaml
hooks:
- id: wardline-scan
name: wardline scan
entry: wardline scan

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 Make the pre-commit scan hook enforce a gate

With this entry the hook runs wardline scan without --fail-on, so a commit that introduces active ERROR/CRITICAL findings still exits 0 because the scan gate is NOT_EVALUATED unless a threshold is supplied. Pre-commit hides successful hook output by default, so developers installing this hook get a green commit with the findings effectively ignored; add the intended --fail-on ... threshold (and optionally --fail-on-unanalyzed) to make the hook load-bearing.

Useful? React with 👍 / 👎.

Comment on lines +14 to +18
push:
branches: [main]
paths:
- 'site/**'
- '.github/workflows/deploy-site.yml'

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 Build the site on pull requests too

Because the old PR docs-build job was removed and this replacement workflow only runs on pushes to main or manual dispatch, a PR that changes site/** can merge without ever running npm install/astro build; the first validation happens during the production Pages deploy. Add a pull_request trigger for the same site/workflow paths (build-only, no deploy) so broken site changes are caught before merge.

Useful? React with 👍 / 👎.

@tachyon-beep
tachyon-beep merged commit 97ed67a into main Jun 16, 2026
8 checks passed
@tachyon-beep
tachyon-beep deleted the rc5 branch June 22, 2026 20:52
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