feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
# nix/checks.nix — Build-time verification tests
|
|
|
|
|
#
|
|
|
|
|
# Checks are Linux-only: the full Python venv (via uv2nix) includes
|
|
|
|
|
# transitive deps like onnxruntime that lack compatible wheels on
|
|
|
|
|
# aarch64-darwin. The package and devShell still work on macOS.
|
|
|
|
|
{ inputs, ... }: {
|
|
|
|
|
perSystem = { pkgs, system, lib, ... }:
|
|
|
|
|
let
|
|
|
|
|
hermes-agent = inputs.self.packages.${system}.default;
|
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
2026-04-28 00:18:32 +05:30
|
|
|
hermesVenv = hermes-agent.hermesVenv;
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
|
|
|
|
configMergeScript = pkgs.callPackage ./configMergeScript.nix { };
|
2026-03-26 04:16:29 +05:30
|
|
|
|
|
|
|
|
# Auto-generated config key reference — always in sync with Python
|
|
|
|
|
configKeys = pkgs.runCommand "hermes-config-keys" {} ''
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
export HOME=$TMPDIR
|
|
|
|
|
${hermesVenv}/bin/python3 -c '
|
|
|
|
|
import json, sys
|
|
|
|
|
from hermes_cli.config import DEFAULT_CONFIG
|
|
|
|
|
|
|
|
|
|
def leaf_paths(d, prefix=""):
|
|
|
|
|
paths = []
|
|
|
|
|
for k, v in sorted(d.items()):
|
|
|
|
|
path = f"{prefix}.{k}" if prefix else k
|
|
|
|
|
if isinstance(v, dict) and v:
|
|
|
|
|
paths.extend(leaf_paths(v, path))
|
|
|
|
|
else:
|
|
|
|
|
paths.append(path)
|
|
|
|
|
return paths
|
|
|
|
|
|
|
|
|
|
json.dump(sorted(leaf_paths(DEFAULT_CONFIG)), sys.stdout, indent=2)
|
|
|
|
|
' > $out
|
|
|
|
|
'';
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
in {
|
2026-03-26 04:16:29 +05:30
|
|
|
packages.configKeys = configKeys;
|
|
|
|
|
|
2026-04-18 09:21:03 -07:00
|
|
|
checks = {
|
|
|
|
|
# Cross-platform evaluation — catches "not supported for interpreter"
|
|
|
|
|
# errors (e.g. sphinx dropping python311) without needing a darwin builder.
|
|
|
|
|
# Evaluation is pure and instant; it doesn't build anything.
|
|
|
|
|
cross-eval = let
|
|
|
|
|
targetSystems = builtins.filter
|
|
|
|
|
(s: inputs.self.packages ? ${s})
|
|
|
|
|
[ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
|
|
|
|
|
tryEvalPkg = sys:
|
|
|
|
|
let pkg = inputs.self.packages.${sys}.default;
|
|
|
|
|
in builtins.tryEval (builtins.seq pkg.drvPath true);
|
|
|
|
|
results = map (sys: { inherit sys; result = tryEvalPkg sys; }) targetSystems;
|
|
|
|
|
failures = builtins.filter (r: !r.result.success) results;
|
|
|
|
|
failMsg = lib.concatMapStringsSep "\n" (r: " - ${r.sys}") failures;
|
|
|
|
|
in pkgs.runCommand "hermes-cross-eval" { } (
|
|
|
|
|
if failures != [] then
|
|
|
|
|
builtins.throw "Package fails to evaluate on:\n${failMsg}"
|
|
|
|
|
else ''
|
|
|
|
|
echo "PASS: package evaluates on all ${toString (builtins.length targetSystems)} platforms"
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
|
|
|
|
''
|
|
|
|
|
);
|
|
|
|
|
} // lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
# Verify binaries exist and are executable
|
|
|
|
|
package-contents = pkgs.runCommand "hermes-package-contents" { } ''
|
|
|
|
|
set -e
|
|
|
|
|
echo "=== Checking binaries ==="
|
|
|
|
|
test -x ${hermes-agent}/bin/hermes || (echo "FAIL: hermes binary missing"; exit 1)
|
|
|
|
|
test -x ${hermes-agent}/bin/hermes-agent || (echo "FAIL: hermes-agent binary missing"; exit 1)
|
|
|
|
|
echo "PASS: All binaries present"
|
|
|
|
|
|
|
|
|
|
echo "=== Checking version ==="
|
|
|
|
|
${hermes-agent}/bin/hermes version 2>&1 | grep -qi "hermes" || (echo "FAIL: version check"; exit 1)
|
|
|
|
|
echo "PASS: Version check"
|
|
|
|
|
|
|
|
|
|
echo "=== All checks passed ==="
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Verify every pyproject.toml [project.scripts] entry has a wrapped binary
|
|
|
|
|
entry-points-sync = pkgs.runCommand "hermes-entry-points-sync" { } ''
|
|
|
|
|
set -e
|
|
|
|
|
echo "=== Checking entry points match pyproject.toml [project.scripts] ==="
|
|
|
|
|
for bin in hermes hermes-agent hermes-acp; do
|
|
|
|
|
test -x ${hermes-agent}/bin/$bin || (echo "FAIL: $bin binary missing from Nix package"; exit 1)
|
|
|
|
|
echo "PASS: $bin present"
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Verify CLI subcommands are accessible
|
|
|
|
|
cli-commands = pkgs.runCommand "hermes-cli-commands" { } ''
|
|
|
|
|
set -e
|
|
|
|
|
export HOME=$(mktemp -d)
|
|
|
|
|
|
|
|
|
|
echo "=== Checking hermes --help ==="
|
|
|
|
|
${hermes-agent}/bin/hermes --help 2>&1 | grep -q "gateway" || (echo "FAIL: gateway subcommand missing"; exit 1)
|
|
|
|
|
${hermes-agent}/bin/hermes --help 2>&1 | grep -q "config" || (echo "FAIL: config subcommand missing"; exit 1)
|
|
|
|
|
echo "PASS: All subcommands accessible"
|
|
|
|
|
|
|
|
|
|
echo "=== All CLI checks passed ==="
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Verify bundled skills are present in the package
|
|
|
|
|
bundled-skills = pkgs.runCommand "hermes-bundled-skills" { } ''
|
|
|
|
|
set -e
|
|
|
|
|
echo "=== Checking bundled skills ==="
|
|
|
|
|
test -d ${hermes-agent}/share/hermes-agent/skills || (echo "FAIL: skills directory missing"; exit 1)
|
|
|
|
|
echo "PASS: skills directory exists"
|
|
|
|
|
|
|
|
|
|
SKILL_COUNT=$(find ${hermes-agent}/share/hermes-agent/skills -name "SKILL.md" | wc -l)
|
|
|
|
|
test "$SKILL_COUNT" -gt 0 || (echo "FAIL: no SKILL.md files found in skills directory"; exit 1)
|
|
|
|
|
echo "PASS: $SKILL_COUNT bundled skills found"
|
|
|
|
|
|
|
|
|
|
grep -q "HERMES_BUNDLED_SKILLS" ${hermes-agent}/bin/hermes || \
|
|
|
|
|
(echo "FAIL: HERMES_BUNDLED_SKILLS not in wrapper"; exit 1)
|
|
|
|
|
echo "PASS: HERMES_BUNDLED_SKILLS set in wrapper"
|
|
|
|
|
|
|
|
|
|
echo "=== All bundled skills checks passed ==="
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
2026-04-09 15:30:29 -04:00
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Verify bundled TUI is present and compiled
|
|
|
|
|
bundled-tui = pkgs.runCommand "hermes-bundled-tui" { } ''
|
|
|
|
|
set -e
|
|
|
|
|
echo "=== Checking bundled TUI ==="
|
|
|
|
|
test -d ${hermes-agent}/ui-tui || (echo "FAIL: ui-tui directory missing"; exit 1)
|
|
|
|
|
echo "PASS: ui-tui directory exists"
|
|
|
|
|
|
|
|
|
|
test -f ${hermes-agent}/ui-tui/dist/entry.js || (echo "FAIL: compiled entry.js missing"; exit 1)
|
|
|
|
|
echo "PASS: compiled entry.js present"
|
|
|
|
|
|
|
|
|
|
test -d ${hermes-agent}/ui-tui/node_modules || (echo "FAIL: node_modules missing"; exit 1)
|
|
|
|
|
echo "PASS: node_modules present"
|
|
|
|
|
|
|
|
|
|
grep -q "HERMES_TUI_DIR" ${hermes-agent}/bin/hermes || \
|
|
|
|
|
(echo "FAIL: HERMES_TUI_DIR not in wrapper"; exit 1)
|
|
|
|
|
echo "PASS: HERMES_TUI_DIR set in wrapper"
|
|
|
|
|
|
|
|
|
|
echo "=== All bundled TUI checks passed ==="
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
2026-04-18 06:51:28 -07:00
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Verify HERMES_NODE is set in wrapper and points to Node 20+
|
|
|
|
|
# (string-width uses the /v regex flag which requires Node 20+)
|
|
|
|
|
hermes-node = pkgs.runCommand "hermes-node-version" { } ''
|
|
|
|
|
set -e
|
|
|
|
|
echo "=== Checking HERMES_NODE in wrapper ==="
|
|
|
|
|
grep -q "HERMES_NODE" ${hermes-agent}/bin/hermes || \
|
|
|
|
|
(echo "FAIL: HERMES_NODE not set in wrapper"; exit 1)
|
|
|
|
|
echo "PASS: HERMES_NODE present in wrapper"
|
|
|
|
|
|
|
|
|
|
HERMES_NODE=$(sed -n "s/^export HERMES_NODE='\(.*\)'/\1/p" ${hermes-agent}/bin/hermes)
|
|
|
|
|
test -x "$HERMES_NODE" || (echo "FAIL: HERMES_NODE=$HERMES_NODE not executable"; exit 1)
|
|
|
|
|
echo "PASS: HERMES_NODE executable at $HERMES_NODE"
|
|
|
|
|
|
|
|
|
|
NODE_MAJOR=$("$HERMES_NODE" --version | sed 's/^v//' | cut -d. -f1)
|
|
|
|
|
test "$NODE_MAJOR" -ge 20 || \
|
|
|
|
|
(echo "FAIL: Node v$NODE_MAJOR < 20, TUI needs /v regex flag support"; exit 1)
|
|
|
|
|
echo "PASS: Node v$NODE_MAJOR >= 20"
|
|
|
|
|
|
|
|
|
|
echo "=== All HERMES_NODE checks passed ==="
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Verify HERMES_MANAGED guard works on all mutation commands
|
|
|
|
|
managed-guard = pkgs.runCommand "hermes-managed-guard" { } ''
|
|
|
|
|
set -e
|
|
|
|
|
export HOME=$(mktemp -d)
|
|
|
|
|
|
|
|
|
|
check_blocked() {
|
|
|
|
|
local label="$1"
|
|
|
|
|
shift
|
|
|
|
|
OUTPUT=$(HERMES_MANAGED=true "$@" 2>&1 || true)
|
|
|
|
|
echo "$OUTPUT" | grep -q "managed by NixOS" || (echo "FAIL: $label not guarded"; echo "$OUTPUT"; exit 1)
|
|
|
|
|
echo "PASS: $label blocked in managed mode"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo "=== Checking HERMES_MANAGED guards ==="
|
|
|
|
|
check_blocked "config set" ${hermes-agent}/bin/hermes config set model foo
|
|
|
|
|
check_blocked "config edit" ${hermes-agent}/bin/hermes config edit
|
|
|
|
|
|
|
|
|
|
echo "=== All guard checks passed ==="
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
|
|
|
|
'';
|
|
|
|
|
|
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
2026-04-28 00:18:32 +05:30
|
|
|
# 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
|
|
|
|
|
'';
|
|
|
|
|
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
# ── Config merge + round-trip test ────────────────────────────────
|
|
|
|
|
# Tests the merge script (Nix activation behavior) across 7
|
|
|
|
|
# scenarios, then verifies Python's load_config() reads correctly.
|
|
|
|
|
config-roundtrip = let
|
|
|
|
|
# Nix settings used across scenarios
|
|
|
|
|
nixSettings = pkgs.writeText "nix-settings.json" (builtins.toJSON {
|
|
|
|
|
model = "test/nix-model";
|
|
|
|
|
toolsets = ["nix-toolset"];
|
|
|
|
|
terminal = { backend = "docker"; timeout = 999; };
|
|
|
|
|
mcp_servers = {
|
|
|
|
|
nix-server = { command = "echo"; args = ["nix"]; };
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
# Pre-built YAML fixtures for each scenario
|
|
|
|
|
fixtureB = pkgs.writeText "fixture-b.yaml" ''
|
|
|
|
|
model: "old-model"
|
|
|
|
|
mcp_servers:
|
|
|
|
|
old-server:
|
|
|
|
|
url: "http://old"
|
|
|
|
|
'';
|
|
|
|
|
fixtureC = pkgs.writeText "fixture-c.yaml" ''
|
|
|
|
|
skills:
|
|
|
|
|
disabled:
|
|
|
|
|
- skill-a
|
|
|
|
|
- skill-b
|
|
|
|
|
session_reset:
|
|
|
|
|
mode: idle
|
|
|
|
|
idle_minutes: 30
|
|
|
|
|
streaming:
|
|
|
|
|
enabled: true
|
|
|
|
|
fallback_model:
|
|
|
|
|
provider: openrouter
|
|
|
|
|
model: test-fallback
|
|
|
|
|
'';
|
|
|
|
|
fixtureD = pkgs.writeText "fixture-d.yaml" ''
|
|
|
|
|
model: "user-model"
|
|
|
|
|
skills:
|
|
|
|
|
disabled:
|
|
|
|
|
- skill-x
|
|
|
|
|
streaming:
|
|
|
|
|
enabled: true
|
|
|
|
|
transport: edit
|
|
|
|
|
'';
|
|
|
|
|
fixtureE = pkgs.writeText "fixture-e.yaml" ''
|
|
|
|
|
mcp_servers:
|
|
|
|
|
user-server:
|
|
|
|
|
url: "http://user-mcp"
|
|
|
|
|
nix-server:
|
|
|
|
|
command: "old-cmd"
|
|
|
|
|
args: ["old"]
|
|
|
|
|
'';
|
|
|
|
|
fixtureF = pkgs.writeText "fixture-f.yaml" ''
|
|
|
|
|
terminal:
|
|
|
|
|
cwd: "/user/path"
|
|
|
|
|
custom_key: "preserved"
|
|
|
|
|
env_passthrough:
|
|
|
|
|
- USER_VAR
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
in pkgs.runCommand "hermes-config-roundtrip" {
|
|
|
|
|
nativeBuildInputs = [ pkgs.jq ];
|
|
|
|
|
} ''
|
|
|
|
|
set -e
|
|
|
|
|
export HOME=$(mktemp -d)
|
|
|
|
|
ERRORS=""
|
|
|
|
|
|
|
|
|
|
fail() { ERRORS="$ERRORS\nFAIL: $1"; }
|
|
|
|
|
|
|
|
|
|
# Helper: run merge then load with Python, output merged JSON
|
|
|
|
|
merge_and_load() {
|
|
|
|
|
local hermes_home="$1"
|
|
|
|
|
export HERMES_HOME="$hermes_home"
|
|
|
|
|
${configMergeScript} ${nixSettings} "$hermes_home/config.yaml"
|
|
|
|
|
${hermesVenv}/bin/python3 -c '
|
|
|
|
|
import json, sys
|
|
|
|
|
from hermes_cli.config import load_config
|
|
|
|
|
json.dump(load_config(), sys.stdout, default=str)
|
|
|
|
|
'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Scenario A: Fresh install — no existing config.yaml
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
echo "=== Scenario A: Fresh install ==="
|
|
|
|
|
A_HOME=$(mktemp -d)
|
|
|
|
|
A_CONFIG=$(merge_and_load "$A_HOME")
|
|
|
|
|
|
|
|
|
|
echo "$A_CONFIG" | jq -e '.model == "test/nix-model"' > /dev/null \
|
|
|
|
|
|| fail "A: model not set from Nix"
|
|
|
|
|
echo "$A_CONFIG" | jq -e '.mcp_servers."nix-server".command == "echo"' > /dev/null \
|
|
|
|
|
|| fail "A: MCP nix-server missing"
|
|
|
|
|
echo "PASS: Scenario A"
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Scenario B: Nix keys override existing values
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
echo "=== Scenario B: Nix overrides ==="
|
|
|
|
|
B_HOME=$(mktemp -d)
|
|
|
|
|
install -m 0644 ${fixtureB} "$B_HOME/config.yaml"
|
|
|
|
|
B_CONFIG=$(merge_and_load "$B_HOME")
|
|
|
|
|
|
|
|
|
|
echo "$B_CONFIG" | jq -e '.model == "test/nix-model"' > /dev/null \
|
|
|
|
|
|| fail "B: Nix model did not override"
|
|
|
|
|
echo "PASS: Scenario B"
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Scenario C: User-only keys preserved
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
echo "=== Scenario C: User keys preserved ==="
|
|
|
|
|
C_HOME=$(mktemp -d)
|
|
|
|
|
install -m 0644 ${fixtureC} "$C_HOME/config.yaml"
|
|
|
|
|
C_CONFIG=$(merge_and_load "$C_HOME")
|
|
|
|
|
|
|
|
|
|
echo "$C_CONFIG" | jq -e '.skills.disabled == ["skill-a", "skill-b"]' > /dev/null \
|
|
|
|
|
|| fail "C: skills.disabled not preserved"
|
|
|
|
|
echo "$C_CONFIG" | jq -e '.session_reset.mode == "idle"' > /dev/null \
|
|
|
|
|
|| fail "C: session_reset.mode not preserved"
|
|
|
|
|
echo "$C_CONFIG" | jq -e '.session_reset.idle_minutes == 30' > /dev/null \
|
|
|
|
|
|| fail "C: session_reset.idle_minutes not preserved"
|
|
|
|
|
echo "$C_CONFIG" | jq -e '.streaming.enabled == true' > /dev/null \
|
|
|
|
|
|| fail "C: streaming.enabled not preserved"
|
|
|
|
|
echo "$C_CONFIG" | jq -e '.fallback_model.provider == "openrouter"' > /dev/null \
|
|
|
|
|
|| fail "C: fallback_model not preserved"
|
|
|
|
|
echo "PASS: Scenario C"
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Scenario D: Mixed — Nix wins for its keys, user keys preserved
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
echo "=== Scenario D: Mixed merge ==="
|
|
|
|
|
D_HOME=$(mktemp -d)
|
|
|
|
|
install -m 0644 ${fixtureD} "$D_HOME/config.yaml"
|
|
|
|
|
D_CONFIG=$(merge_and_load "$D_HOME")
|
|
|
|
|
|
|
|
|
|
echo "$D_CONFIG" | jq -e '.model == "test/nix-model"' > /dev/null \
|
|
|
|
|
|| fail "D: Nix model did not override user model"
|
|
|
|
|
echo "$D_CONFIG" | jq -e '.skills.disabled == ["skill-x"]' > /dev/null \
|
|
|
|
|
|| fail "D: user skills not preserved"
|
|
|
|
|
echo "$D_CONFIG" | jq -e '.streaming.enabled == true' > /dev/null \
|
|
|
|
|
|| fail "D: user streaming not preserved"
|
|
|
|
|
echo "PASS: Scenario D"
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Scenario E: MCP additive merge
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
echo "=== Scenario E: MCP additive merge ==="
|
|
|
|
|
E_HOME=$(mktemp -d)
|
|
|
|
|
install -m 0644 ${fixtureE} "$E_HOME/config.yaml"
|
|
|
|
|
E_CONFIG=$(merge_and_load "$E_HOME")
|
|
|
|
|
|
|
|
|
|
echo "$E_CONFIG" | jq -e '.mcp_servers."user-server".url == "http://user-mcp"' > /dev/null \
|
|
|
|
|
|| fail "E: user MCP server not preserved"
|
|
|
|
|
echo "$E_CONFIG" | jq -e '.mcp_servers."nix-server".command == "echo"' > /dev/null \
|
|
|
|
|
|| fail "E: Nix MCP server did not override same-name user server"
|
|
|
|
|
echo "$E_CONFIG" | jq -e '.mcp_servers."nix-server".args == ["nix"]' > /dev/null \
|
|
|
|
|
|| fail "E: Nix MCP server args wrong"
|
|
|
|
|
echo "PASS: Scenario E"
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Scenario F: Nested deep merge
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
echo "=== Scenario F: Nested deep merge ==="
|
|
|
|
|
F_HOME=$(mktemp -d)
|
|
|
|
|
install -m 0644 ${fixtureF} "$F_HOME/config.yaml"
|
|
|
|
|
F_CONFIG=$(merge_and_load "$F_HOME")
|
|
|
|
|
|
|
|
|
|
echo "$F_CONFIG" | jq -e '.terminal.backend == "docker"' > /dev/null \
|
|
|
|
|
|| fail "F: Nix terminal.backend did not override"
|
|
|
|
|
echo "$F_CONFIG" | jq -e '.terminal.timeout == 999' > /dev/null \
|
|
|
|
|
|| fail "F: Nix terminal.timeout did not override"
|
|
|
|
|
echo "$F_CONFIG" | jq -e '.terminal.custom_key == "preserved"' > /dev/null \
|
|
|
|
|
|| fail "F: terminal.custom_key not preserved"
|
|
|
|
|
echo "$F_CONFIG" | jq -e '.terminal.cwd == "/user/path"' > /dev/null \
|
|
|
|
|
|| fail "F: user terminal.cwd not preserved when Nix does not set it"
|
|
|
|
|
echo "$F_CONFIG" | jq -e '.terminal.env_passthrough == ["USER_VAR"]' > /dev/null \
|
|
|
|
|
|| fail "F: user terminal.env_passthrough not preserved"
|
|
|
|
|
echo "PASS: Scenario F"
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Scenario G: Idempotency — merging twice yields the same result
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
echo "=== Scenario G: Idempotency ==="
|
|
|
|
|
G_HOME=$(mktemp -d)
|
|
|
|
|
install -m 0644 ${fixtureD} "$G_HOME/config.yaml"
|
|
|
|
|
${configMergeScript} ${nixSettings} "$G_HOME/config.yaml"
|
|
|
|
|
FIRST=$(cat "$G_HOME/config.yaml")
|
|
|
|
|
${configMergeScript} ${nixSettings} "$G_HOME/config.yaml"
|
|
|
|
|
SECOND=$(cat "$G_HOME/config.yaml")
|
|
|
|
|
|
|
|
|
|
if [ "$FIRST" != "$SECOND" ]; then
|
|
|
|
|
fail "G: second merge produced different output"
|
|
|
|
|
echo "--- first ---"
|
|
|
|
|
echo "$FIRST"
|
|
|
|
|
echo "--- second ---"
|
|
|
|
|
echo "$SECOND"
|
|
|
|
|
fi
|
|
|
|
|
echo "PASS: Scenario G"
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
# Report
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
if [ -n "$ERRORS" ]; then
|
|
|
|
|
echo ""
|
|
|
|
|
echo "FAILURES:"
|
|
|
|
|
echo -e "$ERRORS"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=== All 7 merge scenarios passed ==="
|
|
|
|
|
mkdir -p $out
|
|
|
|
|
echo "ok" > $out/result
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|