Files
hermes-agent/pyproject.toml

925 lines
46 KiB
TOML

[project]
name = "hermes-agent"
version = "0.0.0"
description = "The self-improving AI agent — creates skills from experience, improves them during use, and runs anywhere"
readme = "README.md"
# 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"]
dependencies = [
# Core — every direct dep is exact-pinned to ==X.Y.Z (no ranges).
# Rationale: ranges allow PyPI to ship a fresh version of a transitive
# at any time without a code review on our side. Exact pins mean the
# only way a new package version reaches a user is via an intentional
# update on our end (bump the pin in this file, regenerate uv.lock).
# This was tightened on 2026-05-12 in response to the Mini Shai-Hulud
# worm hitting mistralai 2.4.6 on PyPI; if that release had been
# captured by `mistralai>=2.3.0,<3` rather than an exact pin, every
# install in the hours before the quarantine would have pulled it.
#
# After a pin change, run `python -m pm.build_env --source . --lock-only`
# so the transitive resolution stays consistent. Do not introduce ranges
# without a written justification.
#
# Scope rule: only packages used by EVERY hermes session belong here.
# Anything that's provider-specific (`anthropic`, `firecrawl-py`,
# `exa-py`, `fal-client`, `edge-tts`, `parallel-web`) belongs in an
# 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; 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.
# ssl_verify uses truststore's private original SSLContext for explicit CA overrides.
# Keep this exact until that path has a public-API replacement.
"truststore==0.10.4; 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.
# 0.18.17 requires ruamel.yaml.clib on 3.14, which has no Windows ARM64 wheel.
"ruamel.yaml==0.18.16; 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
# PDF reads are a common first-session action; the previous lazy-only
# 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; 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; python_version >= '3.14'",
# Interactive CLI (prompt_toolkit is used directly by cli.py)
"prompt_toolkit==3.0.52; python_version >= '3.14'",
# Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter).
"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; 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; 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
# gateway/platforms/matrix.py and tools/send_message_tool.py silently fall
# back to plain text, so cron/agent deliveries render raw `##`/`**`/tables
# in clients like Element (see #32486). Pure-Python py3-none-any wheel
# (~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; python_version >= '3.14'",
# Skills Hub (GitHub App JWT auth — optional, only needed for bot identity)
"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; 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
# oracle in PKCS#7 EnvelopedData, and 49.0.0 fixed GHSA-m2h6-j472-rp4c,
# where a wildcard DNS name escapes permittedSubtrees, and
# GHSA-jwv3-5hgf-82ww, exponential path-building on duplicate self-signed
# intermediates.
#
# 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; 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' 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' 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' 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; python_version >= '3.14'",
# .gitignore-aware file matching for desktop build stamp.
"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's [standard] extra bundles uvloop, httptools, and watchfiles.
# uvloop cannot build on Android/Termux (libuv's ./configure fails), so
# we declare the working components inline and gate uvloop behind the
# [uvloop] optional extra — desktop/server profiles pull it via [all].
"uvicorn>=0.31.0,<1; python_version >= '3.14'",
"httptools>=0.6.3,<0.9; python_version >= '3.14'",
"watchfiles>=0.20,<2; 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; 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' 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' 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' 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' 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
# bricks the session on Anthropic's non-retryable 400. Pure-wheel, no system
# 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; 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
# "not a recognized image". pillow-heif registers a Pillow opener so
# _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; python_version >= '3.14'",
# SVG rendering for the icon generator (scripts/generate_icons.py). Source
# builds render icons with the runtime interpreter (`hermes desktop`,
# `hermes update`), so a source install carries its renderer instead of
# preparing a separate environment at launch. abi3 wheels for every
# shipped platform, including Windows ARM64 and Android.
"resvg-py==0.4.0; 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
# ``agent.log`` (always the case in Hermes — TUI, gateway, ``hy_memory``
# server, MCP servers, and on-demand CLI commands all log from separate
# processes), pinning ``agent.log`` at the 5 MiB threshold and spamming
# stderr on every emit (see #44873). ``concurrent-log-handler`` wraps the
# rename in a cross-process file lock (via ``portalocker``: pywin32 on
# Windows) so only one process rotates at a time. ``hermes_logging.py``
# aliases it ONLY on Windows — POSIX renames an open file fine, so stdlib
# 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' 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
# no network exporter and consumes only its bounded projection. This marker
# must stay false anywhere no compatible native wheel tag can match; otherwise
# installing Python dependencies fails instead of falling back to the no-op
# Relay host (#76469, Termux).
# Termux Python reports plain linux/aarch64 but runs on Bionic
# libc, which satisfies neither manylinux nor musllinux, hence the
# `'android' not in platform_release` guard on the linux arms: Android GKI
# kernels embed "-androidNN-" in the kernel release string. (Official PEP
# 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')) and python_version >= '3.14'",]
[project.optional-dependencies]
# High-performance event loop — uvloop replaces the stdlib asyncio loop with
# a libuv-backed implementation for ~2-4x throughput on uvicorn. Gated as an
# optional extra because libuv's ./configure does not run on Android/Termux
# (bionic libc, non-FHS layout). Desktop and server installs pull this via
# [all]; the [termux] / [termux-all] profiles intentionally omit it.
uvloop = [
"uvloop>=0.15.1,<0.24; sys_platform != 'win32' and sys_platform != 'cygwin' and sys_platform != 'android' and platform_python_implementation != 'PyPy'",
]
# Native Anthropic provider — only needed when provider=anthropic (not via
# OpenRouter or other aggregators).
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"]
firecrawl = ["firecrawl-py==4.17.0"]
parallel-web = ["parallel-web==0.4.2"]
ddgs = ["ddgs==9.16.0"]
# Image generation backends
fal = ["fal-client==0.13.1"]
# Edge TTS — default TTS provider but still optional (users can pick
# ElevenLabs / OpenAI / MiniMax instead).
edge-tts = ["edge-tts==7.2.7"]
# These engines' upstream Python ceilings exclude the managed interpreter.
# Keep dependency markers and PM gates together: bundles omit them, explicit
# setup requests report unavailable rather than succeeding with an empty set.
neutts = ["neutts[all]==1.4.1; python_version < '3.14'"]
kittentts = [
# KittenTTS requires misaki[en]>=0.9.4, whose Python ceiling is <3.13.
"kittentts @ https://github.com/KittenML/KittenTTS/releases/download/0.8.1/kittentts-0.8.1-py3-none-any.whl ; python_version < '3.13'",
"soundfile==0.14.0; python_version < '3.13'",
]
# onnxruntime has no Intel macOS wheel. Piper has no Windows ARM64 wheel.
piper = [
"piper-tts==1.8.0; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')",
]
modal = ["modal==1.3.4"]
daytona = ["daytona==0.155.0"]
vercel = ["vercel==0.7.2"]
hindsight = ["hindsight-client==0.6.1"]
google-meet = ["playwright==1.62.0", "websockets==15.0.1"]
messaging = [
"python-telegram-bot[webhooks]==22.8",
"discord.py[voice]==2.7.1",
"aiohttp==3.14.3",
"brotlicffi==1.2.0.2",
"slack-bolt==1.30.0",
"slack-sdk==3.44.1",
"qrcode==7.4.2",
] # aiohttp 3.14.3: prior CVEs + GHSA-cq5v-8q36-5273/GHSA-mfx4-hv73-q22v/GHSA-mq44-7p77-q5h7
cron = [] # croniter is now a core dependency; this extra kept for back-compat
slack = ["slack-bolt==1.30.0", "slack-sdk==3.44.1", "aiohttp==3.14.3"]
# Matrix gateway deps. `mautrix[encryption]` pulls `python-olm`, whose
# vendored libolm (archived 2023) cannot build on Windows or modern macOS:
# no wheels for those targets AND the sdist fails to compile on current
# clang/MSVC (olm/list.hh const-iterator error) and on CMake >= 4. It is
# linux-only — gate the whole extra so `uv sync --all-extras` (the bundle
# build) doesn't try to build python-olm on the win/darwin lanes. The
# matrix adapter degrades gracefully without mautrix (import stubs).
matrix = [
"mautrix[encryption]==0.21.1; sys_platform == 'linux'",
"aiosqlite==0.22.1; sys_platform == 'linux'",
"asyncpg==0.31.0; sys_platform == 'linux'",
"aiohttp-socks==0.11.0; sys_platform == 'linux'",
"aiohttp==3.14.3; sys_platform == 'linux'",
] # aiohttp 3.14.3: prior CVEs + GHSA-cq5v-8q36-5273/GHSA-mfx4-hv73-q22v/GHSA-mq44-7p77-q5h7 (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
# and XXE. aiohttp/httpx are already in [messaging]; defusedxml lands
# here to keep the dependency local to wecom_callback's threat model.
wecom = ["defusedxml==0.7.1"]
tts-premium = ["elevenlabs==1.59.0"]
voice = [
# Local STT pulls in wheel-only transitive deps (ctranslate2, onnxruntime).
# faster-whisper's closure is prebuilt-only and missing pieces on two
# targets: ctranslate2 has no win_arm64 wheel/sdist (win32-arm64), and
# onnxruntime has no darwin-x64 wheel/sdist (darwin-x64). Gate the local
# STT path off both; sounddevice/numpy still ship.
"faster-whisper==1.2.1; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or platform_system != 'Darwin')",
"sounddevice==0.5.5",
"numpy==2.4.3",
]
# Wake engines are optional. Bundles include the engines supported by their
# target. PM installs the selected engine on first use for source installs.
wake = [
# pyopen-wakeword includes the TFLite library and shared feature models.
# Its universal2 wheel contains an ARM64-only library, so Intel Macs cannot
# load it. There is no native Windows ARM64 wheel either.
"pyopen-wakeword==1.1.0; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')",
"sherpa-onnx==1.13.8",
# uv reads sherpa-onnx metadata from its cp311-linux_armv7l wheel, which
# omits this pin. Every wheel the bundles install requires it. Without the
# pin a frozen sync installs sherpa-onnx alone, and desktop dependency
# preparation fails on `uv pip check`.
"sherpa-onnx-core==1.13.8",
# text2token imports both tokenizers even for English phrases.
"sentencepiece==0.2.2",
"pypinyin==0.55.0",
"pvporcupine==4.0.3",
"sounddevice==0.5.5",
"numpy==2.4.3",
]
honcho = ["honcho-ai==2.2.0"]
# Platform sub-extras / alias extras — declared so pm.extras.ANCHORS names
# only real extras (contract: tests/pm/test_extras.py). Each is
# lazy-installed at first use via pm.ensure_import(name) and therefore
# deliberately NOT in [all] (see tests/test_project_metadata.py's
# lazy_covered_extras list — keep that list in sync when adding here).
telegram = ["python-telegram-bot[webhooks]==22.8"]
discord = ["discord.py[voice]==2.7.1"]
# stt-whisper + audio-io compose [voice] (faster-whisper / sounddevice / numpy).
stt-whisper = [
"faster-whisper==1.2.1; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or platform_system != 'Darwin')",
]
audio-io = ["sounddevice==0.5.5", "numpy==2.4.3"]
# WeChat voice decode (tools/transcription_tools.py self-heal path).
silk = ["pilk==0.2.4"]
# Wake-engine sub-extras — lazy per-engine installs so a user who only wants
# sherpa-onnx doesn't pull the whole [wake] bundle. Pins match [wake].
wake-openwakeword = [
"pyopen-wakeword==1.1.0; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')",
]
wake-sherpa = [
"sherpa-onnx==1.13.8",
# Required by the sherpa-onnx wheel. See [wake] for why it is explicit.
"sherpa-onnx-core==1.13.8",
"sentencepiece==0.2.2",
"pypinyin==0.55.0",
]
wake-porcupine = ["pvporcupine==4.0.3"]
# Google Chat service account push (gateway/platforms/google_chat) — pubsub
# subscriber for the Chat API's Cloud Pub/Sub delivery.
# grpcio (pubsub's native transitive) has no win_arm64 wheel, so win32-arm64
# would build its sdist and fail — gate the whole extra off there like
# faster-whisper. The adapter's pubsub import is lazy, so this just makes the
# google-chat platform unavailable on win32-arm64.
google-chat = [
"google-cloud-pubsub==2.39.2; platform_machine != 'ARM64' or sys_platform != 'win32'",
]
# Document extraction (read_file anydoc converter self-heal) — core already
# bundles firecrawl-anydoc==0.2.4; the extra exists so a lean/broken install
# can re-sync exactly the pin from core.
doc-extract = ["firecrawl-anydoc==0.2.4"]
trace-upload = ["huggingface-hub==1.24.0"]
# Cloud memory providers are opt-in PM extras. They stay outside [all],
# like honcho/hindsight, so a quarantined release cannot break fresh installs.
supermemory = ["supermemory==3.50.0"]
# mem0ai pulls qdrant-client → grpcio, which has no win_arm64 wheel — same
# win32-arm64 gate as google-chat. mem0 is lazy-installed, so on arm64 the
# provider is simply unavailable.
mem0 = [
"mem0ai==2.0.10; platform_machine != 'ARM64' or sys_platform != 'win32'",
]
# Image resize recovery for the vision tools. Pillow is now a CORE dependency
# (see the main `dependencies` list above) since the byte/pixel shrink paths are on
# the default vision-embed path and the mid-session lazy install deadlocked the
# CLI under prompt_toolkit (#40490). This extra is kept as a no-op back-compat
# alias so existing requests for the `vision` extra resolve.
vision = []
# Kept as a no-op back-compat alias — `ptyprocess` and `pywinpty` are now
# in the main `dependencies` list (with the same platform markers), so
# any existing requests for the `pty` extra resolve cleanly
# without pulling in extra packages.
pty = []
# CVE-2026-48710 (BadHost): Starlette is pulled transitively by mcp's
# sse-starlette / HTTP-SSE stack (and by fastapi in the `web` extra). Before
# 1.0.1, a malformed Host header makes `request.url.path` desync from the path
# the ASGI router actually dispatched, so middleware/endpoints that gate on
# `request.url` can be bypassed. We pin a patched Starlette directly in every
# extra that exposes a Starlette-backed server surface so pip/uv can't resolve
# a vulnerable pre-1.0.1 transitive. Bump in lockstep with uv.lock.
#
# mcp 2.0.0 implements MCP revision 2026-07-28 and moved its own HTTP stack
# from `httpx` to `httpx2`. httpx2 arrives transitively, but tools/mcp_tool.py
# and tools/mcp_oauth_manager.py import it by name to build the client objects
# they hand to the SDK, so it is pinned here explicitly rather than left to
# resolution. Hermes' own `httpx[socks]==0.28.1` in [dependencies] is
# unaffected — the two distributions install side by side under different
# module names.
mcp = [
"mcp==2.0.0",
"httpx2==2.7.0",
"starlette==1.3.1",
] # starlette: CVE-2026-48710
# Backwards-compatible no-op alias. Relay is a core dependency on supported
# wheel targets and intentionally unavailable on other platforms.
nemo-relay = []
homeassistant = ["aiohttp==3.14.3"]
sms = ["aiohttp==3.14.3"]
teams = [
"microsoft-teams-apps==2.0.13.4",
"aiohttp==3.14.3",
] # aiohttp 3.14.3: prior CVEs + GHSA-cq5v-8q36-5273/GHSA-mfx4-hv73-q22v/GHSA-mq44-7p77-q5h7
# Computer use talks to PM's pinned cua-driver over MCP stdio. This extra
# provides the MCP client, independently of the native driver package.
computer-use = [
"mcp==2.0.0",
"httpx2==2.7.0",
"starlette==1.3.1",
] # starlette: CVE-2026-48710
acp = ["agent-client-protocol==0.9.0"]
# mistral: Voxtral STT + TTS. Pinned to an exact verified-clean version.
# The `mistralai` PyPI project was quarantined 2026-05-12 after the malicious
# 2.4.6 release (Mini Shai-Hulud worm); 2.4.6 was removed from PyPI and the
# project is serving clean releases again (2.4.7 2026-05-25, 2.4.8 2026-05-28).
# Like other opt-in TTS/STT backends, this is lazy-installed via
# pm (stt.mistral / tts.mistral) at first use — deliberately
# NOT re-added to [all] so a future quarantined release can't break fresh
# installs (see [all] policy comment below).
mistral = ["mistralai==2.4.8"]
# OTLP gateway monitoring export (optional). Provides the OpenTelemetry SDK +
# OTLP/HTTP exporter for monitoring.gateway_health_export. Lazy-installed via
# PM on first use; never a core dependency and deliberately
# NOT in [all].
otlp = [
"opentelemetry-sdk==1.39.1",
"opentelemetry-exporter-otlp-proto-http==1.39.1",
]
langfuse = ["langfuse==4.15.2"]
bedrock = ["boto3==1.42.89"]
vertex = ["google-auth==2.55.1"]
azure-identity = ["azure-identity==1.25.3"]
termux = [
# Baseline Android / Termux path for reliable fresh installs.
"python-telegram-bot[webhooks]==22.8",
"hermes-agent[cron]",
"hermes-agent[mcp]",
"hermes-agent[honcho]",
"hermes-agent[acp]",
]
termux-all = [
# Best-effort "install all" profile for Termux. Same policy as [all]:
# only includes extras that are not prepared on demand by PM.
# Backends like telegram/slack/dingtalk/feishu/honcho lazy-install at
# first use, so they're no longer eager-installed here.
"hermes-agent[termux]",
"hermes-agent[google]",
"hermes-agent[homeassistant]",
"hermes-agent[sms]",
"hermes-agent[web]",
"hermes-agent[pty]",
]
dingtalk = [
"dingtalk-stream==0.24.3",
"alibabacloud-dingtalk==2.2.42",
"qrcode==7.4.2",
]
feishu = ["lark-oapi==1.6.8", "qrcode==7.4.2"]
google = [
# Required by the google-workspace skill (Gmail, Calendar, Drive, Contacts,
# Sheets, Docs). Declared here so PM environments and independent
# packagers can include them without mutating an application's runtime.
"google-api-python-client==2.194.0",
"google-auth==2.55.1",
"google-auth-oauthlib==1.3.1",
"google-auth-httplib2==0.3.1",
# The Google SDKs permit older vulnerable transitives, so unlocked installs
# must carry the same fixed floors as uv.lock and the runtime installers:
# httplib2 0.32.0 (GHSA-j5g9-f88f-gfj3 decompression bomb DoS),
# pyasn1 0.6.4, google-auth 2.55.1.
"httplib2==0.32.0",
"pyasn1==0.6.4",
]
youtube = [
# Required by skills/media/youtube-content and
# optional-skills/productivity/memento-flashcards (youtube_quiz.py).
# Without this declaration dependency sync omits the package and both skills fail
# at first invocation with ModuleNotFoundError (issue #22243).
"youtube-transcript-api==1.2.4",
]
# `hermes dashboard` (localhost SPA + API). Not in core to keep the default install lean.
# starlette==1.3.1 pinned for CVE-2026-48710 (BadHost) — fastapi pulls Starlette
# transitively and pre-1.0.1 is the vulnerable range. See the mcp extra above.
# uvicorn pin without [standard] — httptools / watchfiles are now core deps,
# and uvloop is gated behind the [uvloop] extra. This keeps [web] safe for
# termux-all which includes it.
web = [
"fastapi==0.133.1",
"uvicorn==0.41.0",
"starlette==1.3.1",
"python-multipart==0.0.32",
]
all = [
# Policy (2026-05-12): `[all]` includes only extras that genuinely
# CAN'T be prepared on demand by PM — i.e. things every
# session can use, things needed before the agent loop is alive
# (terminal/CLI), and skill deps that dev environments need.
# Anything an opt-in backend (provider, search, TTS, image, memory,
# messaging platform, terminal sandbox) needs MUST live exclusively in
# opt-in extras and resolve at first use — otherwise one quarantined PyPI
# release breaks every fresh install.
#
# Removed from [all] on 2026-05-12 (covered by lazy-install):
# anthropic, exa, firecrawl, parallel-web, fal, edge-tts,
# modal, daytona, vercel, messaging (telegram/discord/slack),
# matrix, slack, honcho, voice (faster-whisper),
# dingtalk, feishu, bedrock, tts-premium (elevenlabs)
#
# Why: the matrix extra in particular pulls `mautrix[encryption]`
# which depends on `python-olm`. python-olm has Linux-only wheels and
# no native build path on Windows or modern macOS. With matrix in
# [all], `uv sync --locked` on Windows tried to build it from sdist
# and failed on `make`. Lazy-install routes that build to first use,
# where the user is expected to have a toolchain available.
"hermes-agent[cron]",
"hermes-agent[pty]",
"hermes-agent[mcp]",
"hermes-agent[uvloop]",
"hermes-agent[homeassistant]",
"hermes-agent[sms]",
"hermes-agent[acp]",
"hermes-agent[google]",
"hermes-agent[web]",
"hermes-agent[youtube]",
]
[dependency-groups]
# Build and test tooling never ships in runtime payloads.
dev = [
"debugpy==1.8.20",
"pytest==9.1.1",
"pytest-asyncio==1.3.0",
"mcp==2.0.0",
"httpx2==2.7.0",
"starlette==1.3.1",
"ty==0.0.82",
"ruff==0.15.10",
"setuptools==83.0.0",
] # starlette: CVE-2026-48710; setuptools: 83 (torch >=2.13 requires setuptools 83)
# Native launcher and provider-catalog E2E dependencies belong in the test
# environment, not the core runtime. Keep the Anthropic SDK at the same
# reviewed version as the optional runtime extra above.
test = [
"distlib==0.4.3; sys_platform == 'win32'",
"anthropic==0.87.0",
]
[build-system]
# setuptools.build_meta + our setup.py bdist_wheel guard import wheel during
# PEP 517 isolated builds. Without wheel in
# requires, the isolation sandbox only gets setuptools and the build
# fails with ModuleNotFoundError: wheel.cli (#96488).
requires = ["setuptools==83.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project.scripts]
hermes = "hermes_cli.main:main"
hermes-agent = "agent.legacy_cli:main"
hermes-acp = "acp_adapter.entry:main"
# Per-extra platform gates (settled 2026-09-02): an extra listed here is
# only installable on platforms matching the marker. pm consults this
# table (pm.extras.extra_supported) — ensure_import/sync_venv refuse a
# gated-off extra with a clear message instead of resolving it to an
# empty set. The per-package markers inside the extra stay (uv's
# resolver needs them for --all-extras); this table is the readable
# single-authority for WHICH extra is supported WHERE.
[tool.hermes.extras-platforms]
# mautrix[encryption] → python-olm: linux-only wheels, no win/darwin build.
matrix = "sys_platform == 'linux'"
# grpcio (via google-cloud-pubsub) has no win_arm64 wheel.
google-chat = "platform_machine != 'ARM64' or sys_platform != 'win32'"
# qdrant-client → grpcio (via mem0ai) has no win_arm64 wheel.
mem0 = "platform_machine != 'ARM64' or sys_platform != 'win32'"
# Match the engine dependency markers so PM refuses an unsupported install.
wake-openwakeword = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')"
# faster-whisper's closure is prebuilt-only: ctranslate2 has no win_arm64 wheel and onnxruntime
# none for darwin-x64. Its ANCHOR is faster_whisper, so an unsupported machine could install the
# extra, rebuild the whole dependency environment and still fail the anchor on every retry.
# Gate the extra that carries only faster-whisper; [voice] also has ungated deps (sounddevice,
# numpy) and stays installable, see test_declared_extra_gates_match_dependency_selection.
stt-whisper = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or platform_system != 'Darwin')"
# NeuTTS excludes Python 3.14. KittenTTS's misaki dependency excludes 3.13+.
neutts = "python_version < '3.14'"
kittentts = "python_version < '3.13'"
piper = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')"
[tool.uv]
# uv otherwise enables the dev group by default, including in payload builds.
default-groups = []
# 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.
# so, we force-override this to pynacl 1.6. here.
# remove this when we update discord.py
"pynacl>=1.6,<1.7",
# alibabacloud-tea-openapi caps cryptography<49, and its latest release
# still does. Without this override, that cap holds cryptography at 48.0.1
# and its three advisories (see the cryptography pin in
# [project].dependencies). The package uses cryptography only to sign
# requests with RSA and AES, and that API did not break in 49 or 50.
# Keep this exact version equal to the direct dependency. uv overrides
# replace that requirement too, not only the vendor's upper bound.
# Remove this line when alibabacloud-tea-openapi lifts the cap.
"cryptography==50.0.1",
]
exclude-newer = "14 days"
# h2: temporary exclude-newer exception for the CVE-2026-71554 (GHSA-6hr6-w5qg-qmwg,
# request-smuggling) fix in 4.4.1, published 2026-08-03. Remove after 2026-08-17.
# aiohttp, cryptography: same shape — the advisory fixes are newer than the
# 14-day window, so the resolver cannot see them without an exception.
#
# defusedxml, python-olm, unpaddedbase64: the OPPOSITE shape (#80387, #79434,
# #78227 family). These are ancient, effectively frozen releases (2021-2023)
# whose upload dates are frequently absent from mirror indexes and stale uv
# HTTP caches. uv treats a missing upload date as "newer than the cutoff" and
# excludes the package, bricking [youtube]/[wecom]/[matrix] resolution
# ("there are no versions of defusedxml"). Exempting them carries zero aging
# risk — their newest releases are years old — and unbricks resolvers that
# cannot see upload dates.
#
# setuptools, wheel, pillow, mcp: build/core pin bricks (#78227, #75992, #76020, #96488). uv
# applies exclude-newer to build-system.requires and core deps too; when a
# resolver cannot see an upload date (old uv, mirror index, stale HTTP cache)
# it filters the pinned version and the package cannot even BUILD
# ("No solution found when resolving: setuptools==83.0.0"). These deps are
# exact-pinned (==X.Y.Z), so exclude-newer adds zero float protection for
# them — the version cannot move without a reviewed pin bump — while the
# cutoff can still brick installs. Exempting exact pins is pure brick-risk
# removal at no supply-chain cost. Guarded by
# tests/test_packaging_metadata.py::test_build_system_requires_exempt_from_exclude_newer.
#
# firecrawl-anydoc: same exact-pin shape (==0.2.4, hosted-OCR wiring PR).
# The pin bump WAS the review; exclude-newer adds zero float protection to
# an exact pin and would only delay the reviewed version 14 days.
#
# Every other exact-pinned package below: the release-day brick shape
# (observed 2026-08-29 updating three long-running installs v0.20.0 ->
# v0.20.6 — one Termux, two Linux servers). Each release exact-pins at least
# one dependency to a version published days before the release (v0.20.6
# pinned snowballstemmer==3.1.1 and psutil==7.2.2 in core, plus fastapi /
# uvicorn / python-telegram-bot pins across extras). For two weeks after
# release the cutoff filters those versions out, so any venv that predates
# the release bricks on `hermes update` ("no version of
# snowballstemmer==3.1.1") until the window passes. Same zero-float-
# protection logic as setuptools/pillow/mcp above; enforced for every exact
# pin by
# tests/test_packaging_metadata.py::test_exact_pinned_deps_exempt_from_exclude_newer.
#
# maturin, setuptools-rust: build-system.requires of the cryptography pin
# above. cryptography itself is exempted, but on wheel-less platforms
# (Termux/Android) it must build from sdist, and the isolated build
# environment resolves under the same cutoff ("Failed to resolve
# requirements from build-system.requires ... maturin>=1.9,<2").
#
# google-cloud-pubsub: same exact-pin shape (==2.39.2, google-chat adapter).
# The pin bump WAS the review; exclude-newer only filters it because the
# 14-day window is younger than the reviewed release.
[tool.uv.exclude-newer-package]
agent-client-protocol = false
aiohttp = false
aiohttp-socks = false
aiosqlite = false
alibabacloud-dingtalk = false
anthropic = false
asyncpg = false
azure-identity = false
boto3 = false
brotlicffi = false
certifi = false
concurrent-log-handler = false
croniter = false
cryptography = false
daytona = false
ddgs = "2026-08-26T21:52:34Z"
debugpy = false
defusedxml = false
dingtalk-stream = false
discord-py = false
edge-tts = false
elevenlabs = false
exa-py = false
fal-client = false
fastapi = false
faster-whisper = false
fire = false
firecrawl-anydoc = false
firecrawl-py = false
google-api-python-client = false
google-auth = false
google-auth-httplib2 = false
google-auth-oauthlib = false
google-cloud-pubsub = false
h2 = false
hindsight-client = false
honcho-ai = false
httplib2 = false
httpx = false
httpx2 = false
huggingface_hub = false
jinja2 = false
langfuse = "2026-09-09T16:01:26Z"
lark-oapi = false
markdown = false
maturin = false
mautrix = false
mcp = false
mem0ai = false
microsoft-teams-apps = false
mistralai = false
modal = false
nemo-relay = false
neutts = "2026-07-22T14:54:23Z"
numpy = false
openai = false
opentelemetry-exporter-otlp-proto-http = false
opentelemetry-sdk = false
packaging = false
parallel-web = false
pathspec = false
pillow = false
piper-tts = "2026-09-04T16:47:32Z"
prompt-toolkit = false
psutil = false
pvporcupine = false
pyasn1 = false
pydantic = false
pyjwt = false
pytest = false
pytest-asyncio = false
python-dotenv = false
python-multipart = false
python-olm = false
python-telegram-bot = false
pyyaml = false
qrcode = false
requests = false
rich = false
ruamel-yaml = false
ruff = false
sentencepiece = false
setuptools = false
setuptools-rust = false
wheel = false
sherpa-onnx = "2026-09-10T17:00:04Z"
# The core is exact-pinned by sherpa-onnx. Admit all wheels of that release.
sherpa-onnx-core = "2026-09-10T15:58:47Z"
slack-bolt = false
slack-sdk = false
snowballstemmer = false
sounddevice = false
soundfile = "2026-06-06T08:58:48Z"
starlette = false
supermemory = false
tenacity = false
ty = false
tzdata = false
unpaddedbase64 = false
uvicorn = false
vercel = false
websockets = false
youtube-transcript-api = false
[tool.setuptools]
# Root single-file modules are derived by setup.py at build time from the
# source tree (see the ``_root_py_modules`` helper there). A static
# ``py-modules`` list here drifted from the tree each time the layout changed,
# and the drift broke installed wheels. Do not add the list back.
[tool.setuptools.packages.find]
include = [
"agent",
"agent.*",
"tools",
"tools.*",
"hermes_cli",
"hermes_cli.*",
"gateway",
"gateway.*",
"tui_gateway",
"tui_gateway.*",
"cron",
"cron.*",
"acp_adapter",
"plugins",
"plugins.*",
"providers",
"providers.*",
"hermes_platform",
"hermes_platform.*",
"pm",
"pm.*",
"tools",
"tools.*",
]
[tool.setuptools.package-data]
hermes_cli = [
"observability/schemas/*.json",
"data/*.json",
"local_runtime/*.json",
]
# gateway/assets/ ships status_phrases.yaml and the Telegram BotFather
# screenshot. Without this, sealed venvs (uv2nix) silently lose both —
# status phrases fall back to the tiny hardcoded set and the Telegram
# topic-setup image disappears. Loaded via Path(__file__).parent / "assets"
# in gateway/status_phrases.py and gateway/run.py.
gateway = ["assets/**/*"]
# Bundled plugin discovery reads these manifests at runtime. Keep them in
# sealed wheels with the plugin Python modules; without this declaration the
# wheel contains adapters but discovery finds zero bundled plugins.
plugins = ["**/plugin.yaml", "**/plugin.yml"]
# pm/lock.json is the runtime pin table (uv/python/tool versions + sha256s).
# Without it a sealed wheel installs the pm package but has no pins to
# realize — the pm store would be empty.
pm = ["lock.json", "artifact-mirror.json", "pyproject.toml", "uv.lock"]
# Bot Desktop starts Xvnc + Xfce through a shell launcher and seeds the wallpaper; both are read
# via Path(__file__).parent in tools/bot_desktop/runtime.py and vanish from sealed wheels otherwise.
tools = ["bot_desktop/launcher.sh", "bot_desktop/wallpaper.png"]
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"integration: marks tests requiring external services (API keys, Modal, etc.)",
"live: secrets-gated canaries against REAL LLM provider APIs (spends money; select with -m live)",
"real_concurrent_gate: opt out of the autouse stub that disables _detect_concurrent_hermes_instances",
"real_agent_prewarm: opt out of the autouse stub that disables the tui_gateway deferred agent pre-warm timer",
"real_retry_backoff: opt out of the tests/agent autouse stub that zeroes jittered_backoff",
"requires_wal: needs the runtime to actually enable SQLite WAL mode (skipped where Hermes falls back to journal_mode=DELETE)",
"no_isolate: opt out of per-file subprocess isolation (tests share mutable module-level state)",
"ssh: marks tests requiring a reachable SSH server (skipped in normal CI)",
"platforms(*specs, arch=None, arch_negate=False): run only on hosts matching at least one spec — linux/macos/windows/posix/any, 'not X' negation, optional arch filter",
]
# integration tests take way too long to run in the normal CI environments
addopts = "-m 'not integration and not live'"
[tool.ty.environment]
python-version = "3.13"
[tool.ty.rules]
unknown-argument = "warn"
redundant-cast = "ignore"
[tool.ruff]
preview = true # required for PLW1514 (unspecified-encoding) — preview rule
[tool.ruff.lint]
# All other lints are intentionally disabled (see comment history on this
# file) while we wrangle typechecks — but PLW1514 is too load-bearing to
# keep off. Bare open()/read_text()/write_text() in text mode defaults to
# the system locale encoding on Windows (cp1252 on US-locale installs),
# which silently corrupts any non-ASCII file content. We had three
# separate Windows sandbox regressions in one debug session before
# adding the explicit encoding. This rule keeps new code honest.
#
# ASYNC210/220/221/251: blocking calls inside `async def` freeze the whole
# gateway/uvicorn event loop — every adapter, timer, and health check stops
# until the call returns. Real incidents: a 17-minute getaddrinfo hang took
# the backend down (#91912); `time.sleep` in start_gateway froze restarts
# for 10s (#36163). The fix pattern is `await asyncio.to_thread(...)` (or
# `loop.run_in_executor`), `asyncio.create_subprocess_exec`, and
# `await asyncio.sleep`. These four are the direct freeze vectors:
# ASYNC210 — blocking HTTP call in async fn (urllib/requests/httpx-sync)
# ASYNC220 — subprocess.Popen in async fn
# ASYNC221 — subprocess.run / os.system in async fn
# ASYNC251 — time.sleep in async fn
# ASYNC230 (blocking open()) and ASYNC240 (blocking path methods) are real
# but ~180 legacy sites deep; they graduate to this list once the backlog
# is burned down (see per-file-ignores below for the frozen baseline).
select = ["PLW1514", "ASYNC210", "ASYNC220", "ASYNC221", "ASYNC251", "TID251"]
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"pm.uv".msg = "Use PM's semantic Python operations, not an installer executable."
"pm.client.uv".msg = "Use PM's semantic Python operations."
"pm.install.uv".msg = "The raw uv bridge was removed; use PM operations."
"pm._uv".msg = "Tool acquisition is private to PM."
"pm.environment".msg = "The environment engine is private; use PM operations."
"pm.runtime_stage".msg = "PM stages its own worker runtime; consumers call pm operations."
[tool.ruff.lint.per-file-ignores]
"pm/**" = ["TID251"]
# Desktop E2E fixtures stand in for PM's worker/runtime and reach into it like tests do.
"apps/desktop/electron/fixtures/**" = ["TID251"]
# Tests can intentionally exercise locale-encoding edge cases.
"tests/**" = [
"PLW1514",
"ASYNC210",
"ASYNC220",
"ASYNC221",
"ASYNC251",
"TID251",
]
# Skills and plugins are partially user-authored — their own conventions.
"skills/**" = ["PLW1514"]
"optional-skills/**" = ["PLW1514"]
# Plugin platform adapters run ON the gateway event loop — the ASYNC gate
# applies to them with full force; only PLW1514 stays relaxed.
"plugins/**" = ["PLW1514"]
# ---------------------------------------------------------------------------
# ASYNC ratchet baseline — legacy blocking sites that predate the gate.
# Each entry is an EXISTING violation being fixed in its own PR; do NOT add
# new files here. Remove the entry when the file's sites are fixed.
# ---------------------------------------------------------------------------
# Detached restart watchers: Popen of a fully-detached, fire-and-forget
# process (no wait), an accepted momentary spawn cost pending a dedicated
# async-subprocess sweep.
"gateway/run.py" = ["ASYNC220"]
"gateway/run_shutdown.py" = ["ASYNC220"]
"gateway/slash_commands.py" = ["ASYNC220"]
# Off-loop sweep for these routers is in flight (PR #84376).
"hermes_cli/web_routers/profiles.py" = ["ASYNC220", "ASYNC221"]
# Legacy blocking spawn sites in platform adapters, pending their own fixes.
"plugins/platforms/whatsapp/adapter.py" = ["ASYNC220", "ASYNC221"]
"plugins/platforms/photon/adapter.py" = ["ASYNC220"]