fix(deps): align anthropic extra pin with lazy pin + guard whole pin surface (#42335)

The anthropic extra pinned anthropic==0.86.0 while LAZY_DEPS['provider.anthropic']
pins 0.87.0 (CVE-2026-34450, CVE-2026-34452) — the same drift class as the
aiohttp #31817 downgrade. On hermes update the extra pin won and rolled
anthropic 0.87.0 -> 0.86.0, reopening both CVEs until the native-Anthropic
lazy refresh re-bumped it.

Bump the extra to 0.87.0, regenerate uv.lock, and generalize the regression
guard: test_pyproject_pins_match_lazy_deps_pins now fails if ANY package
pinned in both a pyproject extra and a LAZY_DEPS entry drifts, so a third
package can't reintroduce this class. The aiohttp-specific test is kept for
focused #31817 coverage.
This commit is contained in:
Teknium 2026-06-08 12:11:54 -07:00 committed by GitHub
parent c78b3e1d3c
commit 2f510ca8e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 56 additions and 5 deletions

View file

@ -104,7 +104,7 @@ dependencies = [
[project.optional-dependencies]
# Native Anthropic provider — only needed when provider=anthropic (not via
# OpenRouter or other aggregators).
anthropic = ["anthropic==0.86.0"]
anthropic = ["anthropic==0.87.0"] # CVE-2026-34450, CVE-2026-34452
# Web search backends — each only loaded when the user picks it as their
# search provider (configured via `hermes tools` or config.yaml).
exa = ["exa-py==2.10.2"]

View file

@ -134,6 +134,57 @@ def test_pyproject_aiohttp_pins_match_lazy_slack_pin():
)
def test_pyproject_pins_match_lazy_deps_pins():
"""Generalize #31817 to the whole pin surface, not just aiohttp.
Any package that is exact-pinned in BOTH a pyproject extra and a
`tools/lazy_deps.py` LAZY_DEPS entry must use the SAME version in both
places. When they drift, `hermes update` resolves the pyproject extra
pin and downgrades the package to the older version, reopening whatever
the lazy pin fixed (the aiohttp #31817 case, and the anthropic
CVE-2026-34450/34452 case found alongside it) only for the lazy
refresh to re-upgrade it on next feature use. The lazy pin is the
security-current source of truth; extras must track it.
"""
from tools.lazy_deps import LAZY_DEPS
optional_dependencies = _load_optional_dependencies()
# package -> version, as pinned across all pyproject extras. If an
# extra pins a package at a different version than another extra, that
# is itself a bug (caught below); here we just collect the set.
pyproject_pins: dict[str, set[str]] = {}
for specs in optional_dependencies.values():
for package, version in _exact_pins(specs).items():
pyproject_pins.setdefault(package, set()).add(version)
# package -> version, as pinned across all LAZY_DEPS entries.
lazy_pins: dict[str, set[str]] = {}
for specs in LAZY_DEPS.values():
if isinstance(specs, str):
specs = (specs,)
for package, version in _exact_pins(specs).items():
lazy_pins.setdefault(package, set()).add(version)
shared = sorted(set(pyproject_pins) & set(lazy_pins))
assert shared, "expected at least one package pinned in both pyproject and LAZY_DEPS"
drift = {
package: {
"pyproject": sorted(pyproject_pins[package]),
"lazy_deps": sorted(lazy_pins[package]),
}
for package in shared
if pyproject_pins[package] != lazy_pins[package]
}
assert not drift, (
"pyproject extras pins must match tools/lazy_deps.py LAZY_DEPS pins "
"for every shared package — otherwise `hermes update` downgrades the "
"package below the security-current lazy pin (see #31817). Drift: "
f"{drift}"
)
def test_dev_extra_excluded_from_all():
"""End-user installs should not pull test/lint/debug tooling."""
optional_dependencies = _load_optional_dependencies()

8
uv.lock generated
View file

@ -285,7 +285,7 @@ wheels = [
[[package]]
name = "anthropic"
version = "0.86.0"
version = "0.87.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -297,9 +297,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/8f/3281edf7c35cbac169810e5388eb9b38678c7ea9867c2d331237bd5dff08/anthropic-0.87.0.tar.gz", hash = "sha256:098fef3753cdd3c0daa86f95efb9c8d03a798d45c5170329525bb4653f6702d0", size = 588982, upload-time = "2026-03-31T17:52:41.697Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" },
{ url = "https://files.pythonhosted.org/packages/0d/02/99bf351933bdea0545a2b6e2d812ed878899e9a95f618351dfa3d0de0e69/anthropic-0.87.0-py3-none-any.whl", hash = "sha256:e2669b86d42c739d3df163f873c51719552e263a3d85179297180fb4fa00a236", size = 472126, upload-time = "2026-03-31T17:52:40.174Z" },
]
[[package]]
@ -1591,7 +1591,7 @@ requires-dist = [
{ name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = "==0.11.0" },
{ name = "aiosqlite", marker = "extra == 'matrix'", specifier = "==0.22.1" },
{ name = "alibabacloud-dingtalk", marker = "extra == 'dingtalk'", specifier = "==2.2.42" },
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = "==0.86.0" },
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = "==0.87.0" },
{ name = "asyncpg", marker = "extra == 'matrix'", specifier = "==0.31.0" },
{ name = "azure-identity", marker = "extra == 'azure-identity'", specifier = "==1.25.3" },
{ name = "boto3", marker = "extra == 'bedrock'", specifier = "==1.42.89" },