[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "hermes-agent" version = "0.15.1" description = "The self-improving AI agent — creates skills from experience, improves them during use, and runs anywhere" readme = "README.md" requires-python = ">=3.11" authors = [{ name = "Nous Research" }] license = { text = "MIT" } 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", "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", # 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", # Skills Hub (GitHub App JWT auth — optional, only needed for bot identity) "PyJWT[crypto]==2.12.1", # CVE-2026-32597 # 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", # .gitignore-aware file matching for desktop build stamp. "pathspec==1.1.1", "fastapi>=0.104.0,<1", "uvicorn[standard]>=0.24.0,<1", "ptyprocess>=0.7.0,<1; sys_platform != 'win32'", "pywinpty>=2.0.0,<3; sys_platform == 'win32'", ] [project.optional-dependencies] # Native Anthropic provider — only needed when provider=anthropic (not via # OpenRouter or other aggregators). anthropic = ["anthropic==0.86.0"] # 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"] hindsight = ["hindsight-client==0.6.1"] dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "pytest-timeout==2.4.0", "mcp==1.26.0", "starlette==1.0.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==82.0.1"] # starlette: CVE-2026-48710 messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "brotlicffi==1.2.0.1", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"] cron = [] # croniter is now a core dependency; this extra kept for back-compat slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"] matrix = ["mautrix[encryption]==0.21.0", "Markdown==3.10.2", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"] # 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"] cli = ["simple-term-menu==1.6.6"] tts-premium = ["elevenlabs==1.59.0"] voice = [ # Local STT pulls in wheel-only transitive deps (ctranslate2, onnxruntime), # so keep it out of the base install for source-build packagers like Homebrew. "faster-whisper==1.2.1", "sounddevice==0.5.5", "numpy==2.4.3", ] pty = [ # 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 `pip install hermes-agent[pty]` invocations resolve cleanly # without pulling in extra packages. ] honcho = ["honcho-ai==2.0.1"] # 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 = ["mcp==1.26.0", "starlette==1.0.1"] # starlette: CVE-2026-48710 nemo-relay = ["nemo-relay==0.3"] homeassistant = ["aiohttp==3.13.3"] sms = ["aiohttp==3.13.3"] # 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==1.26.0", "starlette==1.0.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"] bedrock = ["boto3==1.42.89"] azure-identity = ["azure-identity==1.25.3"] termux = [ # Baseline Android / Termux path for reliable fresh installs. "python-telegram-bot[webhooks]==22.6", "hermes-agent[cron]", "hermes-agent[cli]", "hermes-agent[pty]", "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]", ] dingtalk = ["dingtalk-stream==0.24.3", "alibabacloud-dingtalk==2.2.42", "qrcode==7.4.2"] feishu = ["lark-oapi==1.5.3", "qrcode==7.4.2"] google = [ # Required by the google-workspace skill (Gmail, Calendar, Drive, Contacts, # Sheets, Docs). Declared here so packagers (Nix, Homebrew) ship them with # the [all] extra and users don't hit runtime `pip install` paths that fail # in environments without pip (e.g. Nix-managed Python). "google-api-python-client==2.194.0", "google-auth-oauthlib==1.3.1", "google-auth-httplib2==0.3.1", ] 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.0.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.0.1"] 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 packagers (Nix, AUR, Homebrew) # need in the wheel. 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, 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[cli]", "hermes-agent[dev]", "hermes-agent[pty]", "hermes-agent[mcp]", "hermes-agent[homeassistant]", "hermes-agent[sms]", "hermes-agent[acp]", "hermes-agent[google]", "hermes-agent[web]", "hermes-agent[youtube]", ] [project.scripts] hermes = "hermes_cli.main:main" hermes-agent = "run_agent:main" hermes-acp = "acp_adapter.entry:main" [tool.setuptools] py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli", "hermes_bootstrap", "hermes_constants", "hermes_state", "hermes_time", "hermes_logging", "utils", "mcp_serve"] [tool.setuptools.package-data] hermes_cli = ["web_dist/**/*", "tui_dist/**/*", "scripts/install.sh", "scripts/install.ps1"] gateway = ["assets/**/*"] plugins = [ "*/dashboard/manifest.json", "*/dashboard/dist/*", "*/dashboard/dist/**/*", # Plugin discovery (hermes_cli/plugins.py) reads a plugin.yaml/plugin.yml # manifest from each bundled plugin directory to register it. Wheels only # carry files declared here, so without this glob the wheel ships every # plugin's Python code but none of its manifests — the scan finds zero # plugins and all gateway platforms fail with "No adapter available for # " (#34034), web-search providers go missing (#28149), etc. "**/plugin.yaml", "**/plugin.yml", "**/README.md", ] [tool.setuptools.packages.find] include = ["agent", "agent.*", "tools", "tools.*", "hermes_cli", "hermes_cli.*", "gateway", "gateway.*", "tui_gateway", "tui_gateway.*", "cron", "acp_adapter", "plugins", "plugins.*", "providers", "providers.*"] [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", ] # pytest-timeout: per-test 30s hard cap with signal method. # This is the fallback inside each per-file pytest subprocess (see # scripts/run_tests_parallel.py). Per-file isolation gives every test # file a fresh Python interpreter; pytest-timeout catches Python-level # hangs within a file. addopts = "-m 'not integration' --timeout=30 --timeout-method=signal" [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. select = ["PLW1514"] [tool.ruff.lint.per-file-ignores] # Tests can intentionally exercise locale-encoding edge cases. "tests/**" = ["PLW1514"] # Skills and plugins are partially user-authored — their own conventions. "skills/**" = ["PLW1514"] "optional-skills/**" = ["PLW1514"] "plugins/**" = ["PLW1514"]