Nix/NixOS matrix integration and improvements

NousResearch/hermes-agent · cluster 4594 · environments

Data as of

Nothing open on GitHub. Fix merged in #7461. Check it covers your case.

The short answer

0open issues
0open PRs
6items grouped
Known?
Yes. 6 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #7461. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PRs #7461, #15953. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items address Matrix support on NixOS, including bug fixes for platform gating, overlay and package overridability, declarative plugin installation via NixOS module, and a migration from matrix-nio to mautrix-python.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #7461 fix(nix): gate matrix extra to Linux in [all] profile

    merged to main

    files: pyproject.toml, tests/test_project_metadata.py

    Show the diff
    • pyproject.toml
      @@ -88,10 +88,10 @@ all = [
         "hermes-agent[modal]",
         "hermes-agent[daytona]",
         "hermes-agent[messaging]",
      -  # matrix excluded: python-olm (required by matrix-nio[e2e]) is upstream-broken
      -  # on modern macOS (archived libolm, C++ errors with Clang 21+). Including it
      -  # here causes the entire [all] install to fail, dropping all other extras.
      -  # Users who need Matrix can install manually: pip install 'hermes-agent[matrix]'
      +  # matrix: python-olm (required by matrix-nio[e2e]) is upstream-broken on
      +  # modern macOS (archived libolm, C++ errors with Clang 21+).  On Linux the
      +  # [matrix] extra's own marker pulls in the [e2e] variant automatically.
      +  "hermes-agent[matrix]; sys_platform == 'linux'",
         "hermes-agent[cron]",
         "hermes-agent[cli]",
         "hermes-agent[dev]",
    • tests/test_project_metadata.py
      @@ -11,12 +11,19 @@ def _load_optional_dependencies():
           return project["optional-dependencies"]
       
       
      -def test_matrix_extra_exists_but_excluded_from_all():
      +def test_matrix_extra_linux_only_in_all():
           """matrix-nio[e2e] depends on python-olm which is upstream-broken on modern
           macOS (archived libolm, C++ errors with Clang 21+).  The [matrix] extra is
      -    kept for opt-in install but deliberately excluded from [all] so one broken
      -    upstream dep doesn't nuke every other extra during ``hermes update``."""
      +    included in [all] but gated to Linux via a platform marker so that
      +    ``hermes update`` doesn't fail on macOS."""
           optional_dependencies = _load_optional_dependencies()
       
           assert "matrix" in optional_dependencies
      +    # Must NOT be unconditional — python-olm has no macOS wheels.
           assert "hermes-agent[matrix]" not in optional_dependencies["all"]
      +    # Must be present with a Linux platform marker.
      +    linux_gated = [
      +        dep for dep in optional_dependencies["all"]
      +        if "matrix" in dep and "linux" in dep
      +    ]
      +    assert linux_gated, "expected hermes-agent[matrix] with sys_platform=='linux' marker in [all]"
    • uv.lock
      @@ -1661,7 +1661,7 @@ dependencies = [
           { name = "fal-client" },
           { name = "fire" },
           { name = "firecrawl-py" },
      -    { name = "httpx" },
      +    { name = "httpx", extra = ["socks"] },
           { name = "jinja2" },
           { name = "openai" },
           { name = "parallel-web" },
      @@ -1691,6 +1691,8 @@ all = [
           { name = "faster-whisper" },
           { name = "honcho-ai" },
           { name = "lark-oapi" },
      +    { name = "markdown", marker = "sys_platform == 'linux'" },
      +    { name = "matrix-nio", extra = ["e2e"], marker = "sys_platform == 'linux'" },
           { name = "mcp" },
           { name = "mistralai" },
           { name = "modal" },
      @@ -1827,6 +1829,7 @@ requires-dist = [
           { name = "hermes-agent", extras = ["homeassistant"], marker = "extra == 'all'" },
           { name = "hermes-agent", extras = ["honcho"], marker = "extra == 'all'" },
           { name = "hermes-agent", extras = ["honcho"], marker = "extra == 'termux'" },
      +    { name = "hermes-agent", extras = ["matrix"], marker = "sys_platform == 'linux' and extra == 'all'" },
           { name = "hermes-agent", extras = ["mcp"], marker = "extra == 'all'" },
           { name = "hermes-agent", extras = ["mcp"], marker = "extra == 'termux'" },
           { name = "hermes-agent", extras = ["messaging"], marker = "extra == 'all'" },
      @@ -1839,7 +1842,7 @@ requires-dist = [
           { name = "hermes-agent", extras = ["tts-premium"], marker = "extra == 'all'" },
           { name = "hermes-agent", extras = ["voice"], marker = "extra == 'all'" },
           { name = "honcho-ai", marker = "extra == 'honcho'", specifier = ">=2.0.1,<3" },
      -    { name = "httpx", specifier = ">=0.28.1,<1" },
      +    { name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1" },
           { name = "jinja2", specifier = ">=3.1.5,<4" },
           { name = "lark-oapi", marker = "extra == 'feishu'", specifier = ">=1.5.3,<2" },
           { name = "markdown", marker = "extra == 'matrix'", specifier = ">=3.6,<4" },
      @@ -2033,6 +2036,9 @@ wheels = [
       http2 = [
           { name = "h2" },
       ]
      +socks = [
      +    { name = "socksio" },
      +]
       
       [[package]]
       name = "httpx-sse"
      @@ -4500,6 +4506,15 @@ wheels = [
           { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
       ]
       
      +[[package]]
      +name = "socksio"
      +version = "1.0.0"
      +source = { registry = "https://pypi.org/simple" }
      +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" }
      +wheels = [
      +    { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
      +]
      +
       [[package]]
       name = "sounddevice"
       version = "0.5.5"
  • #15953 feat(nix): declarative plugin installation for NixOS module

    merged to main

    files: flake.nix, nix/checks.nix, nix/hermes-agent.nix, nix/nixosModules.nix, nix/overlays.nix, nix/packages.nix, nix/python.nix, website/docs/getting-started/nix-setup.md, website/docs/guides/build-a-hermes-plugin.md, website/docs/user-guide/features/plugins.md

    Show the diff
    • flake.nix
      @@ -36,6 +36,7 @@
       
             imports = [
               ./nix/packages.nix
      +        ./nix/overlays.nix
               ./nix/nixosModules.nix
               ./nix/checks.nix
               ./nix/devShell.nix
    • nix/checks.nix
      @@ -7,9 +7,7 @@
         perSystem = { pkgs, system, lib, ... }:
           let
             hermes-agent = inputs.self.packages.${system}.default;
      -      hermesVenv = pkgs.callPackage ./python.nix {
      -        inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
      -      };
      +      hermesVenv = hermes-agent.hermesVenv;
       
             configMergeScript = pkgs.callPackage ./configMergeScript.nix { };
       
      @@ -193,6 +191,35 @@ json.dump(sorted(leaf_paths(DEFAULT_CONFIG)), sys.stdout, indent=2)
                 echo "ok" > $out/result
               '';
       
      +        # Verify extraPythonPackages PYTHONPATH injection
      +        extra-python-packages = let
      +          testPkg = pkgs.python312Packages.pyfiglet;
      +          hermesWithExtra = hermes-agent.override {
      +            extraPythonPackages = [ testPkg ];
      +          };
      +        in pkgs.runCommand "hermes-extra-python-packages" { } ''
      +          set -e
      +          echo "=== Checking extraPythonPackages PYTHONPATH injection ==="
      +
      +          grep -q "PYTHONPATH" ${hermesWithExtra}/bin/hermes || \
      +            (echo "FAIL: PYTHONPATH not in wrapper"; exit 1)
      +          echo "PASS: PYTHONPATH present in wrapper"
      +
      +          grep -q "${testPkg}" ${hermesWithExtra}/bin/hermes || \
      +            (echo "FAIL: test package path not in PYTHONPATH"; exit 1)
      +          echo "PASS: test package path found in wrapper"
      +
      +          echo "=== Checking base package has no PYTHONPATH ==="
      +          if grep -q "PYTHONPATH" ${hermes-agent}/bin/hermes; then
      +            echo "FAIL: base package should not have PYTHONPATH"; exit 1
      +          fi
      +          echo "PASS: base package clean"
      +
      +          echo "=== All extraPythonPackages checks passed ==="
      +          mkdir -p $out
      +          echo "ok" > $out/result
      +        '';
      +
               # ── Config merge + round-trip test ────────────────────────────────
               # Tests the merge script (Nix activation behavior) across 7
               # scenarios, then verifies Python's load_config() reads correctly.
    • nix/hermes-agent.nix
      @@ -0,0 +1,186 @@
      +# nix/hermes-agent.nix — Overridable Hermes Agent package
      +#
      +# callPackage auto-wires nixpkgs args; flake inputs are passed explicitly.
      +# Users override via: pkgs.hermes-agent.override { extraPythonPackages = [...]; }
      +{
      +  lib,
      +  stdenv,
      +  makeWrapper,
      +  callPackage,
      +  python312,
      +  nodejs_22,
      +  ripgrep,
      +  git,
      +  openssh,
      +  ffmpeg,
      +  tirith,
      +  # Flake inputs — passed explicitly by packages.nix and overlays.nix
      +  uv2nix,
      +  pyproject-nix,
      +  pyproject-build-systems,
      +  npm-lockfile-fix,
      +  # Overridable parameters
      +  extraPythonPackages ? [ ],
      +}:
      +let
      +  hermesVenv = callPackage ./python.nix {
      +    inherit uv2nix pyproject-nix pyproject-build-systems;
      +  };
      +
      +  hermesNpmLib = callPackage ./lib.nix {
      +    inherit npm-lockfile-fix;
      +  };
      +
      +  hermesTui = callPackage ./tui.nix {
      +    inherit hermesNpmLib;
      +  };
      +
      +  hermesWeb = callPackage ./web.nix {
      +    inherit hermesNpmLib;
      +  };
      +
      +  bundledSkills = lib.cleanSourceWith {
      +    src = ../skills;
      +    filter = path: _type: !(lib.hasInfix "/index-cache/" path);
      +  };
      +
      +  runtimeDeps = [
      +    nodejs_22
      +    ripgrep
      +    git
      +    openssh
      +    ffmpeg
      +    tirith
      +  ];
      +
      +  runtimePath = lib.makeBinPath runtimeDeps;
      +
      +  sitePackagesPath = python312.sitePackages;
      +
      +  # Walk propagatedBuildInputs to include transitive Python deps in PYTHONPATH.
      +  # Without this, a plugin listing e.g. requests as a dep would fail at runtime
      +  # if requests isn't already in the sealed uv2nix venv.
      +  allExtraPythonPackages = python312.pkgs.requiredPythonModules extraPythonPackages;
      +
      +  pythonPath = lib.makeSearchPath sitePackagesPath allExtraPythonPackages;
      +
      +  pyprojectHash = builtins.hashString "sha256" (builtins.readFile ../pyproject.toml);
      +  uvLockHash =
      +    if builtins.pathExists ../uv.lock then
      +      builtins.hashString "sha256" (builtins.readFile ../uv.lock)
      +    else
      +      "none";
      +in
      +stdenv.mkDerivation {
      +  pname = "hermes-agent";
      +  version = (builtins.fromTOML (builtins.readFile ../pyproject.toml)).project.version;
      +
      +  dontUnpack = true;
      +  dontBuild = true;
      +  nativeBuildInputs = [ makeWrapper ];
      +
      +  installPhase = ''
      +    runHook preInstall
      +
      +    mkdir -p $out/share/hermes-agent $out/bin
      +    cp -r ${bundledSkills} $out/share/hermes-agent/skills
      +    cp -r ${hermesWeb} $out/share/hermes-agent/web_dist
      +
      +    mkdir -p $out/ui-tui
      +    cp -r ${hermesTui}/lib/hermes-tui/* $out/ui-tui/
      +
      +    ${lib.concatMapStringsSep "\n"
      +      (name: ''
      +        makeWrapper ${hermesVenv}/bin/${name} $out/bin/${name} \
      +          --suffix PATH : "${runtimePath}" \
      +          --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills \
      +          --set HERMES_WEB_DIST $out/share/hermes-agent/web_dist \
      +          --set HERMES_TUI_DIR $out/ui-tui \
      +          --set HERMES_PYTHON ${hermesVenv}/bin/python3 \
      +          --set HERMES_NODE ${nodejs_22}/bin/node \
      +          ${lib.optionalString (extraPythonPackages != [ ]) ''--suffix PYTHONPATH : "${pythonPath}"''}
      +      '')
      +      [
      +        "hermes"
      +        "hermes-agent"
      +        "hermes-acp"
      +      ]
      +    }
      +
      +    ${lib.optionalString (extraPythonPackages != [ ]) ''
      +      echo "=== Checking for plugin/core package collisions ==="
      +      ${hermesVenv}/bin/python3 -c "
      +import pathlib, sys, re
      +
      +def canonical(name):
      +    return re.sub(r'[-_.]+', '-', name).lower()
      +
      +# Collect core venv package names
      +core = set()
      +venv_sp = pathlib.Path('${hermesVenv}/${sitePackagesPath}')
      +for di in venv_sp.glob('*.dist-info'):
      +    meta = di / 'METADATA'
      +    if meta.exists():
      +        for line in meta.read_text().splitlines():
      +            if line.startswith('Name:'):
      +                core.add(canonical(line.split(':', 1)[1].strip()))
      +                break
      +
      +# Check each extra package for collisions
      +extras_dirs = [${lib.concatMapStringsSep ", " (p: "'${toString p}'") allExtraPythonPackages}]
      +for edir in extras_dirs:
      +    sp = pathlib.Path(edir) / '${sitePackagesPath}'
      +    if not sp.exists():
      +        continue
      +    for di in sp.glob('*.dist-info'):
      +        meta = di / 'METADATA'
      +        if not meta.exists():
      +            continue
      +        for line in meta.read_text().splitlines():
      +            if line.startswith('Name:'):
      +                pkg = canonical(line.split(':', 1)[1].strip())
      +                if pkg in core:
      +                    print(f'ERROR: plugin package \"{pkg}\" collides with a package in hermes sealed venv', file=sys.stderr)
      +                    print(f'  from: {di}', file=sys.stderr)
      +                    print(f'  Remove this dependency from extraPythonPackages.', file=sys.stderr)
      +                    sys.exit(1)
      +                break
      +
      +print('No collisions found.')
      +      "
      +      echo "=== No collisions ==="
      +    ''}
      +
      +    runHook postInstall
      +  '';
      +
      +  passthru = {
      +    inherit hermesTui hermesWeb hermesNpmLib hermesVenv;
      +
      +    devShellHook = ''
      +      STAMP=".nix-stamps/hermes-agent"
      +      STAMP_VALUE="${pyprojectHash}:${uvLockHash}"
      +      if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then
      +        echo "hermes-agent: installing Python dependencies..."
      +        uv venv .venv --python ${python312}/bin/python3 2>/dev/null || true
      +        source .venv/bin/activate
      +        uv pip install -e ".[all]"
      +        [ -d mini-swe-agent ] && uv pip install -e ./mini-swe-agent 2>/dev/null || true
      +        [ -d tinker-atropos ] && uv pip install -e ./tinker-atropos 2>/dev/null || true
      +        mkdir -p .nix-stamps
      +        echo "$STAMP_VALUE" > "$STAMP"
      +      else
      +        source .venv/bin/activate
      +        export HERMES_PYTHON=${hermesVenv}/bin/python3
      +      fi
      +    '';
      +  };
      +
      +  meta = with lib; {
      +    description = "AI agent with advanced tool-calling capabilities";
      +    homepage = "https://github.com/NousResearch/hermes-agent";
      +    mainProgram = "hermes";
      +    license = licenses.mit;
      +    platforms = platforms.unix;
      +  };
      +}
    • nix/nixosModules.nix
      @@ -28,6 +28,8 @@
       
         let
           cfg = config.services.hermes-agent;
      +    effectivePackage = if cfg.extraPythonPackages == [ ] then cfg.package
      +      else cfg.package.override { inherit (cfg) extraPythonPackages; };
           hermes-agent = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.default;
       
           # Deep-merge config type (from 0xrsydn/nix-hermes-agent)
      @@ -456,6 +458,52 @@
               description = "Extra packages available on PATH.";
             };
       
      +      extraPlugins = mkOption {
      +        type = types.listOf types.package;
      +        default = [ ];
      +        description = ''
      +          Directory-based plugin packages to symlink into the hermes plugins
      +          directory. Each package should contain a plugin.yaml and __init__.py
      +          at its root. Hermes discovers these automatically on startup.
      +        '';
      +        example = literalExpression ''
      +          [
      +            (pkgs.fetchFromGitHub {
      +              owner = "stephenschoettler";
      +              repo = "hermes-lcm";
      +              name = "hermes-lcm";
      +              rev = "v0.7.0";
      +              hash = "sha256-...";
      +            })
      +          ]
      +        '';
      +      };
      +
      +      extraPythonPackages = mkOption {
      +        type = types.listOf types.package;
      +        default = [ ];
      +        description = ''
      +          Python packages to add to PYTHONPATH for entry-point plugin discovery.
      +          These are pip-packaged plugins that register via the
      +          hermes_agent.plugins entry-point group. Each package must be built
      +          with the same Python interpreter as hermes (python312).
      +        '';
      +        example = literalExpression ''
      +          [
      +            (pkgs.python312Packages.buildPythonPackage {
      +              pname = "rtk-hermes";
      +              version = "1.0.0";
      +              src = pkgs.fetchFromGitHub {
      +                owner = "ogallotti";
      +                repo = "rtk-hermes";
      +                rev = "main";
      +                hash = "sha256-...";
      +              };
      +            })
      +          ]
      +        '';
      +      };
      +
             restart = mkOption {
               type = types.str;
               default = "always";
      @@ -570,7 +618,7 @@
             # so interactive shells share state (sessions, skills, cron) with the
             # gateway service instead of creating a separate ~/.hermes/.
             (lib.mkIf cfg.addToSystemPackages {
      -        environment.systemPackages = [ cfg.package ];
      +        environment.systemPackages = [ effectivePackage ];
               environment.variables.HERMES_HOME = "${cfg.stateDir}/.hermes";
             })
       
      @@ -581,6 +629,16 @@
               });
             })
       
      +      # ── Assertions ─────────────────────────────────────────────────────
      +      {
      +        assertions = let
      +          names = map lib.getName cfg.extraPlugins;
      +        in [{
      +          assertion = (lib.length names) == (lib.length (lib.unique names));
      +          message = "services.hermes-agent.extraPlugins: duplicate plugin names detected: ${toString names}. If using fetchFromGitHub, set name = \"plugin-name\" to disambiguate.";
      +        }];
      +      }
      +
             # ── Warnings ──────────────────────────────────────────────────────
             (lib.mkIf (cfg.container.enable && !cfg.addToSystemPackages && cfg.container.hostUsers != []) {
               warnings = [
      @@ -602,6 +660,7 @@
                 "d ${cfg.stateDir}/.hermes/sessions 2770 ${cfg.user} ${cfg.group} - -"
                 "d ${cfg.stateDir}/.hermes/logs   2770 ${cfg.user} ${cfg.group} - -"
                 "d ${cfg.stateDir}/.hermes/memories 2770 ${cfg.user} ${cfg.group} - -"
      +          "d ${cfg.stateDir}/.hermes/plugins 2770 ${cfg.user} ${cfg.group} - -"
                 "d ${cfg.stateDir}/home           0750 ${cfg.user} ${cfg.group} - -"
                 "d ${cfg.workingDirectory}         2770 ${cfg.user} ${cfg.group} - -"
               ];
      @@ -623,7 +682,7 @@
                 find ${cfg.stateDir}/.hermes -maxdepth 1 \
                   \( -name "*.db" -o -name "*.db-wal" -o -name "*.db-shm" -o -name "SOUL.md" \) \
                   -exec chmod g+rw {} + 2>/dev/null || true
      -          for _subdir in cron sessions logs memories; do
      +          for _subdir in cron sessions logs memories plugins; do
                   mkdir -p "${cfg.stateDir}/.hermes/$_subdir"
                   chown ${cfg.user}:${cfg.group} "${cfg.stateDir}/.hermes/$_subdir"
                   chmod 2770 "${cfg.stateDir}/.hermes/$_subdir"
      @@ -732,6 +791,22 @@ HERMES_NIX_ENV_EOF
                 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _value: ''
                   install -o ${cfg.user} -g ${cfg.group} -m 0640 ${documentDerivation}/${name} ${cfg.workingDirectory}/${name}
                 '') cfg.documents)}
      +
      +        # ── Declarative plugins ─────────────────────────────────────────
      +        # Remove stale managed symlinks (plugins removed from config)
      +        find ${cfg.stateDir}/.hermes/plugins -maxdepth 1 -type l -name 'nix-managed-*' -delete 2>/dev/null || true
      +
      +        ${lib.concatStringsSep "\n" (map (plugin:
      +          let
      +            name = lib.getName plugin;
      +          in ''
      +            if [ ! -f "${plugin}/plugin.yaml" ]; then
      +              echo "ERROR: extraPlugins entry '${plugin}' has no plugin.yaml" >&2
      +              exit 1
      +            fi
      +            ln -sfn ${plugin} ${cfg.stateDir}/.hermes/plugins/nix-managed-${name}
      +            chown -h ${cfg.user}:${cfg.group} ${cfg.stateDir}/.hermes/plugins/nix-managed-${name}
      +          '') cfg.extraPlugins)}
               '';
             }
       
      @@ -762,7 +837,7 @@ HERMES_NIX_ENV_EOF
                   # reads them at Python startup — no systemd EnvironmentFile needed.
       
                   ExecStart = lib.concatStringsSep " " ([
      -              "${cfg.package}/bin/hermes"
      +              "${effectivePackage}/bin/hermes"
                     "gateway"
                   ] ++ cfg.extraArgs);
       
      @@ -785,7 +860,7 @@ HERMES_NIX_ENV_EOF
                 };
       
                 path = [
      -            cfg.package
      +            effectivePackage
                   pkgs.bash
                   pkgs.coreutils
                   pkgs.git
      @@ -810,11 +885,11 @@ HERMES_NIX_ENV_EOF
       
                 preStart = ''
                   # Stable symlinks — container references these, not store paths directly
      -            ln -sfn ${cfg.package} ${cfg.stateDir}/current-package
      +            ln -sfn ${effectivePackage} ${cfg.stateDir}/current-package
                   ln -sfn ${containerEntrypoint} ${cfg.stateDir}/current-entrypoint
       
                   # GC roots so nix-collect-garbage doesn't remove store paths in use
      -            ${pkgs.nix}/bin/nix-store --add-root ${cfg.stateDir}/.gc-root --indirect -r ${cfg.package} 2>/dev/null || true
      +            ${pkgs.nix}/bin/nix-store --add-root ${cfg.stateDir}/.gc-root --indirect -r ${effectivePackage} 2>/dev/null || true
                   ${pkgs.nix}/bin/nix-store --add-root ${cfg.stateDir}/.gc-root-entrypoint --indirect -r ${containerEntrypoint} 2>/dev/null || true
       
                   # Check if container needs (re)creation
    • nix/overlays.nix
      @@ -0,0 +1,10 @@
      +# nix/overlays.nix — Expose pkgs.hermes-agent for external NixOS configs
      +{ inputs, ... }:
      +{
      +  flake.overlays.default = final: _: {
      +    hermes-agent = final.callPackage ./hermes-agent.nix {
      +      inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
      +      npm-lockfile-fix = inputs.npm-lockfile-fix.packages.${final.stdenv.hostPlatform.system}.default;
      +    };
      +  };
      +}
    • nix/packages.nix
      @@ -4,120 +4,19 @@
         perSystem =
           { pkgs, inputs', ... }:
           let
      -      hermesVenv = pkgs.callPackage ./python.nix {
      +      hermesAgent = pkgs.callPackage ./hermes-agent.nix {
               inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
      -      };
      -
      -      hermesNpmLib = pkgs.callPackage ./lib.nix {
               npm-lockfile-fix = inputs'.npm-lockfile-fix.packages.default;
             };
      -
      -      hermesTui = pkgs.callPackage ./tui.nix {
      -        inherit hermesNpmLib;
      -      };
      -
      -      # Import bundled skills, excluding runtime caches
      -      bundledSkills = pkgs.lib.cleanSourceWith {
      -        src = ../skills;
      -        filter = path: _type: !(pkgs.lib.hasInfix "/index-cache/" path);
      -      };
      -
      -      hermesWeb = pkgs.callPackage ./web.nix {
      -        inherit hermesNpmLib;
      -      };
      -
      -      runtimeDeps = with pkgs; [
      -        nodejs_22
      -        ripgrep
      -        git
      -        openssh
      -        ffmpeg
      -        tirith
      -      ];
      -
      -      runtimePath = pkgs.lib.makeBinPath runtimeDeps;
      -
      -      # Lockfile hashes for dev shell stamps
      -      pyprojectHash = builtins.hashString "sha256" (builtins.readFile ../pyproject.toml);
      -      uvLockHash =
      -        if builtins.pathExists ../uv.lock then
      -          builtins.hashString "sha256" (builtins.readFile ../uv.lock)
      -        else
      -          "none";
           in
           {
             packages = {
      -        default = pkgs.stdenv.mkDerivation {
      -          pname = "hermes-agent";
      -          version = (fromTOML (builtins.readFile ../pyproject.toml)).project.version;
      -
      -          dontUnpack = true;
      -          dontBuild = true;
      -          nativeBuildInputs = [ pkgs.makeWrapper ];
      -
      -          installPhase = ''
      -            runHook preInstall
      -
      -            mkdir -p $out/share/hermes-agent $out/bin
      -            cp -r ${bundledSkills} $out/share/hermes-agent/skills
      -            cp -r ${hermesWeb} $out/share/hermes-agent/web_dist
      -
      -            # copy pre-built TUI (same layout as dev: ui-tui/dist/ + node_modules/)
      -            mkdir -p $out/ui-tui
      -            cp -r ${hermesTui}/lib/hermes-tui/* $out/ui-tui/
      -
      -            ${pkgs.lib.concatMapStringsSep "\n"
      -              (name: ''
      -                makeWrapper ${hermesVenv}/bin/${name} $out/bin/${name} \
      -                  --suffix PATH : "${runtimePath}" \
      -                  --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills \
      -                  --set HERMES_WEB_DIST $out/share/hermes-agent/web_dist \
      -                  --set HERMES_TUI_DIR $out/ui-tui \
      -                  --set HERMES_PYTHON ${hermesVenv}/bin/python3 \
      -                  --set HERMES_NODE ${pkgs.nodejs_22}/bin/node
      -              '')
      -              [
      -                "hermes"
      -                "hermes-agent"
      -                "hermes-acp"
      -              ]
      -            }
      -
      -            runHook postInstall
      -          '';
      -
      -          passthru.devShellHook = ''
      -            STAMP=".nix-stamps/hermes-agent"
      -            STAMP_VALUE="${pyprojectHash}:${uvLockHash}"
      -            if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$STAMP_VALUE" ]; then
      -              echo "hermes-agent: installing Python dependencies..."
      -              uv venv .venv --python ${pkgs.python312}/bin/python3 2>/dev/null || true
      -              source .venv/bin/activate
      -              uv pip install -e ".[all]"
      -              [ -d mini-swe-agent ] && uv pip install -e ./mini-swe-agent 2>/dev/null || true
      -              [ -d tinker-atropos ] && uv pip install -e ./tinker-atropos 2>/dev/null || true
      -              mkdir -p .nix-stamps
      -              echo "$STAMP_VALUE" > "$STAMP"
      -            else
      -              source .venv/bin/activate
      -              export HERMES_PYTHON=${hermesVenv}/bin/python3
      -            fi
      -          '';
      -
      -          meta = with pkgs.lib; {
      -            description = "AI agent with advanced tool-calling capabilities";
      -            homepage = "https://github.com/NousResearch/hermes-agent";
      -            mainProgram = "hermes";
      -            license = licenses.mit;
      -            platforms = platforms.unix;
      -          };
      -        };
      -
      -        tui = hermesTui;
      -        web = hermesWeb;
      +        default = hermesAgent;
      +        tui = hermesAgent.hermesTui;
      +        web = hermesAgent.hermesWeb;
       
      -        fix-lockfiles = hermesNpmLib.mkFixLockfiles {
      -          packages = [ hermesTui hermesWeb ];
      +        fix-lockfiles = hermesAgent.hermesNpmLib.mkFixLockfiles {
      +          packages = [ hermesAgent.hermesTui hermesAgent.hermesWeb ];
               };
             };
           };
    • nix/python.nix
      @@ -7,6 +7,7 @@
         pyproject-nix,
         pyproject-build-systems,
         stdenv,
      +  dependency-groups ? [ "all" ],
       }:
       let
         workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./..; };
      @@ -96,5 +97,5 @@ let
             ]);
       in
       pythonSet.mkVirtualEnv "hermes-agent-env" {
      -  hermes-agent = [ "all" ];
      +  hermes-agent = dependency-groups;
       }
    • website/docs/getting-started/nix-setup.md
      @@ -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 |
       
    • website/docs/guides/build-a-hermes-plugin.md
      @@ -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:**
    • website/docs/user-guide/features/plugins.md
      @@ -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

2 PRs closed without merging

From the close comments: done elsewhere 1 · other 1

  • #7229 feat(nix): add overlay and make package overridable · done elsewhere
  • #7488 refactor(matrix): migrate from matrix-nio to mautrix-python · other → #4594
Closed issues · 2
  • Closed#4594[Setup]: Matrix does not work on NixOS setup
  • Closed#14453feat(nix): declarative plugin installation for NixOS module
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.29: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
environments
Merged PRs
#7461, #15953
Closed issues
2
Attempts
2 PRs closed without merging
Fix link checked
27 Apr 2026

For your agent

The same facts, typed.