mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
The local vault key sat in vault.key next to vault.json.enc, so any backup, synced folder or copied ~/.hermes carried the ciphertext together with its key. vault.key_storage: keychain (default stays `file`) puts a NEW vault's key in the OS keychain via keyring (new lazy `keychain` extra, keyring==25.7.0), one entry per profile, named by a non-secret vault.keyref. No reachable keychain (headless Linux, extra not installed, plaintext/null keyring backends) falls back to the 0600 file. `hermes vault key` shows where the key is; `hermes vault key migrate --to keychain|file` writes the destination, proves it decrypts the vault, and only then removes the source, so an interrupted move leaves a working vault and a rerun finishes it. A vault whose key is missing (file or keychain entry) now raises a recovery message and never mints a new key over existing ciphertext (before, a deleted vault.key was silently replaced and the vault reported "key mismatch or corruption"). Opt-in rather than default: macOS can prompt for keychain access after Hermes' Python changes, locked Linux keyrings prompt or are absent over SSH, cron while logged out cannot read the key, and backups stop being self-contained.
815 lines
45 KiB
TOML
815 lines
45 KiB
TOML
|
|
|
|
|
|
|
|
[project]
|
|
name = "hermes-agent"
|
|
version = "0.0.0"
|
|
description = "The self-improving AI agent — creates skills from experience, improves them during use, and runs anywhere"
|
|
readme = "README.md"
|
|
# 3.14 is the newest python we know we have wheels for.
|
|
# we *only* support 3.14, BUT we need to allow old hermes installs on <3.14
|
|
# to get thru their update step on old python.
|
|
# Shortly after they do, we update to our latest python.
|
|
# See tests/compat/README.md for the updater contract this keeps.
|
|
requires-python = ">=3.11,<3.15"
|
|
authors = [{ name = "Nous Research" }]
|
|
license = "MIT"
|
|
license-files = ["LICENSE"]
|
|
dependencies = [
|
|
# Core — every direct dep is exact-pinned to ==X.Y.Z (no ranges).
|
|
# Rationale: ranges allow PyPI to ship a fresh version of a transitive
|
|
# at any time without a code review on our side. Exact pins mean the
|
|
# only way a new package version reaches a user is via an intentional
|
|
# update on our end (bump the pin in this file, regenerate uv.lock).
|
|
# This was tightened on 2026-05-12 in response to the Mini Shai-Hulud
|
|
# worm hitting mistralai 2.4.6 on PyPI; if that release had been
|
|
# captured by `mistralai>=2.3.0,<3` rather than an exact pin, every
|
|
# install in the hours before the quarantine would have pulled it.
|
|
#
|
|
# After a pin change, run `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.54.0; python_version >= '3.14'",
|
|
"certifi==2026.10.19; 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.5; python_version >= '3.14'",
|
|
"python-dotenv==1.2.4; python_version >= '3.14'",
|
|
"fire==0.7.1; python_version >= '3.14'",
|
|
"httpx[socks]==0.28.1; python_version >= '3.14'",
|
|
"rich==14.3.4; python_version >= '3.14'",
|
|
"tenacity==9.2.1; 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'",
|
|
# 0.18.16 only pulls in its C parser below 3.14, so every 3.14 install parsed YAML
|
|
# (plugin manifests, config) in pure Python: ~5 s of gateway startup on 1 vCPU.
|
|
# Declared where clib ships a cp314 wheel; elsewhere hermes_yaml falls back to pure.
|
|
"ruamel.yaml.clib==0.2.15; python_version >= '3.14' and platform_python_implementation == 'CPython' and (sys_platform == 'darwin' or (sys_platform == 'linux' and platform_machine in 'x86_64 aarch64 i686') or (sys_platform == 'win32' and platform_machine in 'AMD64 x86'))",
|
|
"requests==2.34.2; 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.14.1; python_version >= '3.14'",
|
|
# Interactive CLI (prompt_toolkit is used directly by cli.py)
|
|
"prompt_toolkit==3.0.53; python_version >= '3.14'",
|
|
# Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter).
|
|
"croniter==6.2.4; 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.3; 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.11.2; python_version >= '3.14'",
|
|
# Skills Hub (GitHub App JWT auth — optional, only needed for bot identity)
|
|
"PyJWT[crypto]==2.15.1; python_version >= '3.14'", # PYSEC-2026-175/177/178/179 + GHSA-ffc3-869f-jxw9, GHSA-42vr-xj54-vc7v (fixed in 2.14.0/2.15.0)
|
|
# urllib3 2.8.0 fixes GHSA-8988-9cw3-xx77 (proxy TLS config ignored),
|
|
# GHSA-gh4c-6fx4-qh6g (chunked-deflate infinite loop) and
|
|
# GHSA-vxq7-64xx-v4gw (unbounded chunk buffering). 2.8.0 is the latest
|
|
# release older than 3 days and still supports Python >= 3.10
|
|
# (3.11 bridge intact).
|
|
"urllib3>=2.8.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.2; 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'",
|
|
# Engine of the default browser_exec tool (the `browser-use` CLI is a thin
|
|
# wrapper over it). Pure Python, and its pins match Pillow/websockets above,
|
|
# so it rides in the main venv and every install, Desktop bundle included,
|
|
# ships it without a separate tool environment.
|
|
"browser-harness==0.1.13; 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==13.0.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.5.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.30; sys_platform == 'win32' and python_version >= '3.14'",
|
|
# First-party lifecycle and shared-metrics runtime. Relay 0.10 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.10,<0.11; ((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.125.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.26.0"]
|
|
firecrawl = ["firecrawl-py==4.53.2"]
|
|
parallel-web = ["parallel-web==0.6.0"]
|
|
ddgs = ["ddgs==9.16.0"]
|
|
# Image generation backends
|
|
fal = ["fal-client==0.14.1"]
|
|
# Edge TTS — default TTS provider but still optional (users can pick
|
|
# ElevenLabs / OpenAI / MiniMax instead).
|
|
edge-tts = ["edge-tts==7.3.1"]
|
|
# 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') and sys_platform != 'android'",
|
|
# 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') and sys_platform != 'android'",
|
|
"soundfile==0.14.0; (platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin') and sys_platform != 'android'",
|
|
]
|
|
# onnxruntime has no Intel macOS or Android 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') and sys_platform != 'android'",
|
|
]
|
|
modal = ["modal==1.6.2"]
|
|
daytona = ["daytona==0.225.0"]
|
|
vercel = ["vercel==0.12.0"]
|
|
google-meet = ["playwright==1.64.0; sys_platform != 'android'", "websockets==15.0.1; sys_platform != 'android'"]
|
|
messaging = [
|
|
"python-telegram-bot[webhooks]==22.8",
|
|
"discord.py[voice]==2.7.1",
|
|
"aiohttp==3.14.5",
|
|
"brotlicffi==1.2.0.2",
|
|
"slack-bolt==1.30.0",
|
|
"slack-sdk==3.45.0",
|
|
"qrcode==7.4.2",
|
|
] # aiohttp 3.14.5: prior CVEs + GHSA-9xw5-w5jj-pvcg/GHSA-2g87-pp6c-29x5/GHSA-5wr8-gcqf-gqf3 family
|
|
cron = [] # croniter is now a core dependency; this extra kept for back-compat
|
|
slack = ["slack-bolt==1.30.0", "slack-sdk==3.45.0", "aiohttp==3.14.5"]
|
|
# 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.32.0; sys_platform == 'linux'",
|
|
"aiohttp-socks==0.12.0; sys_platform == 'linux'",
|
|
"aiohttp==3.14.5; sys_platform == 'linux'",
|
|
] # aiohttp 3.14.5: prior CVEs + GHSA-9xw5-w5jj-pvcg/GHSA-2g87-pp6c-29x5/GHSA-5wr8-gcqf-gqf3 family (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"]
|
|
# Opt-in OS-keychain storage for the local credential vault key (vault.key_storage: keychain).
|
|
# Lazy-installed by `hermes vault key migrate --to keychain`, so NOT in [all].
|
|
keychain = ["keyring==25.7.0"]
|
|
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') and sys_platform != 'android'",
|
|
"sounddevice==0.5.6",
|
|
"numpy==2.5.4",
|
|
]
|
|
# 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.6",
|
|
"numpy==2.5.4",
|
|
]
|
|
# 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') and sys_platform != 'android'",
|
|
]
|
|
audio-io = ["sounddevice==0.5.6", "numpy==2.5.4"]
|
|
# 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.43.0; 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.33.0"]
|
|
# Cloud memory providers are opt-in PM extras. They stay outside [all]
|
|
# so a quarantined release cannot break fresh installs.
|
|
# 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.x 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.3.0",
|
|
"httpx2==2.15.1",
|
|
"starlette==1.9.0",
|
|
] # 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 = []
|
|
sms = ["aiohttp==3.14.5"]
|
|
teams = [
|
|
"microsoft-teams-apps==2.1.1",
|
|
"aiohttp==3.14.5",
|
|
] # aiohttp 3.14.5: prior CVEs + GHSA-9xw5-w5jj-pvcg/GHSA-2g87-pp6c-29x5/GHSA-5wr8-gcqf-gqf3 family
|
|
# 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.3.0",
|
|
"httpx2==2.15.1",
|
|
"starlette==1.9.0",
|
|
] # 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).
|
|
# 2.9.2+ drops the opentelemetry-semantic-conventions<0.61 cap that blocked
|
|
# the [otlp] OpenTelemetry 1.45.x bump.
|
|
# 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.10.1"]
|
|
# 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.45.1",
|
|
"opentelemetry-exporter-otlp-proto-http==1.45.1",
|
|
]
|
|
bedrock = ["boto3==1.43.115"]
|
|
vertex = ["google-auth==2.62.0"]
|
|
azure-identity = ["azure-identity==1.26.0"]
|
|
termux = [
|
|
# Baseline Android / Termux path for reliable fresh installs.
|
|
"python-telegram-bot[webhooks]==22.8",
|
|
"hermes-agent[cron]",
|
|
"hermes-agent[mcp]",
|
|
"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 lazy-install at
|
|
# first use, so they're no longer eager-installed here.
|
|
"hermes-agent[termux]",
|
|
"hermes-agent[google]",
|
|
"hermes-agent[sms]",
|
|
"hermes-agent[web]",
|
|
"hermes-agent[pty]",
|
|
]
|
|
dingtalk = [
|
|
"dingtalk-stream==0.24.3",
|
|
"alibabacloud-dingtalk==2.2.65",
|
|
"qrcode==7.4.2",
|
|
]
|
|
feishu = ["lark-oapi==1.7.5", "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.201.0",
|
|
"google-auth==2.62.0",
|
|
"google-auth-oauthlib==1.5.0",
|
|
"google-auth-httplib2==0.4.4",
|
|
# 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.54.0",
|
|
"starlette==1.9.0",
|
|
"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[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.22",
|
|
"pytest==9.1.1",
|
|
"pytest-asyncio==1.4.0",
|
|
"mcp==2.3.0",
|
|
"httpx2==2.15.1",
|
|
"starlette==1.9.0",
|
|
"ty==0.0.89",
|
|
"ruff==0.15.22",
|
|
"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.4; sys_platform == 'win32'",
|
|
"anthropic==0.125.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'"
|
|
# 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') and sys_platform != 'android'"
|
|
# NeuTTS excludes Python 3.14.
|
|
neutts = "python_version < '3.14'"
|
|
# KittenTTS: onnxruntime has no Intel macOS or Android 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') and sys_platform != 'android'"
|
|
piper = "(platform_machine != 'ARM64' or sys_platform != 'win32') and (platform_machine != 'x86_64' or sys_platform != 'darwin') and sys_platform != 'android'"
|
|
# no playwright on android
|
|
google-meet = "sys_platform != 'android'"
|
|
|
|
[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.2",
|
|
# browser-harness 0.1.13 (core dependency, latest release) pins
|
|
# pillow==12.3.0 exactly, which holds Pillow below 13.0.0, the first
|
|
# release fixing GHSA-8g5g-8vmr-9782, GHSA-w889-685w-c8fh and the rest of
|
|
# the 13.0.0 advisory set. browser-harness only opens, converts, composes
|
|
# and draws images (Image.open/new/convert, ImageDraw, ImageFont,
|
|
# ImageOps.contain), none of which changed in 13. Keep this exact version
|
|
# equal to the direct dependency (uv overrides replace that requirement
|
|
# too). PM's install-time `uv pip check` accepts the resulting mismatch
|
|
# because uv.lock records this override (pm/pip_check.py).
|
|
# Remove this line when browser-harness allows Pillow 13. Plugin workspaces
|
|
# inherit [tool.uv], so a plugin requiring pillow<13 also gets 13.0.0.
|
|
"Pillow==13.0.0",
|
|
# 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'",
|
|
]
|
|
# Release-age check: Hermes's dependencies must be at least 3 days old when Hermes
|
|
# locks them. It runs where uv.lock is resolved (`hermes pm lock`, and CI's lock
|
|
# check), never on users' machines: PM installs from this vetted lock and drops the
|
|
# cutoff from what it generates (pm/workspace.py::_trust_core_lock), because many
|
|
# package mirrors publish no upload times and uv rejects a file it cannot date
|
|
# (#135589). Exact pins follow the same 3-day rule. The one exemption is CA
|
|
# trust stores: a release that revokes or distrusts a compromised CA must reach
|
|
# the lock the day it ships, because holding it back keeps the bad root trusted.
|
|
# Only packages whose job is CA trust belong in exclude-newer-package.
|
|
exclude-newer = "3 days"
|
|
exclude-newer-package = { certifi = 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"]
|