fix(#2422): support packaged Hermes Agent discovery

This commit is contained in:
Rod Boev authored and nesquena-hermes committed 2026-07-14 01:06:49 +00:00
1 parent 53348df68d
commit 9bd96fb75d
7 files changed
+272 -23

No files matched your search

+20 -17
View File
@@ -325,7 +325,7 @@ If an AI assistant is helping with install, reinstall, bootstrap, provider setup
| Thing | How it finds it | | Thing | How it finds it |
|---|---| |---|---|
| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `$HERMES_HOME/hermes-agent` (Windows default `%LOCALAPPDATA%\hermes\hermes-agent`, POSIX default `~/.hermes/hermes-agent`), then sibling `../hermes-agent` | | Hermes agent dir | `HERMES_WEBUI_AGENT_DIR`, then known checkout paths, the `hermes` launcher on `PATH`, and finally the installed `run_agent` module exposed by `HERMES_WEBUI_PYTHON` |
| Python executable | Agent venv first, then `.venv` in this repo, then system `python3` | | Python executable | Agent venv first, then `.venv` in this repo, then system `python3` |
| State directory | `HERMES_WEBUI_STATE_DIR` env, then `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | | State directory | `HERMES_WEBUI_STATE_DIR` env, then `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) |
| Default workspace | `HERMES_WEBUI_DEFAULT_WORKSPACE` env, then `~/workspace`, then state dir | | Default workspace | `HERMES_WEBUI_DEFAULT_WORKSPACE` env, then `~/workspace`, then state dir |
@@ -355,7 +355,7 @@ Full list of environment variables:
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `HERMES_WEBUI_AGENT_DIR` | auto-discovered | Path to the hermes-agent checkout | | `HERMES_WEBUI_AGENT_DIR` | auto-discovered | Path to the Hermes Agent source or installed module root |
| `HERMES_WEBUI_PYTHON` | auto-discovered | Python executable | | `HERMES_WEBUI_PYTHON` | auto-discovered | Python executable |
| `HERMES_WEBUI_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` for all IPv4, `::` for all IPv6, `::1` for IPv6 loopback) | | `HERMES_WEBUI_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` for all IPv4, `::` for all IPv6, `::1` for IPv6 loopback) |
| `HERMES_WEBUI_PORT` | `8787` | Port | | `HERMES_WEBUI_PORT` | `8787` | Port |
@@ -401,35 +401,38 @@ Then add the module and configure it in `nixosModules`:
{ {
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
hermes-agent.url = "github:NousResearch/hermes-agent";
hermes-webui.url = "github:nesquena/hermes-webui"; hermes-webui.url = "github:nesquena/hermes-webui";
}; };
outputs = { self, nixpkgs, hermes-webui, ... }: { outputs = { self, nixpkgs, hermes-agent, hermes-webui, ... }: {
nixosConfigurations.<host> = nixpkgs.lib.nixosSystem { nixosConfigurations.<host> = nixpkgs.lib.nixosSystem {
modules = [ modules = [
hermes-agent.nixosModules.default
hermes-webui.nixosModules.default hermes-webui.nixosModules.default
({ pkgs, ... }: {
services.hermes-agent.enable = true;
services.hermes-webui = {
enable = true;
host = "127.0.0.1";
port = 8787;
stateDir = "/var/lib/hermes-webui";
user = "hermes";
group = "hermes";
hermesHome = "/var/lib/hermes/.hermes";
agent.package = hermes-agent.packages.${pkgs.stdenv.hostPlatform.system}.default;
environmentFiles = [ "/run/secrets/hermes-webui.env" ];
};
})
]; ];
}; };
}; };
} }
``` ```
Service example:
```nix
services.hermes-webui = {
enable = true;
host = "127.0.0.1";
port = 8787;
stateDir = "/var/lib/hermes-webui";
agent.dir = "/var/lib/hermes/hermes-agent";
environmentFiles = [ "/run/secrets/hermes-webui.env" ];
};
```
The module defaults to `127.0.0.1`. Set `host = "0.0.0.0"` and `openFirewall = true` only when you want direct network access, and pair that with auth, for example `HERMES_WEBUI_PASSWORD` via `environmentFiles`. The module defaults to `127.0.0.1`. Set `host = "0.0.0.0"` and `openFirewall = true` only when you want direct network access, and pair that with auth, for example `HERMES_WEBUI_PASSWORD` via `environmentFiles`.
You can also set `agent.package` instead of `agent.dir` when you are using a compatible Hermes Agent package layout. This path depends on package metadata that the current published Hermes Agent flake may not expose yet, so explicit `agent.dir` and `agent.python` are the reliable integration options for existing agent packages. When the package exposes `passthru.hermesAgentDir`, the module derives `HERMES_WEBUI_AGENT_DIR` from that path. When it exposes `passthru.hermesVenv`, the module derives `HERMES_WEBUI_PYTHON` from the venv interpreter so bootstrap can use agent dependencies without creating a local `.venv`. The published Hermes Agent package exposes `passthru.hermesVenv`, so the module derives `HERMES_WEBUI_PYTHON` from its interpreter. Bootstrap then locates the installed `run_agent.py` through that interpreter without importing Agent code and exports its parent as `HERMES_WEBUI_AGENT_DIR`. Packages may expose `passthru.hermesAgentDir` as a direct path instead. Use `agent.dir` and `agent.python` as explicit overrides for custom package layouts.
When WebUI reads shared Hermes Agent state, run the service as a user that can already read that state. For a co-located Hermes Agent service, set `user` and `group` to the agent service account; the module only creates the default `hermes-webui` account and never changes ownership of an existing `hermesHome`. When WebUI reads shared Hermes Agent state, run the service as a user that can already read that state. For a co-located Hermes Agent service, set `user` and `group` to the agent service account; the module only creates the default `hermes-webui` account and never changes ownership of an existing `hermesHome`.
+29 -1
View File
@@ -182,6 +182,31 @@ def _agent_dir_from_hermes_cli() -> Path | None:
return None return None
def _agent_dir_from_python(python_exe: str) -> Path | None:
script = (
'import importlib.util\n'
'spec = importlib.util.find_spec("run_agent")\n'
'print(spec.origin if spec else "")\n'
)
try:
check = subprocess.run(
[python_exe, "-c", script],
capture_output=True,
text=True,
)
except OSError:
return None
if check.returncode != 0:
return None
lines = check.stdout.splitlines()
if not lines:
return None
origin = Path(lines[0].strip())
if not origin.is_absolute() or origin.name != "run_agent.py" or not origin.is_file():
return None
return origin.parent.resolve()
def discover_agent_dir() -> Path | None: def discover_agent_dir() -> Path | None:
home = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))).expanduser() home = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))).expanduser()
candidates = [ candidates = [
@@ -202,7 +227,10 @@ def discover_agent_dir() -> Path | None:
candidate = Path(raw).expanduser().resolve() candidate = Path(raw).expanduser().resolve()
if candidate.exists() and (candidate / "run_agent.py").exists(): if candidate.exists() and (candidate / "run_agent.py").exists():
return candidate return candidate
return _agent_dir_from_hermes_cli() agent_dir = _agent_dir_from_hermes_cli()
if agent_dir:
return agent_dir
return _agent_dir_from_python(discover_launcher_python(None))
def discover_launcher_python(agent_dir: Path | None) -> str: def discover_launcher_python(agent_dir: Path | None) -> str:
+26
View File
@@ -41,8 +41,32 @@
} }
]; ];
}; };
packageOnlyAgentVenv = pkgs.runCommand "hermes-agent-package-only-venv-${system}" { } ''
mkdir -p "$out/bin"
touch "$out/bin/python3"
'';
packageOnlyAgentPackage = pkgs.runCommand "hermes-agent-package-only-${system}" {
passthru.hermesVenv = packageOnlyAgentVenv;
} ''
touch "$out"
'';
packageOnlyModuleConfig = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
hermesModule
{
services.hermes-webui = {
enable = true;
package = package;
agent.package = packageOnlyAgentPackage;
};
}
];
};
moduleServiceEnvironment = nixpkgs.lib.concatStringsSep "\n" moduleConfig.config.systemd.services.hermes-webui.serviceConfig.Environment; moduleServiceEnvironment = nixpkgs.lib.concatStringsSep "\n" moduleConfig.config.systemd.services.hermes-webui.serviceConfig.Environment;
envProbe = pkgs.writeText "hermes-webui-nixos-env-${system}.txt" moduleServiceEnvironment; envProbe = pkgs.writeText "hermes-webui-nixos-env-${system}.txt" moduleServiceEnvironment;
packageOnlyServiceEnvironment = nixpkgs.lib.concatStringsSep "\n" packageOnlyModuleConfig.config.systemd.services.hermes-webui.serviceConfig.Environment;
packageOnlyEnvProbe = pkgs.writeText "hermes-webui-nixos-package-only-env-${system}.txt" packageOnlyServiceEnvironment;
in in
{ {
module-env-mapping = pkgs.runCommand "hermes-webui-nixos-module-${system}" { module-env-mapping = pkgs.runCommand "hermes-webui-nixos-module-${system}" {
@@ -52,6 +76,8 @@
grep -q 'HERMES_WEBUI_PORT=8787' ${envProbe} grep -q 'HERMES_WEBUI_PORT=8787' ${envProbe}
grep -q 'HERMES_WEBUI_STATE_DIR=/var/lib/hermes-webui' ${envProbe} grep -q 'HERMES_WEBUI_STATE_DIR=/var/lib/hermes-webui' ${envProbe}
grep -q 'HERMES_WEBUI_AGENT_DIR=/var/lib/hermes-agent' ${envProbe} grep -q 'HERMES_WEBUI_AGENT_DIR=/var/lib/hermes-agent' ${envProbe}
grep -q 'HERMES_WEBUI_PYTHON=${packageOnlyAgentVenv}/bin/python3' ${packageOnlyEnvProbe}
! grep -q 'HERMES_WEBUI_AGENT_DIR=' ${packageOnlyEnvProbe}
touch "$out" touch "$out"
''; '';
runtime-layout = pkgs.runCommand "hermes-webui-runtime-layout-${system}" { runtime-layout = pkgs.runCommand "hermes-webui-runtime-layout-${system}" {
+1 -1
View File
@@ -147,7 +147,7 @@ in
package = lib.mkOption { package = lib.mkOption {
type = lib.types.nullOr lib.types.package; type = lib.types.nullOr lib.types.package;
default = null; default = null;
description = "Package to derive HERMES_WEBUI_AGENT_DIR from passthru.hermesAgentDir and HERMES_WEBUI_PYTHON from passthru.hermesVenv."; description = "Package to derive HERMES_WEBUI_PYTHON from passthru.hermesVenv and optionally HERMES_WEBUI_AGENT_DIR from passthru.hermesAgentDir.";
}; };
dir = lib.mkOption { dir = lib.mkOption {
+121 -4
View File
@@ -1,16 +1,17 @@
"""Tests for `discover_agent_dir` shebang-based fallback. """Tests for `discover_agent_dir` launcher and interpreter fallbacks.
When the standard candidate paths (`~/.hermes/hermes-agent`, `~/hermes-agent`, When the standard candidate paths (`~/.hermes/hermes-agent`, `~/hermes-agent`,
`<webui-parent>/hermes-agent`, `HERMES_WEBUI_AGENT_DIR`) don't match, bootstrap `<webui-parent>/hermes-agent`, `HERMES_WEBUI_AGENT_DIR`) don't match, bootstrap
should fall back to introspecting the `hermes` console-script's shebang — bootstrap checks the `hermes` launcher, then asks the configured Python for the
that's a reliable pointer to the install root because the installer writes the installed `run_agent` module without importing it.
venv-relative interpreter path there.
""" """
from __future__ import annotations from __future__ import annotations
import textwrap import textwrap
import pytest
import bootstrap import bootstrap
@@ -75,6 +76,8 @@ def _isolate_discover_agent_dir(monkeypatch, tmp_path, hermes_path):
monkeypatch.setattr(bootstrap.shutil, "which", lambda name: str(hermes_path) if name == "hermes" else None) monkeypatch.setattr(bootstrap.shutil, "which", lambda name: str(hermes_path) if name == "hermes" else None)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "no-such-hermes-home")) monkeypatch.setenv("HERMES_HOME", str(tmp_path / "no-such-hermes-home"))
monkeypatch.delenv("HERMES_WEBUI_AGENT_DIR", raising=False) monkeypatch.delenv("HERMES_WEBUI_AGENT_DIR", raising=False)
monkeypatch.delenv("HERMES_WEBUI_PYTHON", raising=False)
monkeypatch.setattr(bootstrap, "_agent_dir_from_python", lambda _python: None)
# Force REPO_ROOT.parent to a dir that won't accidentally contain a # Force REPO_ROOT.parent to a dir that won't accidentally contain a
# `hermes-agent` sibling on the dev machine running these tests. # `hermes-agent` sibling on the dev machine running these tests.
monkeypatch.setattr(bootstrap, "REPO_ROOT", tmp_path / "isolated-repo-root") monkeypatch.setattr(bootstrap, "REPO_ROOT", tmp_path / "isolated-repo-root")
@@ -133,6 +136,11 @@ def test_explicit_candidate_takes_precedence_over_shebang(monkeypatch, tmp_path)
hermes = _make_hermes_cli(tmp_path, str(venv_python)) hermes = _make_hermes_cli(tmp_path, str(venv_python))
_isolate_discover_agent_dir(monkeypatch, tmp_path, hermes) _isolate_discover_agent_dir(monkeypatch, tmp_path, hermes)
monkeypatch.setenv("HERMES_WEBUI_AGENT_DIR", str(explicit_install)) monkeypatch.setenv("HERMES_WEBUI_AGENT_DIR", str(explicit_install))
monkeypatch.setattr(
bootstrap,
"_agent_dir_from_python",
lambda _python: (_ for _ in ()).throw(AssertionError("Python probe must not run")),
)
assert bootstrap.discover_agent_dir() == explicit_install.resolve() assert bootstrap.discover_agent_dir() == explicit_install.resolve()
@@ -189,3 +197,112 @@ def test_bash_wrapper_without_agent_target_returns_none(monkeypatch, tmp_path):
_isolate_discover_agent_dir(monkeypatch, tmp_path, hermes) _isolate_discover_agent_dir(monkeypatch, tmp_path, hermes)
assert bootstrap.discover_agent_dir() is None assert bootstrap.discover_agent_dir() is None
def test_discovers_installed_agent_dir_from_configured_python(monkeypatch, tmp_path):
agent_dir = tmp_path / "site-packages"
python_exe = str(tmp_path / "venv" / "python")
run_agent = agent_dir / "run_agent.py"
agent_dir.mkdir()
run_agent.write_text("raise AssertionError('must not import run_agent')", encoding="utf-8")
calls = []
def fake_run(argv, **kwargs):
calls.append((argv, kwargs))
return bootstrap.subprocess.CompletedProcess(argv, 0, stdout=f"{run_agent}\n", stderr="")
monkeypatch.setattr(bootstrap.subprocess, "run", fake_run)
assert bootstrap._agent_dir_from_python(python_exe) == agent_dir.resolve()
argv, kwargs = calls[0]
assert argv[:2] == [python_exe, "-c"]
assert 'find_spec("run_agent")' in argv[2]
assert "import run_agent" not in argv[2]
assert kwargs == {"capture_output": True, "text": True}
def test_python_probe_returns_none_when_run_agent_spec_is_missing(monkeypatch, tmp_path):
monkeypatch.setattr(
bootstrap.subprocess,
"run",
lambda argv, **kwargs: bootstrap.subprocess.CompletedProcess(argv, 0, stdout="\n", stderr=""),
)
assert bootstrap._agent_dir_from_python(str(tmp_path / "python")) is None
def test_python_probe_rejects_malformed_relative_origin(monkeypatch, tmp_path):
monkeypatch.setattr(
bootstrap.subprocess,
"run",
lambda argv, **kwargs: bootstrap.subprocess.CompletedProcess(argv, 0, stdout="run_agent.py\n", stderr=""),
)
assert bootstrap._agent_dir_from_python(str(tmp_path / "python")) is None
def test_python_probe_rejects_non_file_origin(monkeypatch, tmp_path):
missing = tmp_path / "site-packages" / "run_agent.py"
monkeypatch.setattr(
bootstrap.subprocess,
"run",
lambda argv, **kwargs: bootstrap.subprocess.CompletedProcess(argv, 0, stdout=f"{missing}\n", stderr=""),
)
assert bootstrap._agent_dir_from_python(str(tmp_path / "python")) is None
@pytest.mark.parametrize("error", [FileNotFoundError(), PermissionError()])
def test_python_probe_returns_none_when_interpreter_cannot_start(monkeypatch, tmp_path, error):
monkeypatch.setattr(bootstrap.subprocess, "run", lambda *args, **kwargs: (_ for _ in ()).throw(error))
assert bootstrap._agent_dir_from_python(str(tmp_path / "python")) is None
def test_python_probe_returns_none_on_nonzero_exit(monkeypatch, tmp_path):
monkeypatch.setattr(
bootstrap.subprocess,
"run",
lambda argv, **kwargs: bootstrap.subprocess.CompletedProcess(argv, 1, stdout="", stderr="probe failed"),
)
assert bootstrap._agent_dir_from_python(str(tmp_path / "python")) is None
def test_python_probe_accepts_valid_origin_with_benign_stderr(monkeypatch, tmp_path):
run_agent = tmp_path / "site-packages" / "run_agent.py"
run_agent.parent.mkdir()
run_agent.write_text("", encoding="utf-8")
monkeypatch.setattr(
bootstrap.subprocess,
"run",
lambda argv, **kwargs: bootstrap.subprocess.CompletedProcess(argv, 0, stdout=f"{run_agent}\n", stderr="warning\n"),
)
assert bootstrap._agent_dir_from_python(str(tmp_path / "python")) == run_agent.parent.resolve()
def test_python_probe_rejects_existing_file_with_wrong_name(monkeypatch, tmp_path):
origin = tmp_path / "site-packages" / "agent_entry.py"
origin.parent.mkdir()
origin.write_text("", encoding="utf-8")
monkeypatch.setattr(
bootstrap.subprocess,
"run",
lambda argv, **kwargs: bootstrap.subprocess.CompletedProcess(argv, 0, stdout=f"{origin}\n", stderr=""),
)
assert bootstrap._agent_dir_from_python(str(tmp_path / "python")) is None
def test_hermes_cli_takes_precedence_over_configured_python(monkeypatch, tmp_path):
install, venv_python = _make_agent_install(tmp_path)
hermes = _make_hermes_cli(tmp_path, str(venv_python))
_isolate_discover_agent_dir(monkeypatch, tmp_path, hermes)
monkeypatch.setattr(
bootstrap,
"_agent_dir_from_python",
lambda _python: (_ for _ in ()).throw(AssertionError("Python probe must not run")),
)
assert bootstrap.discover_agent_dir() == install.resolve()
+62
View File
@@ -92,6 +92,8 @@ def clean_env(monkeypatch):
"HERMES_WEBUI_HOST", "HERMES_WEBUI_HOST",
"HERMES_WEBUI_PORT", "HERMES_WEBUI_PORT",
"HERMES_WEBUI_AGENT_DIR", "HERMES_WEBUI_AGENT_DIR",
"HERMES_WEBUI_PYTHON",
"HERMES_WEBUI_DISABLE_LOCAL_VENV",
"HERMES_WEBUI_STATE_DIR", "HERMES_WEBUI_STATE_DIR",
"HERMES_WEBUI_SERVER_CWD", "HERMES_WEBUI_SERVER_CWD",
"HERMES_HOME", "HERMES_HOME",
@@ -531,3 +533,63 @@ class TestForegroundExecutabilityGuard:
bs.main() bs.main()
# execv must NOT have been called when the guard fires # execv must NOT have been called when the guard fires
assert len(execv_calls) == 0 assert len(execv_calls) == 0
def test_package_python_discovers_agent_before_skip_install_gate(import_bootstrap, clean_env, monkeypatch, tmp_path):
bs = import_bootstrap
agent_dir = tmp_path / "site-packages"
agent_dir.mkdir()
(agent_dir / "run_agent.py").write_text("class AIAgent:\n pass\n", encoding="utf-8")
state_dir = tmp_path / "state"
python_exe = sys.executable
execv_calls = []
monkeypatch.setenv("HERMES_WEBUI_PYTHON", python_exe)
monkeypatch.setenv("HERMES_WEBUI_DISABLE_LOCAL_VENV", "1")
monkeypatch.setenv("HERMES_WEBUI_STATE_DIR", str(state_dir))
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes-home"))
monkeypatch.setenv("PYTHONPATH", str(agent_dir))
monkeypatch.setenv("PATH", "")
monkeypatch.setattr(bs, "REPO_ROOT", tmp_path / "webui")
monkeypatch.setattr(bs.Path, "home", classmethod(lambda cls: tmp_path / "home"))
monkeypatch.setattr(bs, "ensure_supported_platform", lambda: None)
monkeypatch.setattr(sys, "argv", ["bootstrap.py", "--foreground", "--skip-agent-install"])
monkeypatch.setattr(sys, "platform", "linux")
monkeypatch.setattr(os, "access", lambda path, mode: True)
monkeypatch.setattr(os, "chdir", lambda path: None)
def fake_execv(path, argv):
execv_calls.append((path, argv))
raise SystemExit(0)
monkeypatch.setattr(os, "execv", fake_execv)
with patch.object(bs, "install_hermes_agent") as mock_install, patch.object(bs.venv, "EnvBuilder") as mock_builder, pytest.raises(SystemExit):
bs.main()
assert execv_calls == [(python_exe, [python_exe, str(bs.REPO_ROOT / "server.py")])]
assert os.environ["HERMES_WEBUI_AGENT_DIR"] == str(agent_dir.resolve())
mock_install.assert_not_called()
mock_builder.assert_not_called()
def test_package_python_without_agent_stays_fail_closed(import_bootstrap, clean_env, monkeypatch, tmp_path):
bs = import_bootstrap
python_exe = sys.executable
monkeypatch.setenv("HERMES_WEBUI_PYTHON", python_exe)
monkeypatch.setenv("HERMES_WEBUI_DISABLE_LOCAL_VENV", "1")
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes-home"))
monkeypatch.setenv("PATH", "")
monkeypatch.setattr(bs, "REPO_ROOT", tmp_path / "webui")
monkeypatch.setattr(bs.Path, "home", classmethod(lambda cls: tmp_path / "home"))
monkeypatch.setattr(bs, "ensure_supported_platform", lambda: None)
monkeypatch.setattr(sys, "argv", ["bootstrap.py", "--foreground", "--skip-agent-install"])
with patch.object(bs, "_agent_dir_from_python", return_value=None) as mock_probe, patch.object(bs, "install_hermes_agent") as mock_install, patch.object(bs.venv, "EnvBuilder") as mock_builder, patch.object(os, "execv") as mock_execv, pytest.raises(RuntimeError, match="Hermes Agent was not found"):
bs.main()
mock_probe.assert_called_once_with(python_exe)
mock_install.assert_not_called()
mock_builder.assert_not_called()
mock_execv.assert_not_called()
+13
View File
@@ -52,6 +52,19 @@ def test_nixos_module_uses_explicit_agent_dir_passthru_when_available():
assert legacy_agent_share_dir not in MODULE_NIX assert legacy_agent_share_dir not in MODULE_NIX
def test_flake_checks_package_with_only_hermes_venv_metadata():
assert "packageOnlyAgentPackage" in FLAKE_NIX
assert "passthru.hermesVenv = packageOnlyAgentVenv;" in FLAKE_NIX
assert "HERMES_WEBUI_PYTHON=${packageOnlyAgentVenv}/bin/python3" in FLAKE_NIX
assert "! grep -q 'HERMES_WEBUI_AGENT_DIR=' ${packageOnlyEnvProbe}" in FLAKE_NIX
def test_readme_wires_published_agent_flake_package():
assert 'hermes-agent.url = "github:NousResearch/hermes-agent";' in README
assert "agent.package = hermes-agent.packages.${pkgs.stdenv.hostPlatform.system}.default;" in README
assert 'hermesHome = "/var/lib/hermes/.hermes";' in README
def test_nixos_module_does_not_chown_existing_hermes_home(): def test_nixos_module_does_not_chown_existing_hermes_home():
assert "d ${cfg.hermesHome}" not in MODULE_NIX assert "d ${cfg.hermesHome}" not in MODULE_NIX