Skip to content

feat: plugins install their declared Python dependencies and keep them across hermes update - #113851

Merged
teknium1 merged 4 commits into
mainfrom
feat/plugin-python-deps
Sep 17, 2026
Merged

teknium1 merged 4 commits into
mainfrom
feat/plugin-python-deps

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Directory plugins can now declare Python dependencies (pyproject.toml or manifest python_dependencies) and Hermes installs them under core constraints, refuses conflicts before install, and re-applies them after hermes update — the contract agreed with the Mnemosyne team, carved onto current main instead of waiting on #102765.

Why

python_dependencies was declare-and-surface only (#15220, #64165): a plugin's packages had to be uv pip installed into the Hermes venv by hand and were lost on every venv rebuild. Mnemosyne asked for exactly this contract in the plugin-API thread; @ethernet8023 built it inside #102765, but that PR ships a whole package manager (2,547 files) and cannot be reviewed or merged as one unit. This PR is the ~1.5k-LOC slice on today's single venv. 23 of the 105 catalog plugins already ship a pyproject.toml, so this turns their installs from "installed, won't load" into working installs.

What changes

  • Install / enable / plugins update / packs / Desktop install — hermes_cli/plugin_python_deps.py reads the declaration (pyproject wins over manifest; pip_dependencies accepted as alias), drops environment-marker-excluded specs, hermes-agent self-deps and direct-URL requirements (the last are surfaced for the user), then installs through the shared tools.lazy_deps.install_specs ladder with constraints built from Hermes' own pinned dependencies and the union of every enabled peer's specs. A satisfied declaration is a no-op.
  • Conflict = refusal before the tree moves into place — dry-run resolve of core + enabled peers + candidate; 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 on the git, zip and both venv-repair paths (_reapply_plugin_python_dependencies), across the default home plus every live profile (custom HERMES_HOME roots included). If the union no longer resolves, each plugin is resolved alone so only culprits drop; remaining plugin-vs-plugin conflicts peel non-memory plugins first. Dropped plugins are disabled through the real config writer with a loud line naming the fix. Never blocks the update.
  • python_runtime: external — sidecar-venv plugins (Mnemosyne's shape) declare it; Hermes installs nothing and they never join the union.
  • hermes plugins validate / catalog CI — a plugin.yaml with no __init__.py, desktop/plugin.js or plugin.json beside it now FAILS (loadable), which is what would have caught the hollow mnemosyne-memory submission (plugin-catalog: add mnemosyne-memory #113581); declared specs must parse and be index specs (URL specs warn).
  • tools.lazy_deps.install_specs gains constraints and dry_run. Loader still never installs at import time; its hint now points at hermes plugins enable <name>.
  • Docs: developer-guide plugins § "Python dependencies".

Every new function is CC < 10 (ruff C901 max-complexity=9 on the new module passes; touched pre-existing functions were moved back to their origin/main complexity by extracting the new branches into helpers).

Live evidence (scratch venv + scratch HERMES_HOME, real uv, real PyPI)

Case Result
pyproject plugin tabulate>=0.9 + pywin32; sys_platform=='win32' installed tabulate 0.10.0, Windows-only spec skipped
manifest python_dependencies: [art>=6.0] installed
httpx<0.20 vs core httpx==0.28.1 refused, dir absent, httpx still 0.28.1
tabulate<0.9 vs enabled peer tabulate>=0.9 refused, peer untouched
requests; this is not a marker refused as invalid declaration
python_runtime: external + torch==99 in pyproject nothing installed
validate: mnemosyne-memory PR tree (no __init__.py) ✗ loadable — nothing to load
uninstall tabulate+art, run re-apply both back
edit okplugin to httpx<0.20, run re-apply only okplugin disabled; manifestplugin (alphabetically first) kept and its deps re-applied
plugins enable on already-enabled plugin with deps removed deps reinstalled
security.allow_lazy_installs: false plugin installs, deps do not, message says so
Desktop path dashboard_install_plugin deps installed, python_dependencies in result; conflict → ok: False with the same message
profile work under custom HERMES_HOME with an enabled plugin included in re-apply union
Mnemosyne: thin wrapper (plugin.yaml kind: exclusive + __init__.py re-exporting mnemosyne_hermes + pyproject mnemosyne-hermes>=0.7,<0.8, mnemosyne-memory[embeddings]>=3.11.1) installed, discover_memory_providers() → ('mnemosyne', available=True); after uninstalling all five packages, re-apply restores them and the provider is available again
All 23 catalog plugins with a pyproject, dry-run against core constraints 22 resolve; lancedb conflicts (requests>=2.34.2 vs core CVE pin requests==2.33.0) → will be refused from the catalog until the author relaxes it; --no-deps is the hatch. Upstream PR opened. cashew/weather have direct-URL deps → the rest install, URL ones are listed for the user

Unit: tests/hermes_cli/test_plugin_python_deps.py (2 invariants: conflicting candidate refused with peers untouched; re-apply drops only the culprit and keeps the memory provider). 39 touched test files, 587 pass.

Infographic

infographic

Credit: design and prior art @ethernet8023 (#102765 §3), contract discussion @dplush / @abdiisan (mnemosyne-oss/mnemosyne#859).

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 72a5844 — fix: read plugin-home config through the loader; register up

debug info

CI timings

CI timings · View report · View job

Wall time 6m9s vs 6m14s (-1.3%). 3 job(s) slower, 7 faster, 3 unchanged.

  • OS-specific tests / Windows-only tests: -21.0s
  • Python tests / Run tests: +19.0s
  • Python lints / Windows footguns (blocking): -5.0s
  • Check no committed infographics / check-no-committed-infographics: -5.0s
  • Profile artifact check / Reject profile archives: -3.0s

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins comp/cli CLI entry point, hermes_cli/, setup wizard area/install-update Installer, updater, packaging, wheels, doctor sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Sep 17, 2026
@whyyagswhy

Copy link
Copy Markdown

Independent verification on the PR head (47f8035): all three plugin suites pass locally, 24/24 on Linux (canonical runner).

Failing validation on trees with nothing loadable closes a real silent-success hole, and checking dependency declarations at validate time beats discovering them at load. No findings.

Plugin dependency installs need two things the shared installer ladder did not
expose: a constraints file (so a plugin can never move a core pin) and a dry run
(so a conflict can be detected before anything is written). Both are threaded
through _venv_pip_install; the durable-target path keeps its own core
constraints and a dry run skips syspath activation and bytecode warming.
… after hermes update

A directory plugin's pyproject.toml [project].dependencies (or manifest
python_dependencies) are now installed into the Hermes venv on install/enable/
update, resolved under a constraints file built from Hermes' own pinned
dependencies together with every enabled peer's declarations. A candidate that
cannot resolve is refused before its tree is moved into place; nothing is
installed and no other plugin is touched. --no-deps opts a single install out.

hermes update rebuilds the venv from Hermes' lock and strips everything else, so
its git, zip and both repair paths now re-apply the union of every profile's
enabled plugins. When the union no longer resolves, each plugin is resolved on
its own so only culprits are dropped (never an alphabetical neighbour); a
plugin-vs-plugin conflict peels non-memory plugins first because a Hermes that
boots without memory reads as data loss. Dropped plugins are disabled through
the real config writer with a loud message naming the fix.

python_runtime: external lets sidecar-venv plugins (Mnemosyne's shape) opt out
of the union; hermes-agent self-dependencies and direct-URL requirements are
never installed (the latter are surfaced for the user to install by hand).
…ency declarations

A plugin.yaml with no __init__.py, desktop/plugin.js or plugin.json beside it
installs "successfully" and does nothing (pip-layout repos whose code sits under
src/ behind an entry point). The validator and catalog CI now fail that shape
and check declared Python dependencies parse and are index specs; direct-URL
requirements warn.

Tests: conflicting candidate refused with peers untouched; post-update re-apply
drops only the culprit and keeps the memory provider. Loader wording test
updated to the new hint.
The config-read guard forbids raw yaml loads of config.yaml outside owner
modules, and hermes_cli.main's frozen lazy-export surface must list every
update_cmd symbol it proxies.
@teknium1
teknium1 force-pushed the feat/plugin-python-deps branch from 49ce586 to 72a5844 Compare September 17, 2026 07:04
@teknium1
teknium1 merged commit a08dee9 into main Sep 17, 2026
34 checks passed
@teknium1
teknium1 deleted the feat/plugin-python-deps branch September 17, 2026 07:11
AxDSan added a commit to mnemosyne-oss/hermes-agent that referenced this pull request Sep 17, 2026
subdir moves to integrations/hermes-catalog, the directory plugin shape
from NousResearch#113851 (plugin.yaml kind exclusive, __init__.py shim, dependency
pyproject). sha 17abb10 is the merge of mnemosyne-oss/mnemosyne#974.
Tool list trimmed to the 37 tools register() exposes in an isolated
probe; requires_hermes >=0.22.
teknium1 added a commit that referenced this pull request Sep 17, 2026
…e the capability probe

Catalog CI validates each pinned tree in a venv that has only hermes-agent, so any plugin whose
code arrives through pyproject dependencies (the wrapper shape from #113851) failed the probe with
"No module named ...". The flag runs the same constrained install `plugins install` would, then
probes; the workflow passes it. Live: mnemosyne pin fails without the flag, passes with it.
konsisumer pushed a commit to konsisumer/hermes-agent that referenced this pull request Sep 17, 2026
subdir moves to integrations/hermes-catalog, the directory plugin shape
from NousResearch#113851 (plugin.yaml kind exclusive, __init__.py shim, dependency
pyproject). sha 17abb10 is the merge of mnemosyne-oss/mnemosyne#974.
Tool list trimmed to the 37 tools register() exposes in an isolated
probe; requires_hermes >=0.22.
konsisumer pushed a commit to konsisumer/hermes-agent that referenced this pull request Sep 17, 2026
…ated Devs-Foundation entry

The Devs-Foundation/mnemosyne entry (created 2026-08-10, last push 2026-08-11, 0 stars) is
unrelated to the Mnemosyne project per its maintainers and registers the same memory-provider
name, which makes memory.provider: mnemosyne ambiguous. It is delisted (plain removal, not the
removed.yaml blocklist: nothing malicious) and welcome back under a distinct name. The official
mnemosyne-oss submission (NousResearch#113581, wrapper shape validated end to end) takes the bare key.
requires_hermes floor set to the first release that carries NousResearch#113851 (0.21.4 or later).
README gains the delist-vs-remove distinction and the provider-name collision rule.
teknium1 added a commit that referenced this pull request Sep 18, 2026
… same-name pip entry point

The pyproject wrapper shape (#113851) depends on a pip package that often ships its own
hermes_agent.plugins entry point under the same name. Discovery appended entry points last with
"later wins", so after a catalog install the plugin row became source=entrypoint with no install
dir or catalog provenance: Desktop lost "installed from catalog @ version / update to ..." for
exactly the shape the catalog now recommends. Entry points no longer displace a directory plugin
of the same key, in the loader and in the CLI/RPC listing.

Live: catalog install of mnemosyne, row before = entrypoint / mnemosyne_hermes:register / no
catalog fields; after = user / ~/.hermes/plugins/mnemosyne / catalog 0.7.0 @ 95ef3be2; provider
still discovered. Test red on base.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants