From 2f510ca8e07b570ad3fdc491400432a96494aaf3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:11:54 -0700 Subject: [PATCH] fix(deps): align anthropic extra pin with lazy pin + guard whole pin surface (#42335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pyproject.toml | 2 +- tests/test_project_metadata.py | 51 ++++++++++++++++++++++++++++++++++ uv.lock | 8 +++--- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e27c6b89bc4..54a54da0409 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index c2ab232afe3..6c761cb2cdb 100644 --- a/tests/test_project_metadata.py +++ b/tests/test_project_metadata.py @@ -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() diff --git a/uv.lock b/uv.lock index bb13f620a41..e7d487bf636 100644 --- a/uv.lock +++ b/uv.lock @@ -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" },