hermes-agent/nix/packages.nix
ethernet f8b6d381e2
fix(nix): dirty-tree wrapper bug + filtered rebuild scope + overlay alias (#65237)
* fix(nix): fold makeWrapper line continuations into optionalStrings

When rev == null (any dirty-tree build), the empty optionalString
expansion left the previous line's trailing backslash dangling onto a
blank line, ending the makeWrapper command early and running
`--suffix PYTHONPATH ...` as its own shell command (`--suffix: command
not found`, exit 127). Clean trees passed CI; dirty trees with
extraPythonPackages failed — exactly the path the NixOS module
exercises.

The continuation now lives inside each optionalString (" \\\n  --set
..."), so the makeWrapper chain stays intact whether or not the
optional flags expand.

Verified by building with rev = null + extraPythonPackages =
[ pyfiglet ]: wrapper builds, PYTHONPATH suffix lands inside the
makeWrapper call, wrapped `hermes --version` runs, and the collision
check still executes (certifi correctly rejected).

* perf(nix): filter derivation sources to shrink rebuild scope

Every derivation previously saw the whole repo, so any file change
rebuilt everything. Each derivation now gets a filtered src with only
the files it consumes:

- lib.nix: derive npm workspace topology from the root package.json
  `workspaces` globs (single source of truth — a new workspace member
  is picked up with zero nix edits). pythonSrc (cleanSourceWith)
  excludes the JS workspace trees, docs/website, docker/.github,
  tests, nix/, flake.nix/flake.lock, root docs, and skills/ +
  optional-skills/. importNpmLock reads from a fileset-filtered
  npmRoot (root manifests + member package.jsons only).
- mkNpmPassthru takes `dirs` — the workspace dirs the package
  contains — and builds a per-package fileset src from them. web and
  desktop include apps/shared (file: dep). One shared
  `nix run .#update-npm-lockfile` replaces the per-package
  update_*_lockfile bins that only existed inside build sandboxes.
- python.nix: release venv loads the uv2nix workspace from pythonSrc.
  The editable venv keeps an unfiltered ./.. root —
  mkEditablePyprojectOverlay calls lib.path.splitRoot, which rejects
  a cleanSourceWith set, and the editable install reads the live
  checkout anyway.
- hermes-agent.nix: skills ship exclusively via HERMES_BUNDLED_SKILLS
  / HERMES_OPTIONAL_SKILLS (same mechanism as Homebrew packaging;
  setup.py's _data_file_tree returns [] for missing dirs), so
  SKILL.md edits no longer rebuild the venv. optional-mcps stays in
  the wheel — pyproject.toml lists its manifests as explicit
  data-files. Bundled assets are symlinked instead of copied, making
  the wrapper drv near-instant when only an input changed.
  __pycache__ filtered from bundled skills.
- checks.nix: find -L through the new symlinks; assert
  optional-skills presence + HERMES_OPTIONAL_SKILLS in the wrapper.
- run_tests.sh: fall back to $HERMES_PYTHON when no local venv
  exists, guarded by an `import pytest` probe (HERMES_PYTHON from a
  wrapped hermes binary points at the release venv, which has no
  pytest — without the guard every test file dies with "No module
  named pytest" while the runner exits 0).

Verified: nix flake check exit 0; built .#default .#tui .#web
.#desktop; SKILL.md and flake.nix edits leave the venv drvPath
unchanged; .py edits leave the tui drvPath unchanged; .tsx edits
leave the venv drvPath unchanged (and do change the tui drv);
scripts/run_tests.sh runs 299 tests green through both the venv and
HERMES_PYTHON paths, and rejects a pytest-less HERMES_PYTHON.

* refactor(nix): overlay aliases the flake's own package instead of re-instantiating

The overlay previously re-called callPackage against the consumer's
nixpkgs (final), so pkgs.hermes-agent could be a different derivation
than nix build .#default and the NixOS module's default — an untested
build matrix against arbitrary consumer nixpkgs versions, for a
package whose Python side is uv2nix-locked anyway.

Now the overlay is a pure alias for the flake's own locked package:
one callPackage site (packages.nix), everything else references it.
.override { ... } still works — callPackage's makeOverridable travels
with the derivation.

Verified: direct drvPath == overlaid drvPath; .override produces a
distinct drv.

* fix(nix): dedupe extraPlugins assertions, replace MESSAGING_CWD with terminal.cwd

- Delete the duplicated extraPlugins duplicate-name assertions block
  (same assertion declared twice back to back).
- Stop setting the deprecated MESSAGING_CWD env var, which made the
  module trip hermes' own startup deprecation warning. The working
  directory is now injected as terminal.cwd into the generated
  config.yaml; cfg.settings wins via recursiveUpdate, and container
  mode maps to the in-container mount path.
2026-07-15 21:45:35 -04:00

65 lines
1.7 KiB
Nix

# nix/packages.nix — Hermes Agent package built with uv2nix
{ inputs, ... }:
{
perSystem =
{
pkgs,
lib,
inputs',
...
}:
let
minimal = pkgs.callPackage ./hermes-agent.nix {
inherit (inputs) uv2nix pyproject-nix pyproject-build-systems;
npm-lockfile-fix = inputs'.npm-lockfile-fix.packages.default;
# Only embed clean revs — dirtyRev doesn't represent any upstream
# commit, so comparing it would always claim "update available".
rev = inputs.self.rev or null;
};
# All platform-portable optional integrations pre-built.
full = minimal.override {
extraDependencyGroups = [
"anthropic"
"azure-identity"
"bedrock"
"daytona"
"dingtalk"
"edge-tts"
"exa"
"fal"
"feishu"
"firecrawl"
"hindsight"
"honcho"
"messaging"
"modal"
"parallel-web"
"tts-premium"
"voice"
]
# matrix is Linux-only (oqs/liboqs lacks aarch64-darwin wheels).
++ lib.optionals pkgs.stdenv.isLinux [ "matrix" ];
};
in
{
packages = {
default = full;
inherit minimal;
# Ships discord.py + python-telegram-bot + slack-sdk so a plain
# `nix profile install .#messaging` connects to Discord/Telegram/Slack
# on first run — lazy-install can't write to the read-only /nix/store.
messaging = minimal.override {
extraDependencyGroups = [ "messaging" ];
};
tui = full.hermesTui;
web = full.hermesWeb;
desktop = full.hermesDesktop;
update-npm-lockfile = full.hermesNpmLib.updateNpmLockfile;
};
};
}