[project] name = "hermes-agent" version = "0.21.0" description = "The self-improving AI agent — creates skills from experience, improves them during use, and runs anywhere" readme = "README.md" # Upper bound is load-bearing, not cosmetic. uv resolves the project's # Python from `requires-python`, and an inherited `UV_PYTHON` env var (or a # fresh distro whose newest interpreter uv auto-picks) will otherwise select # 3.14, where Rust-backed transitives (e.g. pydantic-core) have no cp314 # wheel yet and fall back to a maturin source build that fails. Capping at # <3.14 makes uv refuse 3.14 with a clear error instead of attempting that # build. Raise the ceiling once our Rust transitives ship cp314 wheels. requires-python = ">=3.11,<3.14" 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. # # When updating: bump the version below AND regenerate uv.lock with # `uv lock` so the transitive resolution stays consistent. Don't # introduce ranges back 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 lazy-installed via `tools/lazy_deps.py` when the # user picks that backend. Smaller `dependencies` = smaller blast # radius for the next supply-chain attack. "openai==2.24.0", "certifi==2026.5.20", "python-dotenv==1.2.2", "fire==0.7.1", "httpx[socks]==0.28.1", "rich==14.3.3", "tenacity==9.1.4", "pyyaml==6.0.3", "ruamel.yaml==0.18.17", "requests==2.33.0", # CVE-2026-25645 "jinja2==3.1.6", # 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). tools/lazy_deps.py `tool.doc_extract` remains the # self-heal path for lean/broken installs — keep the pin below and the # lazy pin in lockstep. "firecrawl-anydoc==0.2.4", # 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", # Interactive CLI (prompt_toolkit is used directly by cli.py) "prompt_toolkit==3.0.52", # Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter). "croniter==6.0.0", # 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", # ``packaging`` is imported directly on three production paths but was never # declared, so it only reached users transitively (pip/uv pull it for other # tools). The slim official Docker image ships without it, where the # try/except-ImportError fallbacks silently degrade: Hindsight's # ``_meets_minimum_version`` disables update_mode='append' (#40503), # tools/lazy_deps.py treats every version constraint as satisfied, and # hermes_cli/main.py drops to naive requirement parsing. Pure-Python # py3-none-any wheel, no compiled extensions — safe to ship everywhere. # Pinned to the version already resolved in uv.lock (no resolution churn). "packaging==26.0", # 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", # Skills Hub (GitHub App JWT auth — optional, only needed for bot identity) "PyJWT[crypto]==2.13.0", # 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", # 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.0", # 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'", # 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", # Browser CDP supervisor + browser_dialog import this directly. Keep core # so browser tool discovery doesn't fail on lean installs. "websockets==15.0.1", # .gitignore-aware file matching for desktop build stamp. "pathspec==1.1.1", "fastapi>=0.104.0,<1", # CIDR-aware forwarded_allow_ips requires uvicorn >=0.31.0. "uvicorn[standard]>=0.31.0,<1", # Streaming multipart uploads for the dashboard file manager (NS-501). # FastAPI's UploadFile/Form depend on python-multipart; it is NOT pulled in # by fastapi itself, so the dashboard's multipart upload endpoint would 500 # without an explicit dependency here (and in the `web` extra below). "python-multipart>=0.0.9,<1", "ptyprocess>=0.7.0,<1; sys_platform != 'win32'", "pywinpty>=2.0.0,<3; sys_platform == 'win32'", # 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'", # 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", # 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'", # 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')", ] [project.optional-dependencies] # 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"] # 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"] modal = ["modal==1.3.4"] daytona = ["daytona==0.155.0"] vercel = ["vercel==0.7.2"] hindsight = ["hindsight-client==0.6.1"] 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.21", "ruff==0.15.10", "setuptools==83.0.0"] # starlette: CVE-2026-48710; setuptools: 83 (torch >=2.13 requires setuptools 83) messaging = ["python-telegram-bot[webhooks]==22.8", "discord.py[voice]==2.7.1", "aiohttp==3.14.3", "brotlicffi==1.2.0.1", "slack-bolt==1.30.0", "slack-sdk==3.43.0", "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.43.0", "aiohttp==3.14.3"] matrix = ["mautrix[encryption]==0.21.1", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0", "aiohttp==3.14.3"] # 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==1.2.1", "sounddevice==0.5.5", "numpy==2.4.3", ] # "Hey Hermes" wake word — on-device hotword detection. All engines are # optional; openWakeWord (ONNX) is the free default, sherpa-onnx adds # open-vocabulary phrases (any typed phrase, zero training), Porcupine is # the premium alternative. Desktop installs ([--include-desktop]) eager-install # [wake]+[voice] so the ear works instantly; CLI-only installs lazy-install on # first /wake; mirrored in tools/lazy_deps.py. wake = [ "openwakeword==0.6.0", "onnxruntime==1.27.0", "sherpa-onnx==1.13.4", "sentencepiece==0.2.2", "pvporcupine==4.0.3", "sounddevice==0.5.5", "numpy==2.4.3", # openWakeWord's onnx embedding model scores near-zero on macOS ARM64 # (dscripka/openWakeWord#336), so the wake word runs on tflite there. # Upstream declares tflite-runtime for Linux only; ai-edge-litert is the # macOS equivalent, bridged in tools/wake_word.py. "ai-edge-litert==2.1.6; platform_system == 'Darwin'", ] honcho = ["honcho-ai==2.2.0"] # Cloud memory providers — opt-in, lazy-installed via tools/lazy_deps.py # (memory.supermemory / memory.mem0) at first use. Exact pins MUST match the # LAZY_DEPS pins (enforced by tests/test_project_metadata.py). Deliberately # excluded from [all] like honcho/hindsight so a quarantined upstream release # can't break fresh installs. supermemory = ["supermemory==3.50.0"] mem0 = ["mem0ai==2.0.10"] # 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 — macOS background desktop control via cua-driver (MCP stdio). # The cua-driver binary itself is installed via `hermes tools` post-setup # (curl install script); this extra just pins the MCP client used to talk # to it, which is already provided by the `mcp` extra. 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 # tools/lazy_deps.py (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 # tools/lazy_deps.py 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"] 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 aren't covered by `tools/lazy_deps.py`. # 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 dev environments (`uv sync --extra google`) # and packagers ship them without hitting runtime `pip install` paths that # fail in environments without pip (e.g. Nix-managed Python). "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 uv 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. web = ["fastapi==0.133.1", "uvicorn[standard]==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 lazy-installed via `tools/lazy_deps.py` — 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 # `LAZY_DEPS` 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[homeassistant]", "hermes-agent[sms]", "hermes-agent[acp]", "hermes-agent[google]", "hermes-agent[web]", "hermes-agent[youtube]", ] [build-system] # setuptools.build_meta + our setup.py bdist_wheel guard import wheel during # PEP 517 isolated builds (uv sync / uv pip install -e .). Without wheel in # requires, the isolation sandbox only gets setuptools and Windows installer # 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 = "run_agent:main" hermes-acp = "acp_adapter.entry:main" [tool.uv] 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. # Remove this line when alibabacloud-tea-openapi lifts the cap. "cryptography>=50,<51", ] 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"). [tool.uv.exclude-newer-package] agent-client-protocol = false ai-edge-litert = 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 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 h2 = false hindsight-client = false honcho-ai = false httplib2 = false httpx = false httpx2 = false huggingface_hub = false jinja2 = false lark-oapi = false markdown = false maturin = false mautrix = false mcp = false mem0ai = false microsoft-teams-apps = false mistralai = false modal = false nemo-relay = false numpy = false onnxruntime = false openai = false opentelemetry-exporter-otlp-proto-http = false opentelemetry-sdk = false openwakeword = false packaging = false parallel-web = false pathspec = false pillow = false 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 = false slack-bolt = false slack-sdk = false snowballstemmer = false sounddevice = false starlette = false supermemory = false tenacity = false ty = false tzdata = false unpaddedbase64 = false uvicorn = false vercel = false websockets = false youtube-transcript-api = false [tool.setuptools] # Top-level single-file modules (not packages). Without this, uv2nix's # sealed venv is missing hermes_constants, run_agent, etc. py-modules = [ "run_agent", "registration_lifecycle", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli", "hermes_bootstrap", "hermes_constants", "hermes_state", "hermes_state_common", "hermes_state_portability", "hermes_state_schema", "hermes_state_search", "hermes_startup_watchdog", "hermes_time", "hermes_logging", "utils", "mcp_serve", ] [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.*"] [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"] [tool.pytest.ini_options] testpaths = ["tests"] markers = [ "integration: marks tests requiring external services (API keys, Modal, etc.)", "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", "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)", "linux_only: exercises Linux-specific behaviour; skipped on other hosts", "macos_only: exercises macOS-specific behaviour; skipped on other hosts", "windows_only: exercises native-Windows behaviour; skipped on other hosts", ] # integration tests take way too long to run in the normal CI environments addopts = "-m 'not integration'" [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"] [tool.ruff.lint.per-file-ignores] # Tests can intentionally exercise locale-encoding edge cases. "tests/**" = ["PLW1514", "ASYNC210", "ASYNC220", "ASYNC221", "ASYNC251"] # 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/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"]