diff --git a/docs/nixos-setup.md b/docs/nixos-setup.md index 848e994ea585..e18e71ddbf4f 100644 --- a/docs/nixos-setup.md +++ b/docs/nixos-setup.md @@ -164,7 +164,7 @@ directory setup, config generation, secrets, documents, and service lifecycle. # ── MCP Servers ──────────────────────────────────────────────────── mcpServers.filesystem = { command = "npx"; - args = [ "-y" "@anthropic/mcp-filesystem" "/data/workspace" ]; + args = [ "-y" "@modelcontextprotocol/server-filesystem" "/data/workspace" ]; }; # ── Container options ────────────────────────────────────────────── diff --git a/nix/checks.nix b/nix/checks.nix index 993b96e7a5b6..abb2f10bf2d2 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -51,6 +51,26 @@ 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 + ''; + # Verify HERMES_MANAGED guard works on all mutation commands managed-guard = pkgs.runCommand "hermes-managed-guard" { } '' set -e diff --git a/nix/nixosModules.nix b/nix/nixosModules.nix index 4fba6bee74a9..47a52123e3ec 100644 --- a/nix/nixosModules.nix +++ b/nix/nixosModules.nix @@ -424,9 +424,14 @@ fi if [ "$NEED_CREATE" = "true" ]; then + # Resolve numeric UID/GID for container --user flag + HERMES_UID=$(${pkgs.coreutils}/bin/id -u ${cfg.user}) + HERMES_GID=$(${pkgs.coreutils}/bin/id -g ${cfg.user}) + echo "Creating container..." ${containerBin} create \ --name ${containerName} \ + --user "$HERMES_UID:$HERMES_GID" \ --network=host \ --volume /nix/store:/nix/store:ro \ --volume ${cfg.stateDir}:/data \ diff --git a/nix/packages.nix b/nix/packages.nix index aa2b4bdccd00..1957d7258c48 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -6,6 +6,13 @@ inherit (inputs) uv2nix pyproject-nix pyproject-build-systems; }; + # Import bundled skills, excluding runtime caches + bundledSkills = pkgs.lib.cleanSourceWith { + src = ../skills; + filter = path: _type: + !(pkgs.lib.hasInfix "/index-cache/" path); + }; + runtimeDeps = with pkgs; [ nodejs_20 ripgrep git openssh ffmpeg ]; @@ -23,16 +30,23 @@ installPhase = '' runHook preInstall + # Copy bundled skills into share directory + mkdir -p $out/share/hermes-agent + cp -r ${bundledSkills} $out/share/hermes-agent/skills + # Wrap entry points from the uv2nix venv mkdir -p $out/bin makeWrapper ${hermesVenv}/bin/hermes $out/bin/hermes \ - --prefix PATH : "${runtimePath}" + --prefix PATH : "${runtimePath}" \ + --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills makeWrapper ${hermesVenv}/bin/hermes-agent $out/bin/hermes-agent \ - --prefix PATH : "${runtimePath}" + --prefix PATH : "${runtimePath}" \ + --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills makeWrapper ${hermesVenv}/bin/hermes-acp $out/bin/hermes-acp \ - --prefix PATH : "${runtimePath}" + --prefix PATH : "${runtimePath}" \ + --set HERMES_BUNDLED_SKILLS $out/share/hermes-agent/skills runHook postInstall ''; diff --git a/tests/tools/test_skills_sync.py b/tests/tools/test_skills_sync.py index 1549d5170e61..e3469c805978 100644 --- a/tests/tools/test_skills_sync.py +++ b/tests/tools/test_skills_sync.py @@ -4,6 +4,7 @@ from pathlib import Path from unittest.mock import patch from tools.skills_sync import ( + _get_bundled_dir, _read_manifest, _write_manifest, _discover_bundled_skills, @@ -467,3 +468,24 @@ class TestSyncSkills: new_bundled_hash = _dir_hash(bundled / "old-skill") assert manifest["old-skill"] == new_bundled_hash assert manifest["old-skill"] != old_hash + + +class TestGetBundledDir: + def test_env_var_override(self, tmp_path, monkeypatch): + """HERMES_BUNDLED_SKILLS env var overrides the default path resolution.""" + custom_dir = tmp_path / "custom_skills" + custom_dir.mkdir() + monkeypatch.setenv("HERMES_BUNDLED_SKILLS", str(custom_dir)) + assert _get_bundled_dir() == custom_dir + + def test_default_without_env_var(self, monkeypatch): + """Without the env var, falls back to relative path from __file__.""" + monkeypatch.delenv("HERMES_BUNDLED_SKILLS", raising=False) + result = _get_bundled_dir() + assert result.name == "skills" + + def test_env_var_empty_string_ignored(self, monkeypatch): + """Empty HERMES_BUNDLED_SKILLS should fall back to default.""" + monkeypatch.setenv("HERMES_BUNDLED_SKILLS", "") + result = _get_bundled_dir() + assert result.name == "skills" diff --git a/tools/skills_sync.py b/tools/skills_sync.py index b89e45998f54..f76fcced9281 100644 --- a/tools/skills_sync.py +++ b/tools/skills_sync.py @@ -37,7 +37,14 @@ MANIFEST_FILE = SKILLS_DIR / ".bundled_manifest" def _get_bundled_dir() -> Path: - """Locate the bundled skills/ directory in the repo.""" + """Locate the bundled skills/ directory. + + Checks HERMES_BUNDLED_SKILLS env var first (set by Nix wrapper), + then falls back to the relative path from this source file. + """ + env_override = os.getenv("HERMES_BUNDLED_SKILLS") + if env_override: + return Path(env_override) return Path(__file__).parent.parent / "skills"