Skip to content

fix(adapters): find_tool must prefer the AUDITED repo's venv, not Custodian's - #72

Merged
ProtocolWarden merged 2 commits into
mainfrom
fix/find-tool-prefers-audited-repo-venv
Aug 3, 2026
Merged

fix(adapters): find_tool must prefer the AUDITED repo's venv, not Custodian's#72
ProtocolWarden merged 2 commits into
mainfrom
fix/find-tool-prefers-audited-repo-venv

Conversation

@ProtocolWarden

Copy link
Copy Markdown
Owner

The bug

A globally-installed custodian-multi audited OperationsCenter — which pins ruff==0.15.13 — using a system-wide ruff 0.16.1, and reported 1222 findings against a tree OC's own ruff check calls clean (BLE001 ×316, UP045 ×290, UP037 ×118, …). Same repo, same .custodian/config.yaml, ruff 0.15.13: 0 findings. Vulture skewed the same way (621). Every one of them was phantom.

This is what turned OC's pre-push gate into a wall that no push could clear, on a baseline that is in fact clean.

Root cause

find_tool() resolved Path(sys.executable).parent / name — the venv Custodian itself is installed in — then fell back to PATH.

For a multi-repo auditor that is backwards. Each repo pins the toolchain its config was written against, so an audit is only meaningful when it runs those versions; the venv Custodian happens to live in has no authority over the repo in front of it. The old order was right by accident in the single-repo case (Custodian installed into the audited repo's venv) and silently wrong everywhere else — and silently is the problem, since the output is a plausible-looking wall of real rule codes.

A second bug hid in the same three lines: Path(sys.executable).parent / name can never match on Windows, where console scripts are ruff.exe and live in Scripts/ rather than bin/. The venv branch was dead code on that platform.

The fix

Resolution order is now:

  1. the audited repo's own venv (.venv then venv; bin/ or Scripts/)
  2. Custodian's own venv — unchanged fallback, still needed when the venv is not fully activated and shutil.which alone would miss it
  3. PATH

_executable() tries .exe/.bat/.cmd on Windows, and both script-dir spellings are accepted on either host: a venv built under WSL but audited from Windows over /mnt/c carries the other platform's layout.

The audited repo is scoped with a ContextVar + audited_repo() context manager rather than a new parameter — is_available() takes no arguments, and it and run() must agree on which binary they are talking about. cli/runner._run_adapters wraps its adapter loop in it.

Verification

  • 1238 passed, 5 skipped (full suite).
  • Six new tests: repo-venv preference, no-venv fallback, both script-dir spellings, and that the ContextVar does not leak past the loop — a leak would make later repos in a --repos a b c run inherit the first repo's toolchain.
  • Custodian's own audit is unchanged from baseline. The one remaining finding (W2, core.hooksPath unset) is environmental and pre-existing.
  • Live proof on the dev machine: under audited_repo(~/GitHub/OperationsCenter), find_tool('ruff') returns OC's pinned .venv/bin/ruff instead of Custodian's own.

Noted, not fixed

Pre-existing and reproduces at origin/main: tests/test_reconcile.py does not isolate $REPOGRAPH_BOUNDARY_ARTIFACT_FILE, so two tests fail whenever that variable is set in the caller's environment. Out of scope here.

🤖 Generated with Claude Code

…todian's

A globally-installed `custodian-multi` audited OperationsCenter — which pins
`ruff==0.15.13` — using a system-wide ruff 0.16.1, and reported 1222 findings
against a tree OC's own `ruff check` calls clean (BLE001 x316, UP045 x290,
UP037 x118, ...). Same repo, same config, ruff 0.15.13: 0 findings. Vulture
skewed the same way (621 findings). All of them phantom.

Root cause: `find_tool()` resolved `Path(sys.executable).parent / name` — the
venv Custodian ITSELF is installed in — then fell back to PATH. For a
multi-repo auditor that is backwards. Each repo pins the toolchain its config
was written against, so an audit is only meaningful when it runs those
versions; the venv Custodian happens to live in has no authority over the repo
in front of it. The old order was right by accident in the single-repo case
(Custodian installed into the audited repo's venv) and silently wrong
everywhere else — and "silently" is the problem, since the output is a
plausible-looking wall of real rule codes.

A second bug hid in the same three lines: `Path(sys.executable).parent / name`
can never match on Windows, where console scripts are `ruff.exe` and live in
`Scripts/` rather than `bin/`. The venv branch was dead code on that platform.

Resolution order is now:
  1. the audited repo's own venv (`.venv` then `venv`, `bin/` or `Scripts/`)
  2. Custodian's own venv (unchanged fallback, still needed when the venv is
     not fully activated and `shutil.which` alone would miss it)
  3. PATH

`_executable()` tries `.exe`/`.bat`/`.cmd` on Windows, and both script-dir
spellings are accepted on either host — a venv built under WSL but audited
from Windows over /mnt/c carries the other platform's layout.

The audited repo is scoped with a ContextVar and an `audited_repo()` context
manager rather than a new parameter: `is_available()` takes no arguments, and
it and `run()` must agree on which binary they are talking about.
`cli/runner._run_adapters` wraps its adapter loop in it.

Verified: 1238 passed, 5 skipped. Six new tests cover repo-venv preference, the
no-venv fallback, both script-dir spellings, and that the ContextVar does not
leak past the loop — a leak would make later repos in a `--repos a b c` run
inherit the first repo's toolchain. Custodian's own audit is unchanged from
baseline; the one remaining finding (W2, core.hooksPath unset) is
environmental. Live proof: under `audited_repo(~/GitHub/OperationsCenter)`,
`find_tool('ruff')` returns OC's pinned `.venv/bin/ruff` rather than
Custodian's own.

Noted, not fixed (pre-existing, reproduces at origin/main): tests/test_reconcile.py
does not isolate $REPOGRAPH_BOUNDARY_ARTIFACT_FILE, so two tests fail whenever
that variable is set in the caller's environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2026-08-03 reconciliation (#71) landed log.md at 395 lines against a 400-line
r1_line_budget, leaving no room for the next entry to be written at all — the
find_tool entry took it to 437 and red-failed our own audit on RC1.

Second pass, same convention as the first: prune the two oldest surviving entries
(2026-06-20 INJ1 detector, 2026-07-10 C32 punctuation-only values) into the
reconciled note, and condense the find_tool entry to a summary. Full rationale
lives in the commit message and PR #72; full history in git as always.

396 lines. Custodian's own audit is back to baseline — the single remaining
finding (W2, core.hooksPath unset) is environmental in this clone and does not
occur in CI, which sets it as the audit job's first step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ProtocolWarden
ProtocolWarden merged commit 8cfae58 into main Aug 3, 2026
7 checks passed
@ProtocolWarden
ProtocolWarden deleted the fix/find-tool-prefers-audited-repo-venv branch August 3, 2026 21:12
ProtocolWarden added a commit to ProtocolWarden/OperationsCenter that referenced this pull request Aug 4, 2026
…or a week (#492)

CI has failed on main every day since at least 2026-07-29. Both lint gates
installed ruff UNPINNED while the repo pins ruff==0.15.13:

  ci.yml               pip install "ruff>=0.5"      -> floated to 0.16.1
  custodian-audit.yml  pip install ruff vulture ty  -> same drift

`ruff check .` went from clean to 1996 errors; the Custodian audit reported 1222
findings (the ruff group alone — vulture was clean in CI). None of them real.

[tool.ruff.lint] selects a deliberate rule set, and its own comment records
BLE001 and S110 as DROPPED — "too noisy across codebase, real legitimate uses".
A newer ruff re-enables exactly those: of the 1222, BLE001 accounted for 316 and
UP045 for 290. Verified on the same tree: ruff 0.16.1 -> 1222 findings, ruff
0.15.13 -> "All checks passed!" across the full `ruff check .`, root files
included. The tree was never dirty.

Both jobs now install `-e ".[dev]"`, so the version comes from
[project.optional-dependencies].dev. One source of truth, and no version literal
left in the workflows to drift again.

custodian-audit.yml already carried a paragraph explaining that Custodian itself
must be SHA-pinned, because tracking @main once let an upstream change emit "a
phantom finding fleet-wide". The next line then installed that pinned auditor's
TOOLS unpinned and reproduced the same failure one level down. Pinning the
auditor while floating what the auditor runs pins nothing.

Also drops `|| true` from the repo install. Best-effort was actively harmful
here: on failure the adapters find no ruff, Custodian reports it "not installed"
and skips it, and the gate passes vacuously — a green check that audited
nothing.

Same root cause one layer up, fixed separately in ProtocolWarden/Custodian#72:
find_tool() preferred Custodian's own venv over the audited repo's, so a
globally-installed custodian-multi reproduced this identically off-CI.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant