Repository navigation
fix: the verify extra must not repin a user's environment - #55
Merged
GiulioDER merged 2 commits intoAug 31, 2026
Merged
Conversation
`pip install 'cca-audit[verify]'` downgraded a user's `mcp` from 2.1.0 to 1.29.0
and broke unrelated MCP tooling. Nothing in this project imports `mcp`. The chain:
`verify` listed semgrep, semgrep hard-pins `mcp==1.29.0` (also
`ruamel.yaml.clib==0.2.15`, `pywin32==311`), and pip enforced that across the whole
environment.
What makes it a bug rather than bad luck is that semgrep was never a library
dependency. `semgrep_check.py` resolves it on PATH and spawns it, exactly like
`cargo`, which nobody would think to pip-install. It sat in the extra because "the
whole deterministic layer in one install" read as tidy. It is not tidy when the
cost is rewriting packages that have nothing to do with auditing.
semgrep moves to its own `taint` extra, so those pins are chosen rather than
inherited, and `pipx install semgrep` becomes the documented path: isolated
environment, binary on PATH, which is all `resolve_tool` ever wanted. Measured on
the built wheel: `[verify]` resolves to 15 packages, down from 68, with semgrep,
mcp, pywin32 and ruamel.yaml.clib all absent. `[taint]` still resolves to all 68.
SECOND HALF, AND THE REASON THE FIRST IS SAFE.
Removing semgrep from `verify` would have been the "extra that half-enables a
feature" mistake pyproject.toml itself warns about -- silently -- because
availability was decided by `resolve_tool(name) is not None`. That answers whether
a FILE is on PATH, not whether it can run.
The gap was not hypothetical. On the machine that hit the mcp downgrade, semgrep
resolved fine and its console script died with `OSError: [WinError 4551] An
Application Control policy has blocked this file`. `capabilities` reported `taint`
fully available, `unavailable: {}`, while every taint claim escalated to UNCERTAIN.
The command whose entire job is to say where the verifier is blind was blind to it,
which is worse than not having the command: a coverage report that overstates
coverage is trusted precisely because it is specific.
So `tool_unavailable_reason` probes by execution and the backends use it. A
non-zero exit from `--version` counts as unavailable too, deliberately: that
OSError was raised inside semgrep's own process, so nothing was raised here and the
only visible symptom was the exit code. Checking exceptions alone would have
reproduced the original bug. The probe is cached per process and has one caller,
`_capabilities`, so it costs one spawn per tool per audit.
Verified on the reporting machine: capabilities now names the blocked tool and its
WinError, and pyright and cargo still report available.
tests/test_extras_do_not_repin_the_environment.py pins the packaging decision, and
asserts the premise it rests on -- that nothing under cca_checks imports semgrep --
so a future change that makes it a real dependency fails here rather than leaving
the pipx advice quietly wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught it on the 3.10 job and nowhere else: the test reads pyproject.toml with `tomllib`, which landed in 3.11, while `requires-python` here is >=3.10. Collection failed with ModuleNotFoundError, so the whole suite errored out rather than one test failing. Falls back to `tomli`, added to the dev extra behind a `python_version < '3.11'` marker so no other interpreter installs a backport it will never import. The marker is the point: an unconditional dependency would have fixed 3.10 by giving every other version something to carry. Worth recording, because it nearly shipped: `gh pr checks --watch` exited 0 on the run where 3.10 had already FAILED. The failure was visible only in the per-check conclusions. Read those, not the exit code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported from a real install:
pip install 'cca-audit[verify]'downgraded a user'smcpfrom 2.1.0 to 1.29.0 and broke unrelated MCP tooling. Nothing in this project importsmcp.Why it is a bug, not bad luck
semgrep was never a library dependency.
semgrep_check.pyresolves it on PATH and spawns it, exactly likecargo, which nobody would think to pip-install. It sat inverifybecause "the whole deterministic layer in one install" read as tidy. It is not tidy when the price is rewriting packages unrelated to auditing.It moves to a
taintextra so the pins are chosen rather than inherited, andpipx install semgrepbecomes the documented path: isolated environment, binary on PATH, which is allresolve_toolever wanted.Measured on the built wheel, full resolution with
--ignore-installed:[verify]before[verify]now[taint]The second half, and why the first is safe
Dropping semgrep from
verifywould have been the "extra that half-enables a feature" mistakepyproject.tomlitself warns about, silently, because availability wasresolve_tool(name) is not None. That answers whether a file is on PATH, not whether it can run.That gap was live on the reporting machine. semgrep resolved fine, then died with:
and
capabilitiesreportedtaintfully available withunavailable: {}while every taint claim escalated to UNCERTAIN. The command whose entire job is to say where the verifier is blind was blind to it. A coverage report that overstates coverage is worse than none, because its specificity is what makes it trusted.So tools are now probed by execution. A non-zero exit from
--versioncounts as unavailable too, and that detail is load-bearing: theOSErrorwas raised inside semgrep's own process, so nothing was raised here and the only visible symptom was the exit code. An exception-only check would have reproduced the original bug.After, on the same machine:
pyrightandcargostill report available. The probe is cached per process and has one caller,_capabilities, so it costs one spawn per tool per audit.Tests
tests/test_extras_do_not_repin_the_environment.pypins the packaging decision and asserts the premise it rests on: that nothing undercca_checksimports semgrep. If that ever changes this fails, rather than leaving the pipx advice quietly wrong.tests/test_toolpath.pycovers absent, clean, unexecutable, non-zero-exit and hanging tools.ruff clean, pyright 0 errors, 85 tests pass locally.
🤖 Generated with Claude Code