wardline 1.0.1 — first complete post-reboot release (SEI set-merge)#40
Conversation
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>
…ine codebase conducted on 2026-06-04
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>
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>
There was a problem hiding this comment.
💡 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| @@ -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) | |||
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| "build", | ||
| "dist", |
There was a problem hiding this comment.
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 👍 / 👎.
| 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") |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| for base in (sibling_state_dir(root, "filigree"), legacy_sibling_dir(root, "filigree")): | ||
| try: | ||
| candidates.add(base.resolve()) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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 👍 / 👎.
| # 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") |
There was a problem hiding this comment.
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 👍 / 👎.
| accepted = resp.status not in (401, 403) | ||
| return ProbeResult(reachable=True, accepted=accepted, status=resp.status) |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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 👍 / 👎.
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>
There was a problem hiding this comment.
💡 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".
| if dest.exists(): | ||
| continue # never clobber the pre-migration snapshot | ||
| dest.parent.mkdir(parents=True, exist_ok=True) | ||
| dest.write_bytes(live.read_bytes()) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 👍 / 👎.
| pid = _status_pid(status) | ||
| if pid is not None and _pid_alive(pid): | ||
| try: | ||
| os.killpg(pid, signal.SIGTERM) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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 👍 / 👎.
…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>
There was a problem hiding this comment.
💡 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".
| @@ -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) | |||
There was a problem hiding this comment.
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 👍 / 👎.
| out: list[str] = [] | ||
| for p in paths: | ||
| try: | ||
| value = p.read_text(encoding="utf-8").strip() |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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 👍 / 👎.
| 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") |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 Codex Review
wardline/src/wardline/cli/scan.py
Line 327 in 929adcf
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), |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 👍 / 👎.
| # 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") |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 👍 / 👎.
| # 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"): |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| if resolved == root_resolved or resolved.is_relative_to(root_resolved): | ||
| candidates.add(resolved) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| trusted_packs=trusted_packs, | ||
| strict_defaults=strict_defaults, | ||
| trust_suppressions=trust_suppressions, | ||
| lang=lang, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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")) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| "fingerprint": format_fingerprint(FINGERPRINT_SCHEME, finding.fingerprint), | ||
| "metadata": to_filigree_metadata(finding), | ||
| "language": "python", |
There was a problem hiding this comment.
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 👍 / 👎.
| hooks: | ||
| - id: wardline-scan | ||
| name: wardline scan | ||
| entry: wardline scan |
There was a problem hiding this comment.
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 👍 / 👎.
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'site/**' | ||
| - '.github/workflows/deploy-site.yml' |
There was a problem hiding this comment.
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 👍 / 👎.
wardline 1.0.1 — first complete post-reboot release
This is the cut of
rc5→mainfor 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)
(soundness redesign, 6 sink families, boundary partition, fail-closed parse gate).
wardline[rust]— Tier-A command-injection slice(
RS-WL-108/RS-WL-112), pre-built tree-sitter abi3 wheels (no compiler).wardline mcp(dep-free stdlib JSON-RPC), structured tooloutput on all tools,
doctor/rekey/assure/attest/dossier/finding-lifecycletools, pollable file-backed scan jobs.
weft.toml [wardline], state in.weft/wardline/;enrich-only emit to Filigree (native emitter) and Loomweave taint store; SEI-native
addressing across the federation.
Development Status :: 5 - Production/Stable.Release mechanics
__version__=1.0.1; wheel + sdist build clean aswardline-1.0.1.[1.0.1] - 2026-06-15(rc1/rc2 sections folded in, no content lost).guide, agents guide):
pip install 'wardline[rust]'.Held for the human owner (post-merge, with the set)
v1.0.1git tag and PyPI publish (Trusted Publishing) — the irreversible steps.🤖 Generated with Claude Code