mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
* feat(attribution): conflict-free contributor mappings via contributors/emails/ directory
The AUTHOR_MAP dict in scripts/release.py was a merge-conflict magnet:
every concurrent salvage PR appended entries to the same lines of the
same file, so parallel PRs re-conflicted on every merge to main.
New system: one file per email under contributors/emails/ — filename is
the commit-author email, first non-comment line is the GitHub login.
File additions never conflict, so any number of PRs can add mappings
concurrently.
- scripts/release.py: AUTHOR_MAP is now LEGACY_AUTHOR_MAP (frozen)
merged with the directory at import time (directory wins). All
existing consumers (resolve_author, contributor_audit.py) unchanged.
- scripts/add_contributor.py: idempotent CLI to add a mapping; refuses
conflicting reassignments (incl. against the legacy map), validates
email/login shapes.
- contributor-check.yml: attribution gate now accepts a mapping file OR
a legacy entry; failure message prints the exact add_contributor
command. Also auto-resolves bare <login>@users.noreply.github.com
emails is intentionally NOT added (kept id+login form only, matching
previous behavior).
- contributor_audit.py: guidance now points at add_contributor.py.
- tests/scripts/test_contributor_map.py: 12 tests covering loader,
merge precedence, CLI idempotency/conflict/validation, subprocess E2E.
* feat(ci): one-shot per-file flake retry in the parallel test runner
A failing test FILE is re-run once in a fresh subprocess. Pass-on-retry
counts as green but is loudly reported in a '⚠ FLAKY' summary section
(with both attempts' output preserved) so the flake gets fixed instead
of eating a full-run rerun. Deterministic failures fail both attempts —
regressions cannot be laundered green.
- --file-retries N / HERMES_TEST_FILE_RETRIES (default 1, 0 disables)
- E2E verified: simulated first-run-fail flake goes green with banner;
deterministic failure still exits 1; retries=0 restores old behavior.
This converts the dominant CI failure mode (one timing-sensitive test
flaking a 4600-test shard, requiring a manual 10-minute rerun and an
agent triage loop) into a self-healing retry that costs one file's
runtime.
* test(approval): loosen wall-clock perf bounds 0.15s -> 2.0s
These guard against catastrophic regex backtracking (seconds-to-minutes
class), but 0.15s is within scheduler-stall noise on loaded shared CI
runners — test_max_accepted_separator_free_input_is_fast failed a CI
shard this week on runner load alone. 2.0s still catches the regression
class with zero flake surface.
* fix(ci): job timeouts everywhere + retries on all network installs
Reliability pass over every workflow:
- timeout-minutes on all 21 jobs that lacked one (a hung job previously
burned the 6-hour default runner budget)
- ./.github/actions/retry wrapped around every network-fetching install
that lacked it: pip installs (deploy-site, skills-index), npm ci
(deploy-site website, upload_to_pypi web + ui-tui), uv sync (docker
test deps). Deterministic build steps (npm run build) deliberately
NOT retried — split into separate steps so a real build failure fails
fast instead of retrying 3x.
* docs(agents): document the file-retry flake policy
* fix(ci): curl retries on deploy hook + skills-index probe
* fix(ci): kill the remaining transient-failure classes in workflows + Dockerfile
From the workflow reliability audit:
- tests.yml: duration-cache restore had NO restore-keys while saves use
run_id-suffixed keys — the cache never matched once, so LPT slicing
always ran blind and unbalanced slices pushed heavy files toward the
per-file timeout. One-line restore-keys fixes slice balancing.
- Label gates (lint ci-reviewed, supply-chain mcp-catalog-reviewed):
'gh pr view || true' turned an API blip into 'label absent' → false
BLOCKING failure. Now 3x retry, and API failure is reported as an API
failure instead of a missing label.
- detect-changes action: compare API retried before failing open (was
silently running all lanes on any blip).
- uv-lockfile-check: 'uv lock --check' resolves against PyPI — retried
so registry blips don't read as 'lockfile stale'.
- docker.yml merge job: imagetools create retried (Docker Hub eventual
consistency on just-pushed digests).
- Dockerfile: apt-get Acquire::Retries=3; s6-overlay ADDs converted to
curl --retry 3 (ADD cannot retry; checksums still enforced); npm
--fetch-retries=5; playwright chromium fetch retried 3x.
- Advisory artifact uploads (per-slice durations, ci-timings report)
get continue-on-error so an artifact-service blip can't fail a green
test slice.
* fix(tests): kill the two root-cause flakes — leaking pre-warm timer + env-dependent provider list
- test_tui_gateway_server.py: session.create / non-eager session.resume
arm a 50ms threading.Timer (_schedule_agent_build) that outlives its
test and fires into the NEXT test's _make_agent mock, racily
corrupting captured state (the recurring session_resume shard
failures). Replaced the per-test whack-a-mole stub with a module-wide
autouse fixture; the 3 worker-lifecycle tests that genuinely need the
deferred build opt back in via @pytest.mark.real_agent_prewarm (new
marker in pyproject).
- test_api_key_providers.py: PROVIDER_ENV_VARS is now derived from the
live PROVIDER_REGISTRY instead of a hand-list that had drifted
(missing HF_TOKEN / DEEPINFRA_API_KEY) — resolve_provider('auto')
tests failed on any machine with HF_TOKEN exported. E2E-verified with
HF_TOKEN/DEEPINFRA_API_KEY set: 42/42 pass.
* test: de-flake 30 timing-sensitive test files for loaded CI runners
Root-cause fixes from the flake audit (session-DB mining + repo sweep):
Event-based sync instead of sleep-sync:
- title_generator: mock sets threading.Event, wait(10) replaces
sleep(0.3) hoping the daemon thread got scheduled
- docker zombie_reaping / profile_gateway: poll-for-state helpers
replace fixed 1-3s sleeps (s6 transitions + SIGCHLD reaping are async)
- process_registry tree test: select()-bounded readline replaces an
unbounded blocking read (parent wedge now fails THIS test with a clear
message instead of an opaque rc=124 file kill); SIGTERM grace 1s->2s
(the 1s partition window mid-interpreter-startup is how a child PID
escaped the live-system guard in CI)
Timeout raises (loaded 8-way-sliced runners see ~5s scheduling floors;
all of these complete in ms-to-1s when healthy so the raises cost
nothing on green runs):
- subprocess/thread waits <= 2s raised to 10-15s across mcp_tool,
mcp_circuit_breaker, mcp_reconnect_retry_reset, mcp_parked_self_probe,
mcp_cancelled_error_propagation, registry, clarify_gateway, interrupt,
voice_cli_integration, docker_environment, session_store_lock_io,
planned_stop_watcher, cli_interrupt_subagent, thread_scoped_output
(joins now also assert not is_alive() so stragglers fail loudly)
- wall-clock discrimination ceilings loosened where the guarded hang is
10x larger: local_background_child_hang 4s->10s, interrupt_cleanup
setup 5s->20s + pgid-exit 30s->60s, mcp_stability grandchild spinup
5s->15s, protocol/gil-starvation fast-handler 0.5s->2s,
iso_certify_seam 1.5s->5s, wait_for_mcp_discovery 0.1s->1s
- narrow assertion windows widened: honcho first-turn wait 0.4..0.65 ->
0.25..2.0 (property is bounded-not-hung, not an exact wall-clock);
compression fork-lock TTL 1s->3s (12 refresh chances per lease);
compression-lock expiry margins symmetric (ttl 0.05->0.5, sleep 1.0)
- telegram hung-DNS bound 1.0->1.4 (fake hang is 1.5s — must stay under)
* fix(tests): repair indentation from de-flake batch edit
* fix(tests): harden env isolation and replace remaining sleep-sync races
The full 42k-test run and complete npm check surfaced three more classes:
- Environment isolation: local ~/.honcho defaultHost and SSH_* variables
leaked into Python/TUI tests. Pin the default Honcho host in the
hermetic fixture, isolate the one fallback test from ~/.honcho, and
blank SSH_* around terminalSetup tests. This flipped 20 false failures
back to deterministic behavior on developer machines.
- Background-thread sleep-sync: Honcho async writer tests patched
time.sleep globally, then busy-polled with that same mocked sleep. Under
full-suite load the poller could starve the writer. Each test now waits
on an Event emitted by the exact flush/retry transition; 30/30 passed
under 15-way contention.
- Desktop streaming: the test slept 80ms and assumed a 500ms timer could
not fire before its assertion. A loaded runner descheduled the test for
>500ms and both chunks arrived. Producer controls now gate second-chunk
and completion transitions explicitly.
Also make file-retry observability complete: a self-healed flaky file now
prints BOTH attempts' full output in the FLAKY summary. Two behavioral
runner tests prove pass-on-retry is green+loud+traceback-preserving, while
a deterministic failure remains red.
* refactor(ci): use gh bot pat, better retries
refactor(ci): use retry action for PR label fetch
the retry action now captures stdout as a step output, so it can serve
double duty: retry + output capture for commands like 'gh pr view' whose
result must be consumed by later steps.
Retry action gains:
- 'stdout' output (heredoc-delimited to preserve newlines)
- tee to temp file so stdout still streams to the job log
- step id 'retry' for output reference
Both lint.yml and supply-chain-audit.yml now use the retry action
directly with 'command: gh pr view ...' and read
steps.<id>.outputs.stdout.
ci: use AUTOFIX_BOT_PAT for all gh CLI / GitHub API auth
Replace secrets.GITHUB_TOKEN and github.token with
secrets.AUTOFIX_BOT_PAT across all workflows and composite actions
that use the gh CLI or GitHub API. The PAT has consistent permissions
across fork PRs (where GITHUB_TOKEN is read-only), avoids API rate
limit sharing with the default token, and is already used by
js-autofix.yml for the same reasons.
19 sites swapped across 9 files:
- lint.yml (3): label fetch, comment post/edit, comment update
- supply-chain-audit.yml (5): scan, critical comment, unbounded dep
comment, label fetch, mcp-catalog comment
- lockfile-diff.yml (1): PR comment post/update
- skills-index-freshness.yml (1): issue creation on degraded probe
- skills-index.yml (2): index build, trigger deploy workflow
- upload_to_pypi.yml (2): release view poll, release upload
- ci.yml (1): timings report
- deploy-site.yml (2): skills index crawl
- detect-changes/action.yml (1): compare API call
---------
Co-authored-by: ethernet <arilotter@gmail.com>
395 lines
21 KiB
TOML
395 lines
21 KiB
TOML
# PEP 639 SPDX license expression (`license = "MIT"` below) requires
|
|
# setuptools>=77. Keep this floor in lockstep with the `license` form in
|
|
# [project]; an older build backend rejects the string form.
|
|
[build-system]
|
|
requires = ["setuptools>=77.0,<83"]
|
|
build-backend = "setuptools.build_meta"
|
|
|
|
[project]
|
|
name = "hermes-agent"
|
|
version = "0.18.2"
|
|
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",
|
|
# 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",
|
|
# ``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",
|
|
# cryptography is pulled in transitively by PyJWT[crypto]; pin it explicitly
|
|
# so the WeCom/Weixin crypto paths can't drift below the CVE-fixed floor.
|
|
"cryptography==46.0.7", # CVE-2026-39892, CVE-2026-34073
|
|
# 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",
|
|
"uvicorn[standard]>=0.24.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'",
|
|
# 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.2.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'",
|
|
]
|
|
|
|
[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"]
|
|
hindsight = ["hindsight-client==0.6.1"]
|
|
dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "mcp==1.26.0", "starlette==1.0.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==81.0.0"] # starlette: CVE-2026-48710; setuptools: latest <82 (torch >=2.11 caps setuptools<82)
|
|
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.14.1", "brotlicffi==1.2.0.1", "slack-bolt==1.29.0", "slack-sdk==3.43.0", "qrcode==7.4.2"] # aiohttp 3.14.1: CVE-2026-34513/34518/34519/34520/34525 + 34993(RCE)/47265
|
|
cron = [] # croniter is now a core dependency; this extra kept for back-compat
|
|
slack = ["slack-bolt==1.29.0", "slack-sdk==3.43.0", "aiohttp==3.14.1"]
|
|
matrix = ["mautrix[encryption]==0.21.0", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0", "aiohttp==3.14.1"] # aiohttp 3.14.1: CVE-2026-34993(RCE)/47265 + 34513/34518/34519/34520/34525 (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"]
|
|
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.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) 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 `pip install hermes-agent[vision]` invocations still resolve.
|
|
vision = []
|
|
# 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.5,<1.0"]
|
|
homeassistant = ["aiohttp==3.14.1"]
|
|
sms = ["aiohttp==3.14.1"]
|
|
teams = ["microsoft-teams-apps==2.0.13.4", "aiohttp==3.14.1"] # aiohttp 3.14.1: CVE-2026-34993(RCE)/47265 + 34513/34518/34519/34520/34525
|
|
# 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"]
|
|
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.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.6.8", "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", "python-multipart==0.0.27"]
|
|
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[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.data-files]
|
|
# i18n catalogs. locales/ is a bare data directory (no __init__.py), so it is
|
|
# neither a package (packages.find) nor package-data (which attaches to a
|
|
# package). data-files ships it in the wheel; MANIFEST.in `graft locales`
|
|
# ships it in the sdist. Without this, sealed installs (pip wheel, Nix store
|
|
# venv) drop the catalogs and gateway/CLI commands surface raw i18n keys like
|
|
# `gateway.reset.header_default` (#27632, #35374, #23943).
|
|
locales = ["locales/*.yaml"]
|
|
# Shipped MCP catalog (optional-mcps/<name>/manifest.yaml). Same bare-data-dir
|
|
# case as locales: data-files ships it in the wheel, `graft optional-mcps` in
|
|
# MANIFEST.in ships it in the sdist. Without this, `hermes mcp catalog` and the
|
|
# dashboard catalog screen come up empty on packaged installs even though the
|
|
# manifests exist in the repo (hermes_cli/mcp_catalog.py:_catalog_root resolves
|
|
# the packaged dir; list_catalog() returns [] when it's missing).
|
|
#
|
|
# data-files flattens every glob match into its single target dir, so each
|
|
# catalog entry needs its OWN target to preserve the per-entry directory the
|
|
# catalog iterates over (a shared `optional-mcps/*/*` glob would collapse all
|
|
# manifests into one colliding optional-mcps/manifest.yaml). One target per
|
|
# entry; tests/test_packaging_metadata.py enforces an entry per optional-mcps/<name>.
|
|
"optional-mcps/linear" = ["optional-mcps/linear/manifest.yaml"]
|
|
"optional-mcps/n8n" = ["optional-mcps/n8n/manifest.yaml"]
|
|
|
|
[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
|
|
# <platform>" (#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", "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",
|
|
"real_agent_prewarm: opt out of the autouse stub that disables the tui_gateway deferred agent pre-warm timer",
|
|
]
|
|
# 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.
|
|
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"]
|