mirror of
https://github.com/0xrsydn/nix-hermes-agent.git
synced 2026-08-07 00:53:52 +00:00
Merge pull request #1 from 0xrsydn/feat/skills
This commit is contained in:
commit
ad607ee4dc
7 changed files with 446 additions and 7 deletions
86
README.md
86
README.md
|
|
@ -89,6 +89,20 @@ Everything is configured in Nix. Config, documents, secrets, service — one `ni
|
||||||
# "SOUL.md" = ./documents/SOUL.md;
|
# "SOUL.md" = ./documents/SOUL.md;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# ── Declarative skills (phase 1) ──
|
||||||
|
skills = {
|
||||||
|
bundled.enable = true;
|
||||||
|
optional = [
|
||||||
|
"creative/blender-mcp"
|
||||||
|
];
|
||||||
|
custom = {
|
||||||
|
repo-watch = {
|
||||||
|
category = "research";
|
||||||
|
source = ./skills/repo-watch;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# ── MCP servers ──
|
# ── MCP servers ──
|
||||||
mcpServers = {
|
mcpServers = {
|
||||||
context7 = {
|
context7 = {
|
||||||
|
|
@ -201,6 +215,77 @@ You (Telegram/Discord/WhatsApp/Slack) → Gateway → Tools → Machine does thi
|
||||||
└── gateway.log # Service log
|
└── gateway.log # Service log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Declarative Skills vs Native Hermes Skills
|
||||||
|
|
||||||
|
The `skills` option is designed to **augment Hermes**, not replace Hermes' native skill workflow.
|
||||||
|
|
||||||
|
Both approaches compose into the same runtime directory:
|
||||||
|
|
||||||
|
- `${stateDir}/.hermes/skills/`
|
||||||
|
|
||||||
|
That means you can use both:
|
||||||
|
|
||||||
|
- **declarative skills** from Nix
|
||||||
|
- **interactive/runtime skills** from `hermes skills install`
|
||||||
|
|
||||||
|
### Ownership model
|
||||||
|
|
||||||
|
#### Nix-managed
|
||||||
|
Skills declared via:
|
||||||
|
|
||||||
|
- `services.hermes-agent.skills.bundled`
|
||||||
|
- `services.hermes-agent.skills.optional`
|
||||||
|
- `services.hermes-agent.skills.custom`
|
||||||
|
|
||||||
|
are reconciled by the module and tracked in:
|
||||||
|
|
||||||
|
- `.nix-managed-skills.json`
|
||||||
|
|
||||||
|
These paths are considered **owned by Nix**.
|
||||||
|
|
||||||
|
#### Hermes-managed
|
||||||
|
Skills installed later via Hermes CLI, plus hub metadata under:
|
||||||
|
|
||||||
|
- `.hermes/skills/.hub/`
|
||||||
|
|
||||||
|
are left alone by the module **unless they collide with a Nix-managed path**.
|
||||||
|
|
||||||
|
### Collision rule
|
||||||
|
|
||||||
|
If a Hermes CLI install and a declarative Nix skill target the same installed path,
|
||||||
|
**the declarative Nix version wins on the next activation/rebuild**.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
- Nix declares `creative/blender-mcp`
|
||||||
|
- user later installs another `creative/blender-mcp` via Hermes CLI
|
||||||
|
|
||||||
|
On the next rebuild, the Nix-declared version is restored.
|
||||||
|
|
||||||
|
### Recommended workflow
|
||||||
|
|
||||||
|
Use **Hermes CLI** for:
|
||||||
|
|
||||||
|
- experimentation
|
||||||
|
- hub/community skill discovery
|
||||||
|
- temporary installs
|
||||||
|
- trying before keeping
|
||||||
|
|
||||||
|
Use **Nix declarative skills** for:
|
||||||
|
|
||||||
|
- stable/reproducible deployments
|
||||||
|
- bundled upstream skills you always want
|
||||||
|
- selected optional skills you want pinned to the package revision
|
||||||
|
- local custom house skills stored in git
|
||||||
|
|
||||||
|
A good pattern is:
|
||||||
|
|
||||||
|
1. install/try a skill interactively,
|
||||||
|
2. decide it is worth keeping,
|
||||||
|
3. promote it into Nix config if you want it reproducible.
|
||||||
|
|
||||||
|
This keeps `nix-hermes-agent` useful without interfering with the native Hermes experience.
|
||||||
|
|
||||||
## Module Options
|
## Module Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|
|
@ -209,6 +294,7 @@ You (Telegram/Discord/WhatsApp/Slack) → Gateway → Tools → Machine does thi
|
||||||
| `config` | attrset | `{}` | Declarative config (→ cli-config.yaml) |
|
| `config` | attrset | `{}` | Declarative config (→ cli-config.yaml) |
|
||||||
| `configFile` | path | `null` | Use existing config file (overrides `config`) |
|
| `configFile` | path | `null` | Use existing config file (overrides `config`) |
|
||||||
| `documents` | attrset | `{}` | Workspace files (string or path values) |
|
| `documents` | attrset | `{}` | Workspace files (string or path values) |
|
||||||
|
| `skills` | attrset | `{}` | Declarative Hermes skills (bundled, optional, custom local) |
|
||||||
| `environmentFiles` | list | `[]` | Secret env files (systemd EnvironmentFile) |
|
| `environmentFiles` | list | `[]` | Secret env files (systemd EnvironmentFile) |
|
||||||
| `environment` | attrset | `{}` | Non-secret env vars |
|
| `environment` | attrset | `{}` | Non-secret env vars |
|
||||||
| `authFile` | path | `null` | OAuth credentials file (auth.json) |
|
| `authFile` | path | `null` | OAuth credentials file (auth.json) |
|
||||||
|
|
|
||||||
17
checks.nix
17
checks.nix
|
|
@ -95,4 +95,21 @@
|
||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
echo "ok" > $out/result
|
echo "ok" > $out/result
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# Verify upstream source exposed to the module contains skills trees
|
||||||
|
skills-source-layout = pkgs.runCommand "hermes-skills-source-layout" { } ''
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SRC=${hermes-agent.upstreamSrc}
|
||||||
|
|
||||||
|
echo "=== Checking upstream skills trees ==="
|
||||||
|
test -d "$SRC/skills" || (echo "FAIL: missing skills/ in upstream source"; exit 1)
|
||||||
|
test -d "$SRC/optional-skills" || (echo "FAIL: missing optional-skills/ in upstream source"; exit 1)
|
||||||
|
find "$SRC/skills" -name SKILL.md -print -quit | grep -q . || (echo "FAIL: no bundled SKILL.md found"; exit 1)
|
||||||
|
find "$SRC/optional-skills" -name SKILL.md -print -quit | grep -q . || (echo "FAIL: no optional SKILL.md found"; exit 1)
|
||||||
|
echo "PASS: upstream skills source layout present"
|
||||||
|
|
||||||
|
mkdir -p $out
|
||||||
|
echo "ok" > $out/result
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
flake.nix
14
flake.nix
|
|
@ -23,10 +23,16 @@
|
||||||
default = self.packages.${system}.hermes-agent;
|
default = self.packages.${system}.hermes-agent;
|
||||||
};
|
};
|
||||||
|
|
||||||
checks = import ./checks.nix {
|
checks =
|
||||||
inherit pkgs;
|
(import ./checks.nix {
|
||||||
inherit (self.packages.${system}) hermes-agent;
|
inherit pkgs;
|
||||||
};
|
inherit (self.packages.${system}) hermes-agent;
|
||||||
|
})
|
||||||
|
// {
|
||||||
|
skills-coexistence = import ./tests/skills-coexistence.nix {
|
||||||
|
inherit self nixpkgs system;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = [ self.packages.${system}.hermes-agent ];
|
packages = [ self.packages.${system}.hermes-agent ];
|
||||||
|
|
|
||||||
248
module.nix
248
module.nix
|
|
@ -9,6 +9,7 @@ self:
|
||||||
let
|
let
|
||||||
cfg = config.services.hermes-agent;
|
cfg = config.services.hermes-agent;
|
||||||
inherit (self.packages.${pkgs.system}) hermes-agent;
|
inherit (self.packages.${pkgs.system}) hermes-agent;
|
||||||
|
hermesUpstreamSrc = cfg.package.upstreamSrc or hermes-agent.upstreamSrc;
|
||||||
|
|
||||||
# Deep-merge config type (same pattern as nix-openclaw)
|
# Deep-merge config type (same pattern as nix-openclaw)
|
||||||
deepConfigType = lib.types.mkOptionType {
|
deepConfigType = lib.types.mkOptionType {
|
||||||
|
|
@ -18,6 +19,136 @@ let
|
||||||
merge = _loc: defs: lib.foldl' lib.recursiveUpdate { } (map (d: d.value) defs);
|
merge = _loc: defs: lib.foldl' lib.recursiveUpdate { } (map (d: d.value) defs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
customSkillType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
category = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Category directory under skills/. If null, installs as a top-level skill.";
|
||||||
|
};
|
||||||
|
|
||||||
|
source = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Path to a local skill directory containing SKILL.md and any linked files.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
customSkillSpecs = lib.mapAttrsToList (name: value: {
|
||||||
|
inherit name;
|
||||||
|
inherit (value) category;
|
||||||
|
source = toString value.source;
|
||||||
|
}) cfg.skills.custom;
|
||||||
|
|
||||||
|
skillsManaged = cfg.skills.bundled.enable || cfg.skills.optional != [ ] || cfg.skills.custom != { };
|
||||||
|
|
||||||
|
managedSkillsTree =
|
||||||
|
pkgs.runCommand "hermes-managed-skills"
|
||||||
|
{
|
||||||
|
bundledEnabled = if cfg.skills.bundled.enable then "1" else "0";
|
||||||
|
optionalSkillsJson = builtins.toJSON cfg.skills.optional;
|
||||||
|
customSkillsJson = builtins.toJSON customSkillSpecs;
|
||||||
|
src = hermesUpstreamSrc;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mkdir -p "$out"
|
||||||
|
|
||||||
|
${pkgs.python3}/bin/python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
out = Path(os.environ["out"])
|
||||||
|
src = Path(os.environ["src"])
|
||||||
|
bundled_enabled = os.environ["bundledEnabled"] == "1"
|
||||||
|
optional_skills = json.loads(os.environ["optionalSkillsJson"])
|
||||||
|
custom_skills = json.loads(os.environ["customSkillsJson"])
|
||||||
|
managed = []
|
||||||
|
|
||||||
|
|
||||||
|
def copy_tree(src_dir: Path, dst_dir: Path):
|
||||||
|
if not src_dir.exists():
|
||||||
|
raise SystemExit(f"missing source path: {src_dir}")
|
||||||
|
if not src_dir.is_dir():
|
||||||
|
raise SystemExit(f"expected directory, got: {src_dir}")
|
||||||
|
if dst_dir.exists():
|
||||||
|
shutil.rmtree(dst_dir)
|
||||||
|
dst_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copytree(src_dir, dst_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def skill_dirs_under(root: Path):
|
||||||
|
if not root.exists():
|
||||||
|
return []
|
||||||
|
dirs = []
|
||||||
|
for skill_md in root.rglob("SKILL.md"):
|
||||||
|
rel = skill_md.parent.relative_to(root)
|
||||||
|
if rel.parts and rel.parts[0].startswith('.'):
|
||||||
|
continue
|
||||||
|
dirs.append(rel)
|
||||||
|
return sorted(set(dirs), key=lambda p: tuple(p.parts))
|
||||||
|
|
||||||
|
|
||||||
|
if bundled_enabled:
|
||||||
|
bundled_root = src / "skills"
|
||||||
|
if not bundled_root.exists():
|
||||||
|
raise SystemExit(f"bundled skills directory missing: {bundled_root}")
|
||||||
|
|
||||||
|
for skill_rel in skill_dirs_under(bundled_root):
|
||||||
|
copy_tree(bundled_root / skill_rel, out / skill_rel)
|
||||||
|
managed.append(str(skill_rel))
|
||||||
|
|
||||||
|
for desc in bundled_root.rglob("DESCRIPTION.md"):
|
||||||
|
rel = desc.relative_to(bundled_root)
|
||||||
|
target = out / rel
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(desc, target)
|
||||||
|
|
||||||
|
for rel_str in optional_skills:
|
||||||
|
rel = Path(rel_str)
|
||||||
|
if rel.is_absolute() or ".." in rel.parts or len(rel.parts) == 0:
|
||||||
|
raise SystemExit(f"invalid optional skill path: {rel_str}")
|
||||||
|
|
||||||
|
src_dir = src / "optional-skills" / rel
|
||||||
|
if not src_dir.exists():
|
||||||
|
raise SystemExit(f"optional skill path not found: {rel_str}")
|
||||||
|
if not (src_dir / "SKILL.md").exists():
|
||||||
|
raise SystemExit(f"optional skill missing SKILL.md: {rel_str}")
|
||||||
|
|
||||||
|
copy_tree(src_dir, out / rel)
|
||||||
|
managed.append(str(rel))
|
||||||
|
|
||||||
|
if len(rel.parts) > 1:
|
||||||
|
desc_src = src / "optional-skills" / rel.parts[0] / "DESCRIPTION.md"
|
||||||
|
if desc_src.exists():
|
||||||
|
desc_dst = out / rel.parts[0] / "DESCRIPTION.md"
|
||||||
|
desc_dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(desc_src, desc_dst)
|
||||||
|
|
||||||
|
for spec in custom_skills:
|
||||||
|
name = spec["name"]
|
||||||
|
category = spec.get("category")
|
||||||
|
src_dir = Path(spec["source"])
|
||||||
|
rel = Path(category) / name if category else Path(name)
|
||||||
|
|
||||||
|
if not src_dir.exists():
|
||||||
|
raise SystemExit(f"custom skill source not found: {src_dir}")
|
||||||
|
if not (src_dir / "SKILL.md").exists():
|
||||||
|
raise SystemExit(f"custom skill missing SKILL.md: {src_dir}")
|
||||||
|
|
||||||
|
copy_tree(src_dir, out / rel)
|
||||||
|
managed.append(str(rel))
|
||||||
|
|
||||||
|
(out / ".nix-managed-skills.json").write_text(
|
||||||
|
json.dumps(sorted(set(managed)), indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
'';
|
||||||
|
|
||||||
# Convert Nix attrset → YAML via JSON intermediary
|
# Convert Nix attrset → YAML via JSON intermediary
|
||||||
# (Hermes reads YAML but YAML is a superset of JSON, so JSON works)
|
# (Hermes reads YAML but YAML is a superset of JSON, so JSON works)
|
||||||
configJson = builtins.toJSON cfg.config;
|
configJson = builtins.toJSON cfg.config;
|
||||||
|
|
@ -136,6 +267,57 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# ── Skills (declarative materialization) ─────────────────────────────
|
||||||
|
skills = mkOption {
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Declarative Hermes skills materialized into `${cfg.stateDir}/.hermes/skills`.
|
||||||
|
Phase 1 supports bundled upstream skills, selected optional skills,
|
||||||
|
and custom local skills.
|
||||||
|
'';
|
||||||
|
type = types.submodule {
|
||||||
|
options = {
|
||||||
|
bundled.enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Install upstream bundled skills declaratively into HERMES_HOME/skills.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
optional = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [
|
||||||
|
"creative/storytelling"
|
||||||
|
"research/deep-research"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
Relative paths under upstream `optional-skills/` to install.
|
||||||
|
Example: `creative/some-skill`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
custom = mkOption {
|
||||||
|
type = types.attrsOf customSkillType;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Custom local skills keyed by installed skill name. Each entry points
|
||||||
|
to a directory containing `SKILL.md` and any linked files.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
repo-watch = {
|
||||||
|
category = "research";
|
||||||
|
source = ./skills/repo-watch;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# ── Secrets / environment ────────────────────────────────────────────
|
# ── Secrets / environment ────────────────────────────────────────────
|
||||||
environmentFiles = mkOption {
|
environmentFiles = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
|
|
@ -314,12 +496,22 @@ in
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
"d ${cfg.stateDir}/.hermes 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.stateDir}/.hermes 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.hermes/skills 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
"d ${cfg.workingDirectory} 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.workingDirectory} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
"d ${builtins.dirOf cfg.logPath} 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${builtins.dirOf cfg.logPath} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
# ── Activation: link config + documents into state dir ───────────────
|
# ── Activation: link config + documents + managed skills into state dir ──
|
||||||
system.activationScripts."hermes-agent-setup" = lib.stringAfter [ "users" ] ''
|
system.activationScripts."hermes-agent-setup" = lib.stringAfter [ "users" ] ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Ensure required directories exist during activation (do not rely on tmpfiles ordering)
|
||||||
|
install -d -o ${cfg.user} -g ${cfg.group} -m 0750 ${cfg.stateDir}
|
||||||
|
install -d -o ${cfg.user} -g ${cfg.group} -m 0750 ${cfg.stateDir}/.hermes
|
||||||
|
install -d -o ${cfg.user} -g ${cfg.group} -m 0750 ${cfg.stateDir}/.hermes/skills
|
||||||
|
install -d -o ${cfg.user} -g ${cfg.group} -m 0750 ${cfg.workingDirectory}
|
||||||
|
install -d -o ${cfg.user} -g ${cfg.group} -m 0750 ${builtins.dirOf cfg.logPath}
|
||||||
|
|
||||||
# Link config file
|
# Link config file
|
||||||
install -o ${cfg.user} -g ${cfg.group} -m 0640 -D ${configFile} ${cfg.stateDir}/.hermes/cli-config.yaml
|
install -o ${cfg.user} -g ${cfg.group} -m 0640 -D ${configFile} ${cfg.stateDir}/.hermes/cli-config.yaml
|
||||||
|
|
||||||
|
|
@ -345,6 +537,60 @@ in
|
||||||
install -o ${cfg.user} -g ${cfg.group} -m 0644 ${documentDerivation}/${name} ${cfg.workingDirectory}/${name}
|
install -o ${cfg.user} -g ${cfg.group} -m 0644 ${documentDerivation}/${name} ${cfg.workingDirectory}/${name}
|
||||||
'') cfg.documents
|
'') cfg.documents
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
# Reconcile Nix-managed skills without touching hub/runtime-managed state
|
||||||
|
${lib.optionalString skillsManaged ''
|
||||||
|
skills_dir=${cfg.stateDir}/.hermes/skills
|
||||||
|
managed_tree=${managedSkillsTree}
|
||||||
|
state_file="$skills_dir/.nix-managed-skills.json"
|
||||||
|
desired_file="$managed_tree/.nix-managed-skills.json"
|
||||||
|
|
||||||
|
install -d -o ${cfg.user} -g ${cfg.group} -m 0750 "$skills_dir"
|
||||||
|
|
||||||
|
skills_dir="$skills_dir" \
|
||||||
|
managed_tree="$managed_tree" \
|
||||||
|
state_file="$state_file" \
|
||||||
|
desired_file="$desired_file" \
|
||||||
|
${pkgs.python3}/bin/python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
skills_dir = Path(os.environ["skills_dir"])
|
||||||
|
managed_tree = Path(os.environ["managed_tree"])
|
||||||
|
state_file = Path(os.environ["state_file"])
|
||||||
|
desired_file = Path(os.environ["desired_file"])
|
||||||
|
|
||||||
|
old = []
|
||||||
|
if state_file.exists():
|
||||||
|
old = json.loads(state_file.read_text(encoding="utf-8"))
|
||||||
|
new = json.loads(desired_file.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
for rel in sorted(set(old) - set(new), reverse=True):
|
||||||
|
target = skills_dir / rel
|
||||||
|
if target.exists():
|
||||||
|
shutil.rmtree(target)
|
||||||
|
|
||||||
|
for rel in new:
|
||||||
|
src = managed_tree / rel
|
||||||
|
dst = skills_dir / rel
|
||||||
|
if dst.exists():
|
||||||
|
shutil.rmtree(dst)
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copytree(src, dst)
|
||||||
|
|
||||||
|
for desc in managed_tree.rglob("DESCRIPTION.md"):
|
||||||
|
rel = desc.relative_to(managed_tree)
|
||||||
|
dst = skills_dir / rel
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(desc, dst)
|
||||||
|
|
||||||
|
state_file.write_text(json.dumps(new, indent=2) + "\n", encoding="utf-8")
|
||||||
|
PY
|
||||||
|
|
||||||
|
chown -R ${cfg.user}:${cfg.group} "$skills_dir"
|
||||||
|
''}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# ── systemd service ──────────────────────────────────────────────────
|
# ── systemd service ──────────────────────────────────────────────────
|
||||||
|
|
|
||||||
16
package.nix
16
package.nix
|
|
@ -12,8 +12,16 @@
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
python = python312;
|
# Override python package set to fix broken upstream tests
|
||||||
pythonPackages = python312Packages;
|
python = python312.override {
|
||||||
|
packageOverrides = _final: prev: {
|
||||||
|
sanic = prev.sanic.overridePythonAttrs (_old: {
|
||||||
|
# sanic 25.12.0 has a flaky test_keep_alive_client_timeout in nixpkgs sandbox
|
||||||
|
doCheck = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
pythonPackages = python.pkgs;
|
||||||
|
|
||||||
# --- Missing PyPI packages ---
|
# --- Missing PyPI packages ---
|
||||||
|
|
||||||
|
|
@ -185,6 +193,10 @@ pythonPackages.buildPythonApplication {
|
||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
upstreamSrc = src;
|
||||||
|
};
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
description = "The self-improving AI agent by Nous Research";
|
description = "The self-improving AI agent by Nous Research";
|
||||||
homepage = "https://github.com/NousResearch/hermes-agent";
|
homepage = "https://github.com/NousResearch/hermes-agent";
|
||||||
|
|
|
||||||
8
tests/fixtures/custom-skill/SKILL.md
vendored
Normal file
8
tests/fixtures/custom-skill/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: repo-watch
|
||||||
|
description: Test custom skill for nix-hermes-agent module validation.
|
||||||
|
---
|
||||||
|
|
||||||
|
# repo-watch
|
||||||
|
|
||||||
|
Test skill body.
|
||||||
64
tests/skills-coexistence.nix
Normal file
64
tests/skills-coexistence.nix
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
system,
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
testSkill = builtins.path {
|
||||||
|
path = ./fixtures/custom-skill;
|
||||||
|
name = "hermes-test-custom-skill";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
pkgs.testers.runNixOSTest {
|
||||||
|
name = "hermes-skills-coexistence";
|
||||||
|
|
||||||
|
nodes.machine =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [ self.nixosModules.hermes-agent ];
|
||||||
|
|
||||||
|
services.hermes-agent = {
|
||||||
|
enable = true;
|
||||||
|
package = self.packages.${system}.hermes-agent;
|
||||||
|
skills = {
|
||||||
|
bundled.enable = false;
|
||||||
|
custom.repo-watch = {
|
||||||
|
category = "research";
|
||||||
|
source = testSkill;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
documents = {
|
||||||
|
"SOUL.md" = "# SOUL.md\nTest soul\n";
|
||||||
|
"AGENTS.md" = "# AGENTS.md\nTest agents\n";
|
||||||
|
"USER.md" = "# USER.md\nTest user\n";
|
||||||
|
};
|
||||||
|
config = {
|
||||||
|
toolsets = [ "all" ];
|
||||||
|
model = {
|
||||||
|
default = "moonshotai/kimi-k2.5";
|
||||||
|
provider = "openrouter";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "25.05";
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
machine.succeed("test -f /var/lib/hermes/.hermes/skills/research/repo-watch/SKILL.md")
|
||||||
|
machine.succeed("grep -F 'research/repo-watch' /var/lib/hermes/.hermes/skills/.nix-managed-skills.json")
|
||||||
|
|
||||||
|
machine.succeed("mkdir -p /var/lib/hermes/.hermes/skills/manual-test")
|
||||||
|
machine.succeed("cat > /var/lib/hermes/.hermes/skills/manual-test/SKILL.md <<'EOF'\n---\nname: manual-test\ndescription: unmanaged test skill\n---\n\n# manual-test\nEOF")
|
||||||
|
machine.succeed("chown -R hermes:hermes /var/lib/hermes/.hermes/skills/manual-test")
|
||||||
|
|
||||||
|
machine.succeed("/run/current-system/activate")
|
||||||
|
|
||||||
|
machine.succeed("test -f /var/lib/hermes/.hermes/skills/research/repo-watch/SKILL.md")
|
||||||
|
machine.succeed("test -f /var/lib/hermes/.hermes/skills/manual-test/SKILL.md")
|
||||||
|
'';
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue