feat(nix): declarative plugin installation for NixOS module (#15953)
* feat(nix): parameterize dependency-groups in python.nix
* refactor(nix): extract package to callPackage-able hermes-agent.nix
Makes the package overridable via .override{} and adds
extraPythonPackages parameter for PYTHONPATH injection.
Includes build-time collision check using PEP 503 name
canonicalization.
* feat(nix): add overlay for external NixOS consumption
External flakes can now add overlays = [ inputs.hermes-agent.overlays.default ]
to get pkgs.hermes-agent with full .override support.
* test(nix): add check for extraPythonPackages PYTHONPATH injection
Verifies wrapper has PYTHONPATH when extras provided, and
base package has no PYTHONPATH without extras.
* feat(nix): add extraPlugins option for directory-based plugins
Symlinks plugin packages into HERMES_HOME/plugins/ at activation time.
Validates plugin.yaml presence. Asserts unique plugin names at eval time.
Hermes discovers them automatically via its directory scan.
* feat(nix): add extraPythonPackages option for entry-point plugins
Overrides the hermes package with PYTHONPATH injection when
extraPythonPackages is non-empty. Plugin .dist-info directories
become visible to importlib.metadata for entry-point discovery.
Works in both native systemd and container modes.
* docs: add NixOS declarative plugin installation to nix-setup, plugins, and build-a-plugin guides
- nix-setup.md: new Plugins section with extraPlugins/extraPythonPackages
examples, overlay usage, collision checking note, options reference rows
- plugins.md: Nix row in discovery table, NixOS declarative plugins section
- build-a-hermes-plugin.md: Distribute for NixOS section after pip section
* fix: address review feedback — remove unrelated umask, fix fetchFromGitHub naming, simplify checks
- Remove accidentally introduced umask/migration changes (unrelated to plugins)
- Add pluginName helper, fix fetchFromGitHub producing name='source'
- Show name= in extraPlugins example docs
- Simplify checks.nix: use hermes-agent.override instead of re-callPackage
- Fix fragile grep shell logic in checks
* refactor: address simplify feedback — lib.getName, drop unused inputs', Python list for extras
- Use lib.getName instead of custom pluginName helper
- Drop unused inputs' from checks.nix perSystem args
- Pass extraPythonPackages as Python list literal instead of colon-split string
* fix: walk propagatedBuildInputs for plugin PYTHONPATH and collision check
Uses python312.pkgs.requiredPythonModules to resolve the full transitive
closure of extraPythonPackages. Without this, a plugin with third-party
deps (e.g. requests) would fail at runtime if those deps weren't already
in the sealed uv2nix venv. The collision check now also scans the full
closure, catching transitive conflicts.
* cleanup: fold plugins into subdir loop, use find for symlink cleanup, inline lib.getName
- Add 'plugins' to the existing cron/sessions/logs/memories subdir loop
instead of a separate mkdir/chown/chmod block
- Replace fragile for-glob with find -delete for stale symlink cleanup
- Inline lib.getName at both call sites, remove pluginName wrapper
This commit is contained in:
@@ -599,6 +599,93 @@ The `preStart` script creates a GC root at `${stateDir}/.gc-root` pointing to th
|
||||
|
||||
---
|
||||
|
||||
## Plugins
|
||||
|
||||
The NixOS module supports declarative plugin installation — no imperative `hermes plugins install` needed.
|
||||
|
||||
### Directory Plugins (`extraPlugins`)
|
||||
|
||||
For plugins that are just a source tree with `plugin.yaml` + `__init__.py` (e.g., [hermes-lcm](https://github.com/stephenschoettler/hermes-lcm)):
|
||||
|
||||
```nix
|
||||
services.hermes-agent.extraPlugins = [
|
||||
(pkgs.fetchFromGitHub {
|
||||
owner = "stephenschoettler";
|
||||
repo = "hermes-lcm";
|
||||
rev = "v0.7.0";
|
||||
hash = "sha256-...";
|
||||
})
|
||||
];
|
||||
```
|
||||
|
||||
Plugins are symlinked into `$HERMES_HOME/plugins/` at activation time. Hermes discovers them via its normal directory scan. Removing a plugin from the list and running `nixos-rebuild switch` removes the symlink.
|
||||
|
||||
### Entry-Point Plugins (`extraPythonPackages`)
|
||||
|
||||
For pip-packaged plugins that register via `[project.entry-points."hermes_agent.plugins"]` (e.g., [rtk-hermes](https://github.com/ogallotti/rtk-hermes)):
|
||||
|
||||
```nix
|
||||
services.hermes-agent.extraPythonPackages = [
|
||||
(pkgs.python312Packages.buildPythonPackage {
|
||||
pname = "rtk-hermes";
|
||||
version = "1.0.0";
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "ogallotti";
|
||||
repo = "rtk-hermes";
|
||||
rev = "v1.0.0";
|
||||
hash = "sha256-...";
|
||||
};
|
||||
format = "pyproject";
|
||||
build-system = [ pkgs.python312Packages.setuptools ];
|
||||
})
|
||||
];
|
||||
```
|
||||
|
||||
The package's `site-packages` is added to PYTHONPATH in the hermes wrapper. `importlib.metadata` discovers the entry point at session start.
|
||||
|
||||
### Combining Both
|
||||
|
||||
A directory plugin with third-party Python dependencies needs both options:
|
||||
|
||||
```nix
|
||||
services.hermes-agent = {
|
||||
extraPlugins = [ my-plugin-src ]; # plugin source
|
||||
extraPythonPackages = [ pkgs.python312Packages.redis ]; # its Python dep
|
||||
extraPackages = [ pkgs.redis ]; # system binary it needs
|
||||
};
|
||||
```
|
||||
|
||||
### Using the Overlay
|
||||
|
||||
External flakes can override the package directly:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs.hermes-agent.url = "github:NousResearch/hermes-agent";
|
||||
outputs = { hermes-agent, nixpkgs, ... }: {
|
||||
nixpkgs.overlays = [ hermes-agent.overlays.default ];
|
||||
# Then: pkgs.hermes-agent.override { extraPythonPackages = [...]; }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Configuration
|
||||
|
||||
Plugins still need to be enabled in `config.yaml`. Add them via the declarative settings:
|
||||
|
||||
```nix
|
||||
services.hermes-agent.settings.plugins.enabled = [
|
||||
"hermes-lcm"
|
||||
"rtk-rewrite"
|
||||
];
|
||||
```
|
||||
|
||||
:::note
|
||||
A build-time collision check prevents plugin packages from shadowing core hermes dependencies. If a plugin provides a package already in the sealed venv, `nixos-rebuild` fails with a clear error.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Dev Shell
|
||||
@@ -721,6 +808,8 @@ nix build .#checks.x86_64-linux.config-roundtrip # merge script preserves use
|
||||
|---|---|---|---|
|
||||
| `extraArgs` | `listOf str` | `[]` | Extra args for `hermes gateway` |
|
||||
| `extraPackages` | `listOf package` | `[]` | Extra packages on service PATH (native mode only) |
|
||||
| `extraPlugins` | `listOf package` | `[]` | Directory plugin packages to symlink into `$HERMES_HOME/plugins/`. Each must contain `plugin.yaml` |
|
||||
| `extraPythonPackages` | `listOf package` | `[]` | Python packages added to PYTHONPATH for entry-point plugin discovery. Build with `python312Packages` |
|
||||
| `restart` | `str` | `"always"` | systemd `Restart=` policy |
|
||||
| `restartSec` | `int` | `5` | systemd `RestartSec=` value |
|
||||
|
||||
|
||||
@@ -633,6 +633,43 @@ pip install hermes-plugin-calculator
|
||||
# Plugin auto-discovered on next hermes startup
|
||||
```
|
||||
|
||||
### Distribute for NixOS
|
||||
|
||||
NixOS users can install your plugin declaratively if you provide a `pyproject.toml` with entry points:
|
||||
|
||||
**Entry-point plugins** (recommended for distribution):
|
||||
```nix
|
||||
# User's configuration.nix
|
||||
services.hermes-agent.extraPythonPackages = [
|
||||
(pkgs.python312Packages.buildPythonPackage {
|
||||
pname = "my-plugin";
|
||||
version = "1.0.0";
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "you";
|
||||
repo = "hermes-my-plugin";
|
||||
rev = "v1.0.0";
|
||||
hash = "sha256-..."; # nix-prefetch-url --unpack
|
||||
};
|
||||
format = "pyproject";
|
||||
build-system = [ pkgs.python312Packages.setuptools ];
|
||||
})
|
||||
];
|
||||
```
|
||||
|
||||
**Directory plugins** (no `pyproject.toml` needed):
|
||||
```nix
|
||||
services.hermes-agent.extraPlugins = [
|
||||
(pkgs.fetchFromGitHub {
|
||||
owner = "you";
|
||||
repo = "hermes-my-plugin";
|
||||
rev = "v1.0.0";
|
||||
hash = "sha256-...";
|
||||
})
|
||||
];
|
||||
```
|
||||
|
||||
See the [Nix Setup guide](/docs/getting-started/nix-setup#plugins) for complete documentation including overlay usage and collision checking.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
**Handler doesn't return JSON string:**
|
||||
|
||||
@@ -99,6 +99,7 @@ Project-local plugins under `./.hermes/plugins/` are disabled by default. Enable
|
||||
| User | `~/.hermes/plugins/` | Personal plugins |
|
||||
| Project | `.hermes/plugins/` | Project-specific plugins (requires `HERMES_ENABLE_PROJECT_PLUGINS=true`) |
|
||||
| pip | `hermes_agent.plugins` entry_points | Distributed packages |
|
||||
| Nix | `services.hermes-agent.extraPlugins` / `extraPythonPackages` | NixOS declarative installs — see [Nix Setup](/docs/getting-started/nix-setup#plugins) |
|
||||
|
||||
Later sources override earlier ones on name collision, so a user plugin with the same name as a bundled plugin replaces it.
|
||||
|
||||
@@ -155,6 +156,23 @@ Hermes has three kinds of plugins:
|
||||
|
||||
Memory providers and context engines are **provider plugins** — only one of each type can be active at a time. General plugins can be enabled in any combination.
|
||||
|
||||
## NixOS declarative plugins
|
||||
|
||||
On NixOS, plugins can be installed declaratively via the module options — no `hermes plugins install` needed. See the **[Nix Setup guide](/docs/getting-started/nix-setup#plugins)** for full details.
|
||||
|
||||
```nix
|
||||
services.hermes-agent = {
|
||||
# Directory plugin (source tree with plugin.yaml)
|
||||
extraPlugins = [ (pkgs.fetchFromGitHub { ... }) ];
|
||||
# Entry-point plugin (pip package)
|
||||
extraPythonPackages = [ (pkgs.python312Packages.buildPythonPackage { ... }) ];
|
||||
# Enable in config
|
||||
settings.plugins.enabled = [ "my-plugin" ];
|
||||
};
|
||||
```
|
||||
|
||||
Declarative plugins are symlinked with a `nix-managed-` prefix — they coexist with manually installed plugins and are cleaned up automatically when removed from the Nix config.
|
||||
|
||||
## Managing plugins
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user