[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "hermes-agent"
version = "0.13.0"
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.1",
  "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",
  "pydantic==2.12.5",
  # 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",
]

[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"]
vercel = ["vercel==0.5.7"]
hindsight = ["hindsight-client==0.6.1"]
dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "pytest-xdist==3.8.0", "pytest-split==0.11.0", "mcp==1.26.0", "ty==0.0.21", "ruff==0.15.10"]
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "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"]
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 = [
  "ptyprocess==0.7.0; sys_platform != 'win32'",
  "pywinpty==2.0.15; sys_platform == 'win32'",
]
honcho = ["honcho-ai==2.0.1"]
mcp = ["mcp==1.26.0"]
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"]
acp = ["agent-client-protocol==0.9.0"]
# mistral: extra REMOVED 2026-05-12 — `mistralai` PyPI project quarantined
# after malicious 2.4.6 release (Mini Shai-Hulud worm). Every version of
# `mistralai` returns 404 on PyPI right now, so any pin we'd write is
# unresolvable, which breaks `uv lock --check` in CI.
#
# To restore once PyPI un-quarantines:
#   1. Verify the new release is clean (read the changelog, check Socket
#      advisory page, confirm no malicious code review findings).
#   2. Add back: mistral = ["mistralai==<verified-version>"]
#   3. Re-enable Mistral in:
#        - tools/lazy_deps.py (LAZY_DEPS["tts.mistral"], LAZY_DEPS["stt.mistral"])
#        - hermes_cli/tools_config.py (un-hide from provider picker)
#        - hermes_cli/web_server.py (re-add to dashboard STT options)
#        - tools/transcription_tools.py / tools/tts_tool.py (drop disabled stubs)
#   4. Run `uv lock` to regenerate transitives.
#   5. Optionally re-add to [all] only after a few days of clean operation.
bedrock = ["boto3==1.42.89"]
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.
web = ["fastapi==0.133.1", "uvicorn[standard]==0.41.0"]
rl = [
  "atroposlib @ git+https://github.com/NousResearch/atropos.git@c20c85256e5a45ad31edf8b7276e9c5ee1995a30",
  "tinker @ git+https://github.com/thinking-machines-lab/tinker.git@30517b667f18a3dfb7ef33fb56cf686d5820ba2b",
  "fastapi==0.133.1",
  "uvicorn[standard]==0.41.0",
  "wandb==0.25.1",
]
yc-bench = ["yc-bench @ git+https://github.com/collinear-ai/yc-bench.git@bfb0c88062450f46341bd9a5298903fc2e952a5c ; python_version >= '3.12'"]
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, 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[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", "rl_cli", "utils"]

[tool.setuptools.package-data]
hermes_cli = ["web_dist/**/*"]
gateway = ["assets/**/*"]

[tool.setuptools.packages.find]
include = ["agent", "agent.*", "tools", "tools.*", "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.)",
]
addopts = "-m 'not integration' -n auto"

[tool.ty.environment]
python-version = "3.13"

[tool.ty.rules]
unknown-argument = "warn"
redundant-cast = "ignore"

[tool.ty.src]
exclude = ["tinker-atropos"]

[tool.ruff]
exclude = ["tinker-atropos"]
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"]
