[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 `hermes pm lock` 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"] # NeuTTS's upstream Python ceiling excludes 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 @ https://github.com/KittenML/KittenTTS/releases/download/0.8.1/kittentts-0.8.1-py3-none-any.whl ; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')", # KittenTTS requires misaki[en]>=0.9.4, and PyPI's misaki 0.9.4 caps Python # below 3.13. The NousResearch fork is the same 0.9.4 code with the cap # raised to <3.15. Declaring it here makes KittenTTS's transitive # requirement resolve to the fork. Drop this line when upstream misaki # publishes a release that allows 3.14. "misaki[en] @ git+https://github.com/NousResearch/misaki.git@f03fd2be7346952a83d3d4845c217fc7667f322d ; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')", "soundfile==0.14.0; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')", ] # 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"] 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, 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" [tool.hermes] # Extras that only an explicit selection installs (setup, sync_venv). Bundles # build with --all-extras and pass --no-extra for each of these, so their # closures never ship. KittenTTS pulls torch, transformers and spaCy through # misaki[en]: installed when the user picks it, not preinstalled. opt-in-extras = ["kittentts"] # 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. neutts = "python_version < '3.14'" # KittenTTS: onnxruntime has no Intel macOS wheel; misaki[en]'s torch has no # Windows ARM64 wheel. kittentts = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin')" 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", # misaki[en] (pulled by [kittentts]) lists spacy-curated-transformers. # The newest release that fits spacy 3.8 (2.x needs thinc 9), 0.3.1, caps # Python below 3.14, so PM's `uv pip check` rejects the venv. Nothing # imports it: only transformer spaCy models (trf=True) do, and KittenTTS # never builds a misaki G2P. The never-true marker removes it from the lock. # Remove this line when misaki drops it or a 3.14-capable release fits. "spacy-curated-transformers; sys_platform == 'never'", ] 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 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"]