Hermes Agent plugins now declare and self-install Python dependencies
- hermes
- plugins
- python
- dependencies
- cli

Hermes Agent merged a PR that lets directory plugins declare Python dependencies and have them managed automatically. The PR is #113851 by teknium1, merged September 17, 2026. 785 additions across 17 files.
The problem
Before this PR, a plugin's python_dependencies declaration was "declare and surface only" -- the packages had to be manually uv pip installed into the Hermes venv and were lost on every venv rebuild. Plugins that shipped a pyproject.toml would install but not load because their Python packages were absent.
The Mnemosyne team needed exactly this contract. The upstream fix (#102765) ships a full package manager (2,547 files) and could not be reviewed or merged as one unit. This PR is the ~1.5k-LOC slice on today's single venv.
What changed
The new module hermes_cli/plugin_python_deps.py (443 LOC, every function CC < 10) reads the declaration from pyproject.toml (preferred) or plugin.yaml manifest python_dependencies. It drops environment-marker-excluded specs, handles hermes-agent self-deps and direct-URL requirements, then installs through tools.lazy_deps.install_specs with constraints built from Hermes' own pinned dependencies and the union of every enabled peer's specs.
The lazy_deps.install_specs ladder now accepts a constraints parameter -- a pinned set derived from Hermes' own uv.lock plus the enabled peer plugins' declared specs. A candidate that conflicts with any constraint is refused before the tree moves into place. A dry-run resolve of core + enabled peers + candidate runs first. A conflict raises PluginOperationError naming the pins. Nothing is installed, no peer is touched. --no-deps on hermes plugins install opts one install out.
hermes update re-applies dependencies on the git, zip, and both venv-repair paths across the default home plus every live profile. If the union no longer resolves, each plugin resolves alone so only culprits drop. Dropped plugins are disabled through the real config writer with a loud line naming the fix -- the update never blocks.
python_runtime: external marks sidecar-venv plugins (Mnemosyne's shape) -- Hermes installs nothing and they never join the union.
Live evidence
The PR documents 15 test cases run in a scratch venv with real uv and real PyPI:
| Case | Result |
|---|---|
| pyproject plugin tabulate>=0.9 + pywin32; sys_platform=='win32' | tabulate 0.10.0 installed, Windows-only spec skipped |
| manifest python_dependencies: [art>=6.0] | installed |
| httpx<0.20 vs core httpx==0.28.1 | refused, dir absent, httpx stays 0.28.1 |
| tabulate<0.9 vs enabled peer tabulate>=0.9 | refused, peer untouched |
| python_runtime: external + torch==99 in pyproject | nothing installed |
| validate: mnemosyne-memory (no __init__.py) | ✗ loadable -- nothing to load |
| uninstall tabulate+art, run re-apply | both back |
| edit plugin to httpx<0.20, run re-apply | only culprit disabled, other kept |
| security.allow_lazy_installs: false | plugin installs, deps do not |
| Desktop path dashboard_install_plugin | deps installed, conflict → ok: False |
| Mnemosyne thin wrapper (pyproject + re-export) | installed, discover_memory_providers → ('mnemosyne', available=True) |
Validation and scope
23 of 105 catalog plugins already ship a pyproject.toml. Against core constraints, 22 of those 23 resolve. The outlier is lancedb -- requests>=2.34.2 conflicts with the core CVE pin requests==2.33.0 -- that one will be refused from the catalog until the author relaxes it. An upstream PR was opened.
hermes plugins validate and catalog CI now catch hollow submissions. A plugin.yaml with no __init__.py, desktop/plugin.js, or plugin.json beside it fails loadable -- which is what would have caught the hollow mnemosyne-memory submission (#113581). Declared specs must parse and be index specs; URL specs warn.
Unit tests cover two invariants: conflicting candidate refused with peers untouched, and re-apply drops only the culprit while keeping the memory provider. 39 touched test files, 587 pass.
Implementation details
The constraint resolution works in three phases. Phase one -- declaration read -- parses pyproject.toml [tool.uv] or [project] section first, falls back to plugin.yaml python_dependencies. Environment markers are evaluated against the running platform; markers that do not match are dropped, not flagged. Self-deps (hermes-agent) and direct-URL requirements (git+https://...) are accepted and surfaced to the user rather than silently dropped.
Phase two -- dry-run resolve -- builds a candidate set of core constraints plus enabled peer specs plus the new plugin's specs, then resolves. A conflict anywhere raises PluginOperationError with the specific pins that clash. The tree never moves into place on a conflict.
Phase three -- install -- passes the resolved spec set to tools.lazy_deps.install_specs with the constraints parameter. The loader still never installs at import time; its hint now points at hermes plugins enable <name>.
The re-apply path in update_cmd_deps runs after every update path -- git, zip, and both venv-repair branches. It covers the default home, every live profile sharing the venv, and custom HERMES_HOME roots. If the union no longer resolves, each plugin resolves alone so only culprits drop. The update never blocks on a dropped plugin; the config writer disables it with a loud line naming the fix.
The Mnemosyne pattern
The Mnemosyne memory provider is the working example. A thin wrapper plugin declares mnemosyne-hermes>=0.7,<0.8 and mnemosyne-memory[embeddings]>=3.11.1 in pyproject.toml. The wrapper's __init__.py re-exports mnemosyne_hermes. After install, discover_memory_providers() returns ('mnemosyne', available=True). Uninstall all five packages, run re-apply, the provider is available again.
This is the plugin contract the Mnemosyne team agreed on: declare dependencies, Hermes manages the venv, the plugin just works. The contract covers the default home, custom HERMES_HOME profiles, and the Desktop install path through dashboard_install_plugin.
[^1]: teknium1. "feat: plugins install their declared Python dependencies and keep them across hermes update." GitHub PR #113851. September 17, 2026.
[^2]: ethernet8023. "#102765 §3 -- prior art and design discussion." GitHub PR #102765.
[^3]: dplush / abdiisan. "mnemosyne-oss/mnemosyne#859 -- contract discussion." GitHub Issue.