Skip to content
89 changes: 89 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,95 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Concurrent-writer detection**: the scan now stat-snapshots the discovered file
inventory at discovery and re-checks it after analysis. If any scanned file was
written, truncated, or deleted while the scan ran, it emits a non-gating
`WLN-ENGINE-TREE-CHANGED-DURING-SCAN` FACT (and a CLI `warning:`) naming the files
and the consequence — findings reflect each file as first read, and a pre-commit
harness may fail the hook via its own files-modified check even though wardline's
gate verdict is unaffected. Motivated by the 2026-07-03 elspeth RCA
(`docs/handoffs/2026-07-03-elspeth-precommit-intermittent-rca.md`), where exactly
that pre-commit behavior on a shared checkout was misread as a wardline failure.

### Security
- **HTTP redirects are never followed on any credential-bearing transport.** urllib's
default handler re-sends every non-`Content-*` header — `Authorization: Bearer` and
the `X-Weft-*` HMAC trio — to a redirect target, cross-origin included, and rewrites
a redirected POST into a body-less GET whose 200 parsed as a clean, reachable emit
(silent false-green telemetry). The shared `WeftHttp` transport now refuses the first
3xx before dialing the target (pre-seeded stdlib redirect-loop guard, backstopped by
a typed fail-closed `WeftRedirectError`), and a 3xx surfaces as an ordinary
status-band outcome: `verify_token` reports it inconclusive (never pins a token on an
unverified probe), emit reports it as a failure, and the promote/judge legs reject
loudly. All federation clients (Filigree emit + promote/entity-association, Loomweave,
dossier) and the judge's OpenRouter transport (operator API key) now route through
this guard — the last two raw-`urlopen` credential paths (`core/filigree_issue.py`,
`core/judge.py`) were converted as part of this change (wardline-f68c483d92).
- **Signing paths fail closed on indeterminate git state.** Git dirtiness is now
tri-state (`clean / dirty / unknown`): the legis artifact signs only on a proven-clean
tree, and `attest` signs an indeterminate tree only under explicit `allow_dirty`,
recording `dirty: null` uncoerced (previously a failed `git status` read as *clean*
and could sign a falsely-clean bundle). `verify_attestation` never coerces an unknown
state to clean.
- **Credential-bearing URLs are redacted at every diagnostic seam.** Scheme-gate
errors, `filigree`-unreachable reasons, scan-job `status.json`/request persistence,
promote rejects, and redirect backstop messages now echo scheme+host only — no
userinfo, path, or query from a configured or attacker-steered URL reaches logs,
status files, or MCP results.

### Fixed
- **MCP boolean arguments no longer coerce truthy strings.** A string `"false"` passed
to a boolean tool arg (`fix.apply`, `judge.write`, `baseline.overwrite`,
`attest.allow_dirty`, `verify_attestation.reproduce`, `scan.*`, and every other
converted site) now returns a typed `ToolError` instead of being treated as `True` —
pre-fix, `fix` with `apply="false"` **applied**.
- **The MCP handshake gate no longer special-cases test runs.** The
reject-before-`initialize` rule is enforced by the same code path everywhere (the
pytest-sniffing bypass was removed); protocol conformance tests now exercise the true
default.
- **Scan-job terminal statuses are monotonic.** A landed terminal status
(`done`/`error`/`cancelled`) is no longer replaced by a slower writer finishing
second (parent/worker race).
- **A configured gate over zero scanned files is `NOT_EVALUATED`, not a vacuous
`PASSED`** (`no_files_scanned`): an empty-but-existing source root or an exclude-all
config no longer reads as an authoritative green.

### Changed
- **Filigree emit accounting is honest about mid-batch failures**: `EmitResult` now
carries the landed chunk count, per-finding `partial` failures, and a
"connection dropped mid-emit: K of N chunk(s) landed" warning when the connection
drops after chunks landed (machine envelope; the CLI text surface is tracked in
wardline-7924d67d3b).
- **Loomweave resolve stops on auth rejection**: a hinted 401/403 halts the batch and
is carried as `ResolveResult.auth_status` so surfaces can steer the operator to the
token rather than a phantom unresolved entity (consumer threading tracked in
wardline-6f9eece880); a zero-fact scan now reports the taint-store write as skipped
(`no facts`) rather than written.
- **An armed severity gate that passes now says so** (wardline-eef3d30c7d). `wardline
scan --fail-on <SEV>` used to print a gate verdict only for `FAILED`,
`NOT_EVALUATED`, and the unanalyzed-only pass — a clean armed pass ended with the
bare summary. It now prints `gate: PASSED (--fail-on <SEV>) — <reason>` and
`gate: evaluated <population>` on stderr, so a hook-captured log is
self-diagnosing: when a hook harness fails the hook for its own reasons (e.g.
pre-commit's "files were modified by this hook" check tripped by a concurrent
writer on a shared checkout), the log now shows wardline's own verdict instead of
reading like a silent wardline failure. Diagnosed from an elspeth pre-commit
consumer report (2026-07-03); the intermittent commit failures were pre-commit's
diff-before/diff-after check attributing another session's tracked-file writes to
the slowest (whole-tree) hook — wardline exited 0 in every observed occurrence.

### Docs
- Documented the Filigree **server-registry rung** of sibling-URL resolution
(`docs/guides/weft.md`, `configuration.md`, `agents.md`): a repo registered
with a server-mode Filigree daemon needs no `--filigree-url` — Wardline
derives the project-scoped `/api/p/<prefix>/weft/scan-results` target from
`~/.config/filigree/server.json`, and `wardline install` bakes it into the
`.mcp.json` entry. The emitter example now uses a path-scoped URL, since
server-mode Filigree fail-closes unscoped writes; the unscoped form is
correct only for a single-project (ethereal) Filigree. Doc-only; the
resolution behavior itself shipped earlier.

## [1.2.0] - 2026-06-30

### Fixed
Expand Down
13 changes: 8 additions & 5 deletions docs/guides/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ If you have not installed Wardline yet, start with
- writes a global Codex MCP entry in `~/.codex/config.toml`;
- **detects** a Loomweave taint store (`loomweave` on `PATH` or
`WARDLINE_LOOMWEAVE_URL`) and a Filigree project (`.filigree.conf`) and reports
what it found — it writes **no** binding and persists **no** URL. `weft.toml`
stays operator-authored; live URLs come from the `--filigree-url` /
`--loomweave-url` flag, the `WARDLINE_FILIGREE_URL` / `WARDLINE_LOOMWEAVE_URL`
env var, or the published `.weft/<sibling>/ephemeral.port` rung (legacy
`.<sibling>/ephemeral.port` tolerated).
what it found. `weft.toml` stays operator-authored; live URLs come from the
`--filigree-url` / `--loomweave-url` flag, the `WARDLINE_FILIGREE_URL` /
`WARDLINE_LOOMWEAVE_URL` env var, Filigree's server-mode registry
(`~/.config/filigree/server.json` — a server-registered repo needs no flag;
install bakes that scoped target into the `.mcp.json` entry so the MCP
server's emit target is inspectable), or the published
`.weft/<sibling>/ephemeral.port` rung (legacy `.<sibling>/ephemeral.port`
tolerated).

```console
$ wardline install
Expand Down
7 changes: 5 additions & 2 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ Everything nests under the `[wardline]` table.
There is **no** `[wardline.filigree].url` or `[wardline.loomweave].url`
config key. Sibling endpoint URLs resolve only via the `--filigree-url` /
`--loomweave-url` flag, the `WARDLINE_FILIGREE_URL` / `WARDLINE_LOOMWEAVE_URL`
environment variable, or the published `<root>/.weft/<sibling>/ephemeral.port`
rung (legacy `<root>/.<sibling>/ephemeral.port` is tolerated). See
environment variable, or live discovery: for Filigree, the server-mode
registry (`~/.config/filigree/server.json`) supplies the project-scoped
`/api/p/<prefix>/` URL — a server-registered repo needs no flag — else the
published `<root>/.weft/<sibling>/ephemeral.port` rung (legacy
`<root>/.<sibling>/ephemeral.port` is tolerated). See
[Weft integration](weft.md).

!!! note "Waivers are not config keys"
Expand Down
37 changes: 30 additions & 7 deletions docs/guides/weft.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,24 @@ output format, so SARIF output and CI gating compose.

`--filigree-url` POSTs findings into Filigree's Weft scan-results lifecycle —
Filigree owns finding *state* (status, seen-count, issue links); Wardline owns
the analysis fact and the local baseline. Pass the full endpoint URL:
the analysis fact and the local baseline. The flag is **optional for a repo
registered with a server-mode Filigree daemon**: Wardline derives the
project-scoped endpoint from Filigree's home registry
(`~/.config/filigree/server.json`, matched against the repo's `.weft/filigree`
store), so a bare `wardline scan .` already emits to the right project. Pass
the full endpoint URL only to override that discovery:

```console
$ wardline scan . --filigree-url http://localhost:8377/api/weft/scan-results
$ wardline scan . --filigree-url http://localhost:8377/api/p/myproject/weft/scan-results
```

Against a **server-mode** daemon the URL must carry the project scope —
`/api/p/<prefix>/…` path or a `?project=<key>` query. Server-mode Filigree
fail-closes an unscoped write with a 400 rather than let one project's
findings land silently in another's tracker. The unscoped
`…/api/weft/scan-results` form is correct only for a single-project
(ethereal / per-project-port) Filigree.

This is layered on top of the normal local output — Wardline still writes a
timestamped JSONL artifact (or your exact `--output`) and runs the gate;
emission is additive.
Expand Down Expand Up @@ -188,12 +200,23 @@ resolves, in precedence order, from:

1. the `--filigree-url` / `--loomweave-url` flag;
2. the `WARDLINE_FILIGREE_URL` / `WARDLINE_LOOMWEAVE_URL` environment variable;
3. the published `<root>/.weft/<sibling>/ephemeral.port` file written by a running
3. **(Filigree only)** the server-mode registry: when the repo's
`.weft/filigree` store is registered in `~/.config/filigree/server.json`,
Wardline derives the project-scoped
`http://localhost:<port>/api/p/<prefix>/weft/scan-results` from it —
a server-registered repo needs no flag at all;
4. the published `<root>/.weft/<sibling>/ephemeral.port` file written by a running
sibling (the legacy `<root>/.<sibling>/ephemeral.port` location is tolerated
during the transition window).

`wardline install` / `wardline doctor` only **detect** whether a sibling is
present — they write no binding and persist no URL.
during the transition window). For Filigree this rung yields the unscoped
single-project URL, which is correct only outside server mode.

`wardline doctor` only **detects** whether a sibling is present (and probes the
emit token, the `filigree.auth` check) — it persists no URL. `wardline install`
resolves URLs live the same way, with one deliberate exception: when the
Filigree server registry supplies a scoped target, install bakes it into the
`.mcp.json` `wardline` entry (and repairs a stale loopback pin to the
registered scope) so the long-lived MCP server's emit target is explicit and
inspectable.

## See also

Expand Down
65 changes: 65 additions & 0 deletions docs/handoffs/2026-07-01-scan-manifest-seam-closed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# scan_manifest seam — BUILT + RELEASED (bless + closure handover)

Date: 2026-07-01
From: warpline session (the seam was built here at the owner's direction; this documents
the shipped result and the one hub-side act left).
Re: **AMBER-2 / `weft-9a35aa00e7`** — "Plainweave peer-facts dependency: Wardline must emit
a scan_manifest contract". **Now satisfied on both sides.**

## TL;DR — the half-built seam is closed

Plainweave's `wardline_adapter` reached for a `scan_manifest` record wardline never emitted,
so it degraded to a path-set heuristic (`wardline_scan_identity_absent`). Wardline now emits
it. **Producer and consumer are both committed and released** — the only thing left is the
governance step: **bless the contract hub-side and close the ticket.**

## What shipped

**Producer — wardline (DONE, released in `v1.2.0`):**
- `cli/scan.py` prepends a `scan_manifest` header line to the default
`.wardline/<ts>-findings.jsonl` artifact (the glob plainweave reads), **unconditionally**
(a clean zero-finding scan still carries coverage). Additive — the signed legis artifact /
`scan_scope` is untouched; existing line-by-line finding readers ignore the unknown `kind`.
- `core/run.py` exposes `ScanResult.analyzed_paths` (the analyzed subset).
- Committed in `14030acb`, on `release/consolidation-2026-06-26`, **in tag `v1.2.0`**.
- Verified: real scan emits the exact shape; e2e (wardline scan → plainweave reads
`covered_paths`, no degrade); 358-test regression green; ruff/mypy clean.

**Consumer — plainweave (DONE):** `wardline_adapter` consumes the real manifest; recent
commits `9ca6698` (guard ruleset-mismatch on both manifests) + `4782fd6` (green `make ci` +
wardline scan for wardline/warpline producers) show it reading real wardline output CI-green.

## The contract to bless hub-side — `weft.wardline.scan_manifest.v1`

Register this as the canonical federation contract so it can't drift (the same lesson as the
un-ratified requirements-enrichment schema). Emitted as the **first line** of
`.wardline/<ts>-findings.jsonl`:

```json
{"kind": "scan_manifest",
"scope": {"covered_paths": ["<repo-relative posix path>", "..."]},
"ruleset_id": "sha256:<hex>"}
```

Field semantics (the load-bearing part):
- **`covered_paths`** = the paths wardline actually **analyzed**, in the SAME repo-relative
POSIX format as `Finding.location.path`. A now-absent prior finding reads **RESOLVED** only
when its path is genuinely in this set. **Defaults to the ANALYZED set** so `--affected`
delta mode does not over-claim coverage (a discovered-but-not-re-analyzed file stays
*indeterminate*, not falsely resolved); `wardline scan --manifest-full-coverage` restores
the full discovered inventory.
- **`ruleset_id`** = `ruleset_hash(config)` (`sha256:…`, == the legis `rule_set_version`) so
the consumer can detect a ruleset change between two snapshots and NOT read a finding's
disappearance under a different ruleset as a resolution.
- Producer: **wardline**. Consumer: **plainweave** (echo-only, advisory).

## The two acts left (owner / hub)

1. **Bless `weft.wardline.scan_manifest.v1`** as the canonical contract (register the shape +
field semantics above hub-side), so wardline emission and plainweave consumption stay pinned.
2. **Close `weft-9a35aa00e7` / AMBER-2** — the producer-side gap it tracks no longer exists.
Recommended final gate before closing: one end-to-end confirmation on the released code
(real `wardline scan` → plainweave adapter reads `covered_paths`, `wardline_scan_identity_absent`
does not fire) — plainweave's `make ci + wardline scan` commit indicates this already passes.

*Everything above is shipped; these two are governance/closure, not engineering.*
Loading
Loading