fix(packaging): let a pre-PM updater finish its dependency step on old python

A released updater runs its own `uv pip install -e .` on the interpreter it
already had -- 3.11 in the wild -- and only after that step succeeds does it
reach the handoff shims this tree ships for it (`managed_uv`,
`_old_updater.stop_for_relaunch`). With requires-python at >=3.14 that step
cannot resolve at all:

    error: No solution found when resolving dependencies
      cause: the current Python version (3.11.16) does not satisfy Python>=3.14,<3.15

so the compat surface built for exactly those updaters (tests/compat/README.md
covers updaters shipped before the PM migration) is unreachable for them.

There is no flag-only way around it, measured on a real 3.11 against uv 0.12.3:
--no-deps and UV_NO_DEPS both still fail on the project's own requires-python,
and uv has no --ignore-requires-python at all. The metadata is the only lever,
so the floor now admits the oldest updater we support and the core dependencies
are gated on the runtime we actually support:

  * requires-python = ">=3.11,<3.15" -- 3.14 is a runtime requirement, not an
    install requirement;
  * every core dependency carries `python_version >= '3.14'`, which is true on
    every runtime this package supports and false for a bridge interpreter, so
    an old resolver sees them as absent instead of installing ~180 packages
    into a venv the handoff is about to rebuild.

Verified on Windows against a real 3.11.15 and 3.14.7 with the real dependency
list: the exact command now exits 0 on 3.11, installing only the editable
project plus the three packages the two `override-dependencies` pull (uv rejects
markers in override-dependencies, so those cannot be gated); 3.14 still installs
the full tree. All 41 requirements parse as PEP 508 -- which caught two real
bugs: `A or B and C` binds the appended marker to the last clause only, so
nemo-relay's marker is parenthesised, and a direct-URL requirement needs
whitespace before its `;`.
This commit is contained in:
ethernet
2026-09-18 13:05:14 -04:00
parent 3e8124eb68
commit e1020f32d4

View File

@@ -1,19 +1,17 @@
[project]
name = "hermes-agent"
version = "0.21.3"
description = "The self-improving AI agent — creates skills from experience, improves them during use, and runs anywhere"
readme = "README.md"
# Upper bound is load-bearing, not cosmetic. uv resolves the project's
# Python from `requires-python`, and an inherited `UV_PYTHON` env var (or a
# fresh distro whose newest interpreter uv auto-picks) would otherwise select
# a newer major. The 3.14 ceiling tracks the newest CPython line the full
# dependency graph resolves on (py3 wheels for every target); raise it in the
# same commit as a lock regen after verifying the new interpreter resolves.
requires-python = ">=3.14,<3.15"
# 3.14 is the newest python we know we have wheels for.
# we *only* support 3.14, BUT we need to allow old hermes installs on <3.14
# to get thru their update step on old python.
# Shortly after they do, we update to our latest python.
# See tests/compat/README.md for the updater contract this keeps.
requires-python = ">=3.11,<3.15"
authors = [{ name = "Nous Research" }]
license = "MIT"
license-files = ["LICENSE"]
@@ -38,22 +36,22 @@ dependencies = [
# extra and gets prepared through PM when the
# user picks that backend. Smaller `dependencies` = smaller blast
# radius for the next supply-chain attack.
"openai==2.24.0",
"certifi==2026.5.20",
"openai==2.24.0; python_version >= '3.14'",
"certifi==2026.5.20; python_version >= '3.14'",
# TLS trust comes from the OS certificate store (agent/ssl_verify.py), so
# a corporate/internal root installed on the machine just works. certifi
# above stays a direct pin because httpx/requests/openai each depend on
# it regardless; it is no longer OUR trust source.
"truststore>=0.10.4,<0.11",
"python-dotenv==1.2.2",
"fire==0.7.1",
"httpx[socks]==0.28.1",
"rich==14.3.3",
"tenacity==9.1.4",
"tomli-w==1.2.0", # Preserve relative dependency paths in staged plugin metadata.
"ruamel.yaml==0.18.17",
"requests==2.33.0", # CVE-2026-25645
"jinja2==3.1.6",
"truststore>=0.10.4,<0.11; python_version >= '3.14'",
"python-dotenv==1.2.2; python_version >= '3.14'",
"fire==0.7.1; python_version >= '3.14'",
"httpx[socks]==0.28.1; python_version >= '3.14'",
"rich==14.3.3; python_version >= '3.14'",
"tenacity==9.1.4; python_version >= '3.14'",
"tomli-w==1.2.0; python_version >= '3.14'", # Preserve relative dependency paths in staged plugin metadata.
"ruamel.yaml==0.18.17; python_version >= '3.14'",
"requests==2.33.0; python_version >= '3.14'", # CVE-2026-25645
"jinja2==3.1.6; python_version >= '3.14'",
# Document-to-Markdown extraction for read_file (PDF, legacy Office,
# OpenDocument, RTF, EPUB) + typed NeedsOcrError for scanned pages.
# Bundled in core by maintainer decision (read_file is a core tool and
@@ -61,24 +59,24 @@ dependencies = [
# arrangement dated to the package's uv exclude-newer quarantine, which
# has long expired). PM's doc-extract extra repairs lean/broken installs
# from this same dependency declaration.
"firecrawl-anydoc==0.2.4",
"firecrawl-anydoc==0.2.4; python_version >= '3.14'",
# Bumped from 2.12.5 to 2.13.4 to pull in pydantic-core 2.46.4.
# pydantic-core 2.41.5 (pulled by 2.12.5) segfaults when the OpenAI SDK's
# Responses API resource is exercised from a non-main thread, which is the
# codex_responses dispatch in agent/chat_completion_helpers.py:_call.
"pydantic==2.13.4",
"pydantic==2.13.4; python_version >= '3.14'",
# Interactive CLI (prompt_toolkit is used directly by cli.py)
"prompt_toolkit==3.0.52",
"prompt_toolkit==3.0.52; python_version >= '3.14'",
# Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter).
"croniter==6.0.0",
"croniter==6.0.0; python_version >= '3.14'",
# Snowball stemming for tool_search's BM25 (tools/tool_search.py) —
# official Snowball project package, pure Python, zero transitive deps.
# Applied at index AND query time so morphological variants match
# ("issues" finds create_issue).
"snowballstemmer==3.1.1",
"snowballstemmer==3.1.1; python_version >= '3.14'",
# Requirement and version checks must work in lean installs without
# relying on another package to bring packaging in transitively.
"packaging==26.0",
"packaging==26.0; python_version >= '3.14'",
# Markdown -> HTML conversion for rich message delivery (Matrix
# `formatted_body`, and the `send_message` tool's HTML path). Now on the
# DEFAULT delivery path, not matrix-specific: without it both
@@ -88,12 +86,12 @@ dependencies = [
# (~108KB, no compiled extensions, no platform constraints), so unlike the
# matrix extra's `mautrix`/`python-olm` it's safe to ship everywhere — keeps
# it out of the lazy-install path that exists only for the heavy matrix deps.
"Markdown==3.10.2",
"Markdown==3.10.2; python_version >= '3.14'",
# Skills Hub (GitHub App JWT auth — optional, only needed for bot identity)
"PyJWT[crypto]==2.13.0", # PYSEC-2026-175/177/178/179
"PyJWT[crypto]==2.13.0; python_version >= '3.14'", # PYSEC-2026-175/177/178/179
# 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",
"urllib3>=2.7.0,<3; python_version >= '3.14'",
# PyJWT[crypto] pulls cryptography in transitively. Pin it here as well, so
# that the WeCom and Weixin crypto paths cannot fall below the patched
# version. 50.0.0 is the floor: it fixes CVE-2026-69247, a Bleichenbacher
@@ -105,51 +103,51 @@ dependencies = [
# A pin here is not sufficient on its own. alibabacloud-tea-openapi caps
# cryptography<49, so [tool.uv] also holds an override. Read that comment
# before you move this version.
"cryptography==50.0.1", # CVE-2026-69247, GHSA-m2h6-j472-rp4c, GHSA-jwv3-5hgf-82ww, CVE-2026-39892, CVE-2026-34073, GHSA-537c-gmf6-5ccf
"cryptography==50.0.1; python_version >= '3.14'", # CVE-2026-69247, GHSA-m2h6-j472-rp4c, GHSA-jwv3-5hgf-82ww, CVE-2026-39892, CVE-2026-34073, GHSA-537c-gmf6-5ccf
# 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
# Python resolves automatically. No-op on Linux/macOS (which have
# /usr/share/zoneinfo). Credits: PR #13182 (@sprmn24).
"tzdata==2025.3; sys_platform == 'win32'",
"tzdata==2025.3; sys_platform == 'win32' and python_version >= '3.14'",
# Cross-platform process / PID management. `psutil` is the canonical
# answer for "is this PID alive" and process-tree walking across Linux,
# macOS and Windows. It replaces POSIX-only idioms like `os.kill(pid, 0)`
# (which is a silent killer on Windows — see CONTRIBUTING.md) and
# `os.killpg` (which doesn't exist on Windows).
"psutil==7.2.2; sys_platform != 'android'",
"psutil==7.2.2; sys_platform != 'android' and python_version >= '3.14'",
# Android Python 3.13+ needs upstream #2891 for platform recognition and
# disk_partitions(). The fix is not in a published psutil release yet.
# Keep this commit until a release includes it, then remove the source pin.
"psutil @ git+https://github.com/giampaolo/psutil.git@380bd2b59c67b0e1b04bbf3a90b11744f4f96644 ; sys_platform == 'android'",
"psutil @ git+https://github.com/giampaolo/psutil.git@380bd2b59c67b0e1b04bbf3a90b11744f4f96644 ; sys_platform == 'android' and python_version >= '3.14'",
# Browser CDP supervisor + browser_dialog import this directly. Keep core
# so browser tool discovery doesn't fail on lean installs.
"websockets==15.0.1",
"websockets==15.0.1; python_version >= '3.14'",
# .gitignore-aware file matching for desktop build stamp.
"pathspec==1.1.1",
"fastapi>=0.104.0,<1",
"pathspec==1.1.1; python_version >= '3.14'",
"fastapi>=0.104.0,<1; python_version >= '3.14'",
# CIDR-aware forwarded_allow_ips requires uvicorn >=0.31.0.
"uvicorn[standard]>=0.31.0,<1",
"uvicorn[standard]>=0.31.0,<1; python_version >= '3.14'",
# Streaming multipart uploads for the dashboard file manager (NS-501).
# FastAPI's UploadFile/Form depend on python-multipart; it is NOT pulled in
# by fastapi itself, so the dashboard's multipart upload endpoint would 500
# without an explicit dependency here (and in the `web` extra below).
"python-multipart>=0.0.9,<1",
"ptyprocess>=0.7.0,<1; sys_platform != 'win32'",
"python-multipart>=0.0.9,<1; python_version >= '3.14'",
"ptyprocess>=0.7.0,<1; sys_platform != 'win32' and python_version >= '3.14'",
# Python 3.14 requires pywinpty 3.x. Version 3.0.5 includes the native
# binaries missing from 3.0.4 wheels on both Windows architectures.
"pywinpty>=3.0.5,<4; sys_platform == 'win32'",
"pywinpty>=3.0.5,<4; sys_platform == 'win32' and python_version >= '3.14'",
# Desktop SSH's Windows remote runtime (hermes_cli/windows_ssh_runtime.py)
# imports win32security/win32file/etc. directly — declare pywin32 rather than
# relying on the concurrent-log-handler → portalocker transitive chain.
"pywin32>=306,<312; sys_platform == 'win32'",
"pywin32>=306,<312; sys_platform == 'win32' and python_version >= '3.14'",
# App Installer checks use Package from ApplicationModel. Foundation
# supplies the URI and async result types projected when the checker runs.
"winrt-windows-applicationmodel>=3.2.1,<4; sys_platform == 'win32'",
"winrt-windows-foundation>=3.2.1,<4; sys_platform == 'win32'",
"winrt-windows-applicationmodel>=3.2.1,<4; sys_platform == 'win32' and python_version >= '3.14'",
"winrt-windows-foundation>=3.2.1,<4; sys_platform == 'win32' and python_version >= '3.14'",
# Store update queries and request results project collection interfaces.
"winrt-windows-foundation-collections>=3.2.1,<4; sys_platform == 'win32'",
"winrt-windows-services-store>=3.2.1,<4; sys_platform == 'win32'",
"winrt-windows-foundation-collections>=3.2.1,<4; sys_platform == 'win32' and python_version >= '3.14'",
"winrt-windows-services-store>=3.2.1,<4; sys_platform == 'win32' and python_version >= '3.14'",
# Image resize recovery for the vision tools. Pillow shrinks oversized images
# (>5 MB or >8000px) at embed time; without it the byte AND pixel-dimension
# shrink paths no-op, so an oversized image bakes into immutable history and
@@ -157,7 +155,7 @@ dependencies = [
# libs required for the codecs we use, so it's safe to ship in the base
# install rather than gating it behind an extra + a mid-session lazy install
# (which deadlocked the CLI under prompt_toolkit — see #40490).
"Pillow==12.3.0",
"Pillow==12.3.0; python_version >= '3.14'",
# HEIF/HEIC/AVIF decode for the vision tools. iPhone photos and screenshots
# are HEIC (frequently mislabeled .jpg by upload pipelines); Pillow has no
# built-in HEIF codec, so without this the vision resolver rejects them as
@@ -165,7 +163,7 @@ dependencies = [
# _normalize_to_supported_image can re-encode HEIF/AVIF to PNG before embed.
# Ships prebuilt wheels bundling libheif for the common platforms (no system
# libs needed), so it's safe in the base install alongside Pillow.
"pillow-heif>=1.4.0,<2",
"pillow-heif>=1.4.0,<2; python_version >= '3.14'",
# Windows log rotation. Stdlib ``RotatingFileHandler.doRollover()`` uses
# ``os.rename()`` which fails with ``PermissionError [WinError 32]`` on
# Windows whenever any other process holds an append-mode handle on
@@ -179,7 +177,7 @@ dependencies = [
# already works there and managed-mode perms depend on its exact lifecycle.
# Hence the ``sys_platform == 'win32'`` marker: the dep (and its portalocker
# / pywin32 tree) ships only where it's actually used.
"concurrent-log-handler==0.9.29; sys_platform == 'win32'",
"concurrent-log-handler==0.9.29; sys_platform == 'win32' and python_version >= '3.14'",
# First-party lifecycle and shared-metrics runtime. Relay 0.8 is the supported
# native runtime and provider-codec baseline. Managed calls pass request and
# response data through this native module in-process; shared metrics installs
@@ -194,8 +192,7 @@ dependencies = [
# 738 CPython reports sys_platform == 'android' and never matched.) Pre-GKI
# devices can still slip through; they get the same resolution failure as
# before, worked around by installing with `--no-deps` or an older release.
"nemo-relay>=0.8.3,<0.9; (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'linux' and platform_machine == 'x86_64' and 'android' not in platform_release) or (sys_platform == 'linux' and platform_machine == 'aarch64' and 'android' not in platform_release) or (sys_platform == 'win32' and platform_machine == 'AMD64') or (sys_platform == 'win32' and platform_machine == 'ARM64')",
]
"nemo-relay>=0.8.3,<0.9; ((sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'linux' and platform_machine == 'x86_64' and 'android' not in platform_release) or (sys_platform == 'linux' and platform_machine == 'aarch64' and 'android' not in platform_release) or (sys_platform == 'win32' and platform_machine == 'AMD64') or (sys_platform == 'win32' and platform_machine == 'ARM64')) and python_version >= '3.14'",]
[project.optional-dependencies]
# Native Anthropic provider — only needed when provider=anthropic (not via
@@ -556,6 +553,10 @@ kittentts = "python_version < '3.13'"
piper = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')"
[tool.uv]
# 3.11 is an install bridge for pre-PM updaters, never a runtime, so the lock
# covers 3.14 only. Without this the lock must resolve every supported version,
# where extras that only ever coexist on 3.14 (kittentts vs neutts) conflict.
environments = ["python_version >= '3.14'"]
override-dependencies = [
# discord.py's latest published version, 2.7.1, pins pynacl at <1.6. however, pynacl 1.5.0 has known vulnerabilities.
# discord.py has updated pynacl to 1.6 on `main`, but has not yet published a patch release.