Skip to content

fix: the verify extra must not repin a user's environment - #55

Merged
GiulioDER merged 2 commits into
masterfrom
fix/verify-extra-must-not-repin-the-environment
Aug 31, 2026
Merged

GiulioDER merged 2 commits into
masterfrom
fix/verify-extra-must-not-repin-the-environment

Conversation

@GiulioDER

@GiulioDER GiulioDER commented Aug 31, 2026 •

Copy link
Copy Markdown
Owner

Reported from a real install: 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.

semgrep 1.175.0 requires mcp==1.29.0, but you have mcp 2.1.1 which is incompatible.

Why it is a bug, not bad luck

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 verify because "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 taint extra so the 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, full resolution with --ignore-installed:

Extra Packages semgrep / mcp / pywin32 / ruamel.yaml.clib
[verify] before 68 all present
[verify] now 15 all absent
[taint] 68 present, by explicit opt-in

The second half, and why the first is safe

Dropping semgrep from verify would have been the "extra that half-enables a feature" mistake pyproject.toml itself warns about, silently, because availability was resolve_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:

OSError: [WinError 4551] An Application Control policy has blocked this file

and capabilities reported taint fully available with 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. 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 --version counts as unavailable too, and that detail is load-bearing: the OSError was 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:

"unavailable": {"taint": "semgrep at ...semgrep.EXE exited 1 on --version (OSError: [WinError 4551] An Application Control policy has blocked this file)"}

pyright and cargo still 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.py pins the packaging decision and asserts the premise it rests on: that nothing under cca_checks imports semgrep. If that ever changes this fails, rather than leaving the pipx advice quietly wrong.

tests/test_toolpath.py covers absent, clean, unexecutable, non-zero-exit and hanging tools.

ruff clean, pyright 0 errors, 85 tests pass locally.

🤖 Generated with Claude Code

GiulioDER and others added 2 commits August 31, 2026 13:37
`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.
@GiulioDER
GiulioDER merged commit a0f5994 into master Aug 31, 2026
5 checks passed
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