fix(supply-chain): tell Dependabot why mcp is held below 2 — it proposed lifting it twice (INV-SUPPLY-2) - #63
Merged
Conversation
…sed lifting it twice (INV-SUPPLY-2) `mcp>=1.0,<2` was stated in three places Dependabot cannot read: a long pyproject.toml comment, INV-MCP-5 in docs/INVARIANTS.md, and test_mcp_version_bound.py. So it proposed lifting the bound twice in one week: PR #59 pyproject.toml mcp>=1.0,<2 -> mcp>=1.0,<3 (widens the range) PR #60 specialists/* mcp==1.28.1 -> mcp==2.0.0 (pins DEPLOYED containers) PR #60 came from the container coverage added last round (INV-SUPPLY-1) — the first PR that entry produced would have shipped a breaking version into the Runtime images. Both re-verified against the real mcp 2.0.0 release rather than trusted from the record, since a bound whose evidence expired is worse than no bound: from mcp.server import Server -> OK (proves nothing) Server("probe").list_tools / .call_tool -> GONE create_server() -> AttributeError: 'Server' object has no attribute 'list_tools' from mcp import ClientSession -> OK from mcp.client.streamable_http import streamablehttp_client -> ImportError The second pair is what could have been missed. The root bound is about the SERVER surface, which the specialists never touch — they use the CLIENT surface, so "the same bound applies" needed its own measurement. It held for an INDEPENDENT reason: all four specialists/*/agent_a2a.py import streamablehttp_client, which 2.0 also removed. Two separate breakages behind one version number. Fix: declare the bound in .github/dependabot.yml with its evidence and its lift procedure, generalising what iac-cdk's TypeScript bound already did right (INV-IAC) — "a fix applied to one call site is not an invariant" applied to a supply-chain declaration. Guard (test_breaking_bounds_are_told_to_dependabot.py) encodes the non-obvious detail: `ignore` is PER-UPDATE-BLOCK, not global, so the root pip entry's ignore does not cover the container pip entry — exactly why PR #60 existed — and a guard checking "mcp is ignored somewhere" would have passed while it was open. Every block of the relevant ecosystem is checked. Also asserted: each ignore carries a VERSION RANGE (a bare `ignore: mcp` would block 1.x security patches too, turning a compatibility bound into an unmaintained dependency); the pyproject bound and the ignore describe the SAME boundary (widening to <3 while the ignore stays >=2.0.0 lets the repo install a version Dependabot no longer warns about); and the file records HOW to re-verify, so the bound is lifted deliberately rather than becoming permanent by default. Mutation-tested 7/7, including the exact PR #59 and PR #60 conditions. Counts synced repo-wide (4039->4073 tests, 171->175 files, +6->+8 skipped): the file count drifted 4 past test_docs_drift's +-3 tolerance, which is the guard doing its job. Suite: 4074 passed / 8 skipped. ruff clean.
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.
The gap
mcp>=1.0,<2was stated in three places Dependabot cannot read — a longpyproject.tomlcomment, INV-MCP-5 indocs/INVARIANTS.md, andtests/test_mcp_version_bound.py. So it proposed lifting the bound twice in one week:pyproject.toml:mcp>=1.0,<2→mcp>=1.0,<3specialists/*:mcp==1.28.1→mcp==2.0.0PR #60 came from the container coverage added last round (INV-SUPPLY-1) — the very first PR that entry produced would have shipped a breaking version into the Runtime images.
Re-verified, not trusted from the record
A bound whose evidence has expired is worse than no bound, so both were measured against the real 2.0.0 release:
The first block confirms INV-MCP-5's own recorded lesson: an import check is not a compatibility check.
The second block is what could have been missed. The root bound is about the server surface, which the specialists never touch — they use the client surface, so "the same bound applies" was an assumption that needed its own measurement. It held, for an independent reason: all four
specialists/*/agent_a2a.pyimportstreamablehttp_client, which 2.0 also removed. Two separate breakages behind one version number.Fix
Declare the bound in
.github/dependabot.yml— the one file Dependabot reads — with its evidence and its lift procedure. This generalises whatiac-cdk's TypeScript bound already did right (INV-IAC): "a fix applied to one call site is not an invariant", applied to a supply-chain declaration.The non-obvious detail the guard encodes
ignoreis per-update-block, not global. The rootpipentry's ignore does not cover the containerpipentry — which is exactly why PR #60 existed — so a guard checking merely "mcp is ignored somewhere" would have passed while the dangerous PR was open. Every block of the relevant ecosystem is checked.Also asserted:
ignore: mcpsilently blocks 1.x security patches too, turning a compatibility bound into an unmaintained dependency.<3while the ignore stays at>=2.0.0is the worst combination: the repo permits a version Dependabot has stopped warning about — the guard that would complain is the one now disabled.Verification
<3while the ignore stays.dependabot-2.0JSON schema.4039→4073tests,171→175files,+6→+8skipped) — the file count had drifted 4 pasttest_docs_drift's ±3 tolerance, which is that guard doing its job.ruffclean.PRs #59 and #60 should be closed once this merges; the
ignoreentries stop them recurring.