Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ dependencies = [
# urllib3 2.7.0 fixes GHSA-mf9v-mfxr-j63j (decompression-bomb bypass)
# and GHSA-qccp-gfcp-xxvc (header leak across origins).
"urllib3>=2.7.0,<3",
# cryptography is pulled in transitively by PyJWT[crypto]; pin it explicitly
# so the WeCom/Weixin crypto paths can't drift below the CVE-fixed floor.
"cryptography==46.0.7", # CVE-2026-39892, CVE-2026-34073
# Windows has no IANA tzdata shipped with the OS, so Python's ``zoneinfo``
# (PEP 615) raises ``ZoneInfoNotFoundError`` for every non-UTC timezone
# out of the box. ``tzdata`` ships the Olson database as a data package
Expand Down Expand Up @@ -155,10 +158,10 @@ modal = ["modal==1.3.4"]
daytona = ["daytona==0.155.0"]
hindsight = ["hindsight-client==0.6.1"]
dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "mcp==1.26.0", "starlette==1.0.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==81.0.0"] # starlette: CVE-2026-48710; setuptools: latest <82 (torch >=2.11 caps setuptools<82)
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.4", "brotlicffi==1.2.0.1", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"] # aiohttp: CVE-2026-34513/34518/34519/34520/34525
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.14.1", "brotlicffi==1.2.0.1", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"] # aiohttp 3.14.1: CVE-2026-34513/34518/34519/34520/34525 + 34993(RCE)/47265
cron = [] # croniter is now a core dependency; this extra kept for back-compat
slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.4"]
matrix = ["mautrix[encryption]==0.21.0", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"]
slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.14.1"]
matrix = ["mautrix[encryption]==0.21.0", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0", "aiohttp==3.14.1"] # aiohttp 3.14.1: CVE-2026-34993(RCE)/47265 + 34513/34518/34519/34520/34525 (mautrix/aiohttp-socks only cap aiohttp<4 / >=3.10, so pin the patched floor directly)
# WeCom callback-mode adapter — parses untrusted XML POST bodies from
# WeCom-controlled callback endpoints, so we use defusedxml (drop-in
# replacement for stdlib xml.etree.ElementTree) to block billion-laughs
Expand Down Expand Up @@ -203,9 +206,9 @@ vision = []
# a vulnerable pre-1.0.1 transitive. Bump in lockstep with uv.lock.
mcp = ["mcp==1.26.0", "starlette==1.0.1"] # starlette: CVE-2026-48710
nemo-relay = ["nemo-relay==0.3"]
homeassistant = ["aiohttp==3.13.4"]
sms = ["aiohttp==3.13.4"]
teams = ["microsoft-teams-apps==2.0.13.4", "aiohttp==3.13.4"]
homeassistant = ["aiohttp==3.14.1"]
sms = ["aiohttp==3.14.1"]
teams = ["microsoft-teams-apps==2.0.13.4", "aiohttp==3.14.1"] # aiohttp 3.14.1: CVE-2026-34993(RCE)/47265 + 34513/34518/34519/34520/34525
# Computer use — macOS background desktop control via cua-driver (MCP stdio).
# The cua-driver binary itself is installed via `hermes tools` post-setup
# (curl install script); this extra just pins the MCP client used to talk
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"peet.wannasarnmetha@gmail.com": "peetwan", # PR #51841 salvage (loopback ws-ping tuning + token-frame coalescing + loop heartbeat; #48445/#50005)
"297292863+Zyxxx-xxxyZ@users.noreply.github.com": "Zyxxx-xxxyZ", # PR #54287 salvage (route frontend-polled inline RPCs to _LONG_HANDLERS; #48445/#50005)
"kevenyanisme@gmail.com": "DataAdvisory", # PR #9562 salvage (flatten multi-part user_message in codex intermediate-ack detector so vision turns don't crash)
"huangsen365@gmail.com": "huangsen365", # PR #42334 (CVE dependency pins + pin-drift guard)
"telos@apex-z.com": "telos-oc", # PR #14353 salvage (propagate custom_providers key_env into ProviderDef.api_key_env_vars; named + bare-custom self-heal paths)
"256073454+Kolektori@users.noreply.github.com": "Kolektori", # PR #6436 salvage (require approval for host-bound Docker commands; container guard fast-path)
"41764686+LIC99@users.noreply.github.com": "LIC99", # PR #4682 salvage (warn + default to manual on unknown approvals.mode; #4261)
Expand Down
205 changes: 204 additions & 1 deletion tests/test_packaging_metadata.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path
import ast
import re
import tomllib
from pathlib import Path

import pytest

Expand Down Expand Up @@ -265,3 +266,205 @@ def test_locale_catalogs_ship_in_both_wheel_and_sdist():
on_disk = list((REPO_ROOT / "locales").glob("*.yaml"))
assert on_disk, "expected locales/*.yaml catalogs on disk"


# ---------------------------------------------------------------------------
# Dependency-pin consistency: pyproject extras <-> tools/lazy_deps.py
#
# The same package is exact-pinned in two hand-maintained places: the
# [project.optional-dependencies] extras in pyproject.toml and the LAZY_DEPS
# allowlist in tools/lazy_deps.py (the lazy-install path deliberately mirrors
# the extras — see the comments on LAZY_DEPS: "match the corresponding extra
# in pyproject.toml ... update both this map AND the corresponding extra").
#
# They have silently drifted more than once: the aiohttp Slack pin (3.13.3 in
# the extras vs 3.13.4 in lazy_deps) and the anthropic pin (0.86.0 vs 0.87.0).
# The version a user ends up with then depends on whether the backend was
# installed eagerly (extra) or lazily (lazy_deps) — and for a CVE bump applied
# to only one side, that divergence is a latent security regression. These two
# tests assert the documented contract: the two sources agree, in lockstep.
# ---------------------------------------------------------------------------

# Matches "name==version" and "name[extra]==version", ignoring any trailing
# environment marker / comment. Only exact pins are collected; ranged specs
# (">=", "<") can't be compared for equality and are skipped.
_PIN_RE = re.compile(
r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*==\s*([^\s;,#]+)"
)


def _canonical(name: str) -> str:
# PEP 503 normalization so e.g. discord.py / discord-py compare equal.
return re.sub(r"[-_.]+", "-", name).lower()


def _pins_from_specs(specs):
"""Map canonical package name -> set of exact-pinned versions seen."""
pins: dict[str, set[str]] = {}
for spec in specs:
m = _PIN_RE.match(spec)
if not m:
continue
pins.setdefault(_canonical(m.group(1)), set()).add(m.group(2))
return pins


def _pyproject_pinned_specs():
data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
specs = list(data["project"].get("dependencies", []))
for extra in data["project"].get("optional-dependencies", {}).values():
specs.extend(extra)
return specs


def _lazy_deps_pinned_specs():
"""Extract every string literal inside the LAZY_DEPS dict via AST.

Parsing rather than importing keeps this test free of
tools/lazy_deps.py's runtime imports and side effects.
"""
src = (REPO_ROOT / "tools" / "lazy_deps.py").read_text(encoding="utf-8")
tree = ast.parse(src)
specs: list[str] = []
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
targets = node.targets
elif isinstance(node, ast.AnnAssign):
targets = [node.target]
else:
continue
if not any(isinstance(t, ast.Name) and t.id == "LAZY_DEPS" for t in targets):
continue
for sub in ast.walk(node.value):
if isinstance(sub, ast.Constant) and isinstance(sub.value, str):
specs.append(sub.value)
assert specs, "could not extract specs from LAZY_DEPS — the AST parser drifted"
return specs


def test_pyproject_pins_are_internally_consistent():
"""No package may be exact-pinned to two different versions in pyproject.

A package legitimately appearing in several extras (e.g. aiohttp in
messaging/slack/homeassistant/sms) must use the SAME version everywhere.
"""
pins = _pins_from_specs(_pyproject_pinned_specs())
conflicts = {name: sorted(v) for name, v in pins.items() if len(v) > 1}
assert not conflicts, (
"pyproject.toml exact-pins the same package to different versions "
"across [project.dependencies] / extras: " + str(conflicts)
)


def test_pyproject_and_lazy_deps_pins_agree():
"""Every package pinned in BOTH places must use the same version.

Regression guard for the aiohttp / anthropic extras-vs-lazy drift:
tools/lazy_deps.py mirrors the pyproject extras, so a CVE bump applied to
one and not the other leaves users on a vulnerable version depending on
the install path. Bump both in lockstep.
"""
py = _pins_from_specs(_pyproject_pinned_specs())
lazy = _pins_from_specs(_lazy_deps_pinned_specs())

mismatches = [
f"{name}: pyproject={sorted(py[name])} lazy_deps={sorted(lazy[name])}"
for name in sorted(set(py) & set(lazy))
if py[name] != lazy[name]
]
assert not mismatches, (
"pyproject.toml extras and tools/lazy_deps.py disagree on the pinned "
"version of the same package — bump both in lockstep:\n "
+ "\n ".join(mismatches)
)


def _lazy_deps_by_feature():
"""Parse LAZY_DEPS into {feature_name: [spec, ...]} via AST.

Same parse-don't-import rationale as _lazy_deps_pinned_specs, but keeps the
feature -> specs grouping so per-feature coverage can be asserted.
"""
src = (REPO_ROOT / "tools" / "lazy_deps.py").read_text(encoding="utf-8")
tree = ast.parse(src)
for node in ast.walk(tree):
targets = (
node.targets if isinstance(node, ast.Assign)
else [node.target] if isinstance(node, ast.AnnAssign)
else []
)
if not any(isinstance(t, ast.Name) and t.id == "LAZY_DEPS" for t in targets):
continue
if not isinstance(node.value, ast.Dict):
continue
by_feature: dict[str, list[str]] = {}
for key, value in zip(node.value.keys, node.value.values):
if not (isinstance(key, ast.Constant) and isinstance(key.value, str)):
continue
by_feature[key.value] = [
sub.value
for sub in ast.walk(value)
if isinstance(sub, ast.Constant) and isinstance(sub.value, str)
]
assert by_feature, "could not extract features from LAZY_DEPS — AST parser drifted"
return by_feature
raise AssertionError("LAZY_DEPS dict literal not found in tools/lazy_deps.py")


# Security-critical packages whose patched floor must be enforced on EVERY
# install path, eager and lazy. test_pyproject_and_lazy_deps_pins_agree only
# fires when a package is pinned in BOTH sources, so it cannot catch a lazy
# feature that omits the pin entirely — the exact gap that left platform.slack
# carrying aiohttp==3.14.0 while platform.discord (whose discord.py dep pulls
# aiohttp transitively as its HTTP backbone) shipped without it, so the lazy
# Discord path could keep an already-installed vulnerable aiohttp. A fully
# general "no mirrored feature drops a pin" check is impossible statically
# (it can't see transitive deps), so this is the explicit coverage contract:
# each security package -> the lazy features that bundle an SDK pulling it and
# must therefore carry the same pin as the pyproject extra.
_REQUIRED_SECURITY_PINS = {
# Every lazy messaging feature whose SDK pulls aiohttp transitively must
# carry the patched floor directly: discord.py (aiohttp<4), slack-bolt,
# mautrix/aiohttp-socks (aiohttp<4 / >=3.10), and microsoft-teams-apps —
# none of those upper/lower bounds excludes a vulnerable already-installed
# aiohttp, so the lazy path would not upgrade it without an explicit pin.
"aiohttp": {
"platform.discord",
"platform.slack",
"platform.matrix",
"platform.teams",
},
}


def test_security_pins_present_in_mirrored_lazy_features():
"""Curated security pins must be present (not just version-consistent) in
every lazy feature that bundles an SDK pulling that package transitively.
"""
py = _pins_from_specs(_pyproject_pinned_specs())
by_feature = _lazy_deps_by_feature()

problems = []
for pkg, features in _REQUIRED_SECURITY_PINS.items():
canon = _canonical(pkg)
expected = py.get(canon)
assert expected, (
f"{pkg} is listed in _REQUIRED_SECURITY_PINS but is not exact-pinned "
f"in pyproject.toml — update the map or the pin."
)
for feature in sorted(features):
specs = by_feature.get(feature)
assert specs is not None, (
f"lazy feature {feature!r} named in _REQUIRED_SECURITY_PINS no "
f"longer exists in LAZY_DEPS — update the map."
)
got = _pins_from_specs(specs).get(canon)
if got != expected:
problems.append(
f"{feature}: {pkg}="
f"{sorted(got) if got else 'MISSING'}, expected {sorted(expected)}"
)
assert not problems, (
"a lazy feature is missing a security pin it must mirror from the "
"pyproject extras — the lazy install path would not enforce the "
"CVE-patched floor:\n " + "\n ".join(problems)
)
18 changes: 15 additions & 3 deletions tools/lazy_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,29 @@
# back to google's `Brotli` package (1-arg API), and any .txt/.md/.doc
# uploaded to the Discord gateway fails to decode at att.read() with
# "Can not decode content-encoding: br" — see #12511 / #15744.
"platform.discord": ("discord.py[voice]==2.7.1", "brotlicffi==1.2.0.1"),
"platform.discord": (
"discord.py[voice]==2.7.1",
"brotlicffi==1.2.0.1",
# discord.py pulls aiohttp transitively (>=3.7.4,<4) as its HTTP
# backbone. Pin the patched floor here too so the lazy Discord path
# can't keep an already-installed vulnerable aiohttp satisfying that
# range — mirrors the messaging extra and platform.slack.
"aiohttp==3.14.1", # CVE-2026-34513/34518/34519/34520/34525 + 34993(RCE)/47265
),
"platform.slack": (
"slack-bolt==1.27.0",
"slack-sdk==3.40.1",
"aiohttp==3.13.4", # CVE-2026-34513/34518/34519/34520/34525
"aiohttp==3.14.1", # CVE-2026-34513/34518/34519/34520/34525 + 34993(RCE)/47265
),
"platform.matrix": (
"mautrix[encryption]==0.21.0",
"aiosqlite==0.22.1",
"asyncpg==0.31.0",
"aiohttp-socks==0.11.0",
# mautrix (aiohttp>=3,<4) and aiohttp-socks (aiohttp>=3.10.0) only cap
# aiohttp transitively, so a vulnerable already-installed aiohttp still
# satisfies both — pin the patched floor here too, like platform.discord.
"aiohttp==3.14.1", # CVE-2026-34513/34518/34519/34520/34525 + 34993(RCE)/47265
),
"platform.dingtalk": (
"dingtalk-stream==0.24.3",
Expand All @@ -183,7 +195,7 @@
# (microsoft-teams-api/cards/common, dependency-injector, msal). Lazy-
# installed on demand like every other messaging platform; also exposed
# as the `teams` extra in pyproject for packagers / explicit installs.
"platform.teams": ("microsoft-teams-apps==2.0.13.4", "aiohttp==3.13.4"),
"platform.teams": ("microsoft-teams-apps==2.0.13.4", "aiohttp==3.14.1"), # aiohttp 3.14.1: CVE-2026-34993(RCE)/47265 + 34513/34518/34519/34520/34525

# ─── Terminal backends ─────────────────────────────────────────────────
"terminal.modal": ("modal==1.3.4",),
Expand Down
Loading
Loading