mirror of
https://github.com/0xrsydn/nix-hermes-agent.git
synced 2026-08-07 00:53:52 +00:00
feat: declarative NixOS module following nix-openclaw patterns
- Deep-merge config attrset → cli-config.yaml (like nix-openclaw's JSON config) - Declarative documents option (SOUL.md, AGENTS.md, USER.md — inline or file paths) - MCP servers as structured Nix options (merged into config) - Environment files for secrets (systemd EnvironmentFile, outside Nix store) - Non-secret environment vars (separate from secrets) - Proper tmpfiles rules, activation scripts for config + document linking - systemd hardening (NoNewPrivileges, ProtectSystem, PrivateTmp) - Comprehensive README with full usage examples
This commit is contained in:
parent
f70dc0f39b
commit
3dd03ba2ae
2 changed files with 437 additions and 92 deletions
210
README.md
210
README.md
|
|
@ -1,10 +1,12 @@
|
||||||
# nix-hermes
|
# nix-hermes
|
||||||
|
|
||||||
Nix package and NixOS module for [Hermes Agent](https://github.com/NousResearch/hermes-agent) by Nous Research.
|
Declarative Nix package and NixOS module for [Hermes Agent](https://github.com/NousResearch/hermes-agent) by Nous Research.
|
||||||
|
|
||||||
|
Everything is configured in Nix. Config, documents, secrets, service — one `nixos-rebuild switch` and it's live.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Flake usage
|
### 1. Add to your flake
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
|
|
@ -14,60 +16,196 @@ Nix package and NixOS module for [Hermes Agent](https://github.com/NousResearch/
|
||||||
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||||
modules = [
|
modules = [
|
||||||
nix-hermes.nixosModules.hermes-agent
|
nix-hermes.nixosModules.hermes-agent
|
||||||
{
|
./hermes.nix # your config (see below)
|
||||||
services.hermes-agent = {
|
|
||||||
enable = true;
|
|
||||||
environmentFile = "/run/secrets/hermes-env"; # API keys
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Just the package
|
### 2. Configure declaratively
|
||||||
|
|
||||||
```bash
|
```nix
|
||||||
nix run github:0xrsydn/nix-hermes -- --help
|
# hermes.nix
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
services.hermes-agent = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
# ── Declarative config (renders to cli-config.yaml) ──
|
||||||
|
config = {
|
||||||
|
model = {
|
||||||
|
default = "anthropic/claude-opus-4.6";
|
||||||
|
provider = "openrouter";
|
||||||
|
};
|
||||||
|
terminal = {
|
||||||
|
backend = "local";
|
||||||
|
timeout = 180;
|
||||||
|
lifetime_seconds = 300;
|
||||||
|
};
|
||||||
|
agent = {
|
||||||
|
max_turns = 60;
|
||||||
|
reasoning_effort = "medium";
|
||||||
|
};
|
||||||
|
memory = {
|
||||||
|
memory_enabled = true;
|
||||||
|
user_profile_enabled = true;
|
||||||
|
memory_char_limit = 2200;
|
||||||
|
nudge_interval = 10;
|
||||||
|
};
|
||||||
|
compression = {
|
||||||
|
enabled = true;
|
||||||
|
threshold = 0.85;
|
||||||
|
summary_model = "google/gemini-3-flash-preview";
|
||||||
|
};
|
||||||
|
toolsets = [ "all" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── Secrets (not in Nix store) ──
|
||||||
|
environmentFiles = [
|
||||||
|
"/run/secrets/hermes-env" # ANTHROPIC_API_KEY, TELEGRAM_TOKEN, etc.
|
||||||
|
];
|
||||||
|
|
||||||
|
# ── Non-secret env vars ──
|
||||||
|
environment = {
|
||||||
|
LLM_MODEL = "anthropic/claude-opus-4.6";
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── Workspace documents (inline or file paths) ──
|
||||||
|
documents = {
|
||||||
|
"SOUL.md" = ''
|
||||||
|
# SOUL.md
|
||||||
|
You are a sharp, pragmatic AI assistant.
|
||||||
|
'';
|
||||||
|
"AGENTS.md" = ''
|
||||||
|
# AGENTS.md
|
||||||
|
Read SOUL.md first. Then help the user.
|
||||||
|
'';
|
||||||
|
"USER.md" = ''
|
||||||
|
# USER.md
|
||||||
|
Name: Your Human
|
||||||
|
'';
|
||||||
|
# Or reference a file:
|
||||||
|
# "SOUL.md" = ./documents/SOUL.md;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── MCP servers ──
|
||||||
|
mcpServers = {
|
||||||
|
context7 = {
|
||||||
|
command = "npx";
|
||||||
|
args = [ "-y" "@upstash/context7-mcp@latest" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── Extra tools on PATH ──
|
||||||
|
extraPackages = with pkgs; [ jq ripgrep curl ];
|
||||||
|
};
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### NixOS module options
|
### 3. Create secrets file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /run/secrets/hermes-env (or wherever you manage secrets)
|
||||||
|
OPENROUTER_API_KEY=sk-or-...
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
TELEGRAM_TOKEN=123456:ABC...
|
||||||
|
TELEGRAM_ALLOWED_USERS=your_user_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nixos-rebuild switch
|
||||||
|
systemctl status hermes-agent
|
||||||
|
journalctl -u hermes-agent -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
You (Telegram/Discord/WhatsApp/Slack) → Gateway → Tools → Machine does things
|
||||||
|
```
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. `services.hermes-agent.config` attrset is deep-merged and rendered to `cli-config.yaml`
|
||||||
|
2. Documents are installed into the workspace directory
|
||||||
|
3. Secrets stay outside the Nix store via `environmentFiles`
|
||||||
|
4. systemd service runs `hermes gateway` with everything wired up
|
||||||
|
|
||||||
|
### Directory layout
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/lib/hermes/ # stateDir
|
||||||
|
├── .hermes/ # Hermes home (HERMES_HOME)
|
||||||
|
│ ├── cli-config.yaml # Generated from config option
|
||||||
|
│ ├── .env # Secrets (from environmentFiles)
|
||||||
|
│ ├── memory/ # Agent memory (runtime)
|
||||||
|
│ ├── skills/ # Skills (runtime)
|
||||||
|
│ └── logs/ # Session logs
|
||||||
|
├── workspace/ # workingDirectory
|
||||||
|
│ ├── SOUL.md # From documents option
|
||||||
|
│ ├── AGENTS.md
|
||||||
|
│ └── USER.md
|
||||||
|
└── logs/
|
||||||
|
└── gateway.log # Service log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|--------|------|---------|-------------|
|
|--------|------|---------|-------------|
|
||||||
| `enable` | bool | `false` | Enable the Hermes Agent gateway service |
|
| `enable` | bool | `false` | Enable Hermes Agent gateway |
|
||||||
|
| `config` | attrset | `{}` | Declarative config (→ cli-config.yaml) |
|
||||||
|
| `configFile` | path | `null` | Use existing config file (overrides `config`) |
|
||||||
|
| `documents` | attrset | `{}` | Workspace files (string or path values) |
|
||||||
|
| `environmentFiles` | list | `[]` | Secret env files (systemd EnvironmentFile) |
|
||||||
|
| `environment` | attrset | `{}` | Non-secret env vars |
|
||||||
|
| `mcpServers` | attrset | `{}` | MCP server configs (merged into config) |
|
||||||
| `user` | string | `"hermes"` | Service user |
|
| `user` | string | `"hermes"` | Service user |
|
||||||
| `group` | string | `"hermes"` | Service group |
|
| `group` | string | `"hermes"` | Service group |
|
||||||
| `homeDir` | path | `/var/lib/hermes` | State directory |
|
| `stateDir` | path | `/var/lib/hermes` | State directory |
|
||||||
| `workDir` | path | `${homeDir}/workspace` | Working directory |
|
| `workingDirectory` | path | `${stateDir}/workspace` | Working directory |
|
||||||
| `environmentFile` | path | `null` | Secrets file (API keys, tokens) |
|
|
||||||
| `extraEnvironment` | attrs | `{}` | Extra env vars |
|
|
||||||
| `extraArgs` | list | `[]` | Extra CLI args for `hermes gateway` |
|
|
||||||
| `extraPackages` | list | `[]` | Extra packages on PATH |
|
| `extraPackages` | list | `[]` | Extra packages on PATH |
|
||||||
|
| `extraArgs` | list | `[]` | Extra `hermes gateway` args |
|
||||||
|
| `logPath` | path | `${stateDir}/logs/gateway.log` | Log file |
|
||||||
|
| `restart` | string | `"always"` | systemd Restart policy |
|
||||||
|
| `restartSec` | int | `5` | Restart delay |
|
||||||
|
|
||||||
### Environment file example
|
## Config Reference
|
||||||
|
|
||||||
|
The `config` attrset maps directly to Hermes' `cli-config.yaml`. Key sections:
|
||||||
|
|
||||||
|
| Section | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `model` | Default model, provider, base_url |
|
||||||
|
| `terminal` | Backend (local/ssh/docker/modal), cwd, timeout |
|
||||||
|
| `agent` | max_turns, verbose, reasoning_effort, personalities |
|
||||||
|
| `memory` | memory_enabled, user_profile_enabled, char limits |
|
||||||
|
| `compression` | Context compression settings |
|
||||||
|
| `session_reset` | Auto-reset policy for messaging |
|
||||||
|
| `skills` | Skill creation nudge settings |
|
||||||
|
| `toolsets` | Which tool groups to enable |
|
||||||
|
| `mcp_servers` | MCP server connections |
|
||||||
|
| `delegation` | Subagent settings |
|
||||||
|
| `browser` | Browser tool settings |
|
||||||
|
| `stt` | Voice transcription config |
|
||||||
|
| `display` | UI/skin settings |
|
||||||
|
|
||||||
|
See the [full config reference](https://raw.githubusercontent.com/NousResearch/hermes-agent/main/cli-config.yaml.example).
|
||||||
|
|
||||||
|
## Just the Package
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ANTHROPIC_API_KEY=sk-ant-...
|
# Run directly
|
||||||
OPENROUTER_API_KEY=sk-or-...
|
nix run github:0xrsydn/nix-hermes -- --help
|
||||||
TELEGRAM_TOKEN=123456:ABC...
|
|
||||||
OPENAI_API_KEY=sk-...
|
|
||||||
```
|
|
||||||
|
|
||||||
## What's included
|
# In a dev shell
|
||||||
|
nix develop github:0xrsydn/nix-hermes
|
||||||
|
|
||||||
- **`hermes`** — Interactive CLI
|
# Use the overlay
|
||||||
- **`hermes-agent`** — Agent runner
|
nixpkgs.overlays = [ nix-hermes.overlays.default ];
|
||||||
- **`hermes-acp`** — ACP adapter
|
|
||||||
- **NixOS module** — systemd service for gateway mode
|
|
||||||
- Runtime deps wrapped: Node.js 22, ripgrep, ffmpeg, git
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix develop # Enter dev shell with hermes on PATH
|
|
||||||
nix build # Build the package
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
||||||
327
module.nix
327
module.nix
|
|
@ -4,122 +4,329 @@ self:
|
||||||
let
|
let
|
||||||
cfg = config.services.hermes-agent;
|
cfg = config.services.hermes-agent;
|
||||||
hermes-agent = self.packages.${pkgs.system}.hermes-agent;
|
hermes-agent = self.packages.${pkgs.system}.hermes-agent;
|
||||||
|
|
||||||
|
# Deep-merge config type (same pattern as nix-openclaw)
|
||||||
|
deepConfigType = lib.types.mkOptionType {
|
||||||
|
name = "hermes-config-attrs";
|
||||||
|
description = "Hermes YAML config (attrset), merged deeply via lib.recursiveUpdate.";
|
||||||
|
check = builtins.isAttrs;
|
||||||
|
merge = _loc: defs: lib.foldl' lib.recursiveUpdate { } (map (d: d.value) defs);
|
||||||
|
};
|
||||||
|
|
||||||
|
# Convert Nix attrset → YAML via JSON intermediary
|
||||||
|
# (Hermes reads YAML but YAML is a superset of JSON, so JSON works)
|
||||||
|
configJson = builtins.toJSON cfg.config;
|
||||||
|
generatedConfigFile = pkgs.writeText "cli-config.yaml" configJson;
|
||||||
|
configFile = if cfg.configFile != null then cfg.configFile else generatedConfigFile;
|
||||||
|
|
||||||
|
# Generate .env file from environment attrset (non-secret env vars)
|
||||||
|
envFileContent = lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (k: v: "${k}=${v}") cfg.environment
|
||||||
|
);
|
||||||
|
generatedEnvFile = pkgs.writeText "hermes-env" envFileContent;
|
||||||
|
|
||||||
|
# Document files → symlinked into workspace
|
||||||
|
documentDerivation = pkgs.runCommand "hermes-documents" { } (
|
||||||
|
''
|
||||||
|
mkdir -p $out
|
||||||
|
'' + lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (name: value:
|
||||||
|
if builtins.isPath value || lib.isStorePath value
|
||||||
|
then "cp ${value} $out/${name}"
|
||||||
|
else "cat > $out/${name} <<'HERMES_DOC_EOF'\n${value}\nHERMES_DOC_EOF"
|
||||||
|
) cfg.documents
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.hermes-agent = {
|
options.services.hermes-agent = with lib; {
|
||||||
enable = lib.mkEnableOption "Hermes Agent gateway service";
|
enable = mkEnableOption "Hermes Agent gateway service";
|
||||||
|
|
||||||
package = lib.mkOption {
|
# ── Package ──────────────────────────────────────────────────────────
|
||||||
type = lib.types.package;
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
default = hermes-agent;
|
default = hermes-agent;
|
||||||
description = "The hermes-agent package to use.";
|
description = "The hermes-agent package to use.";
|
||||||
};
|
};
|
||||||
|
|
||||||
user = lib.mkOption {
|
# ── Service identity ─────────────────────────────────────────────────
|
||||||
type = lib.types.str;
|
unitName = mkOption {
|
||||||
default = "hermes";
|
type = types.str;
|
||||||
description = "User under which Hermes Agent runs.";
|
default = "hermes-agent";
|
||||||
|
description = "systemd unit name (<unitName>.service).";
|
||||||
};
|
};
|
||||||
|
|
||||||
group = lib.mkOption {
|
user = mkOption {
|
||||||
type = lib.types.str;
|
type = types.str;
|
||||||
default = "hermes";
|
default = "hermes";
|
||||||
description = "Group under which Hermes Agent runs.";
|
description = "System user running the gateway.";
|
||||||
};
|
};
|
||||||
|
|
||||||
homeDir = lib.mkOption {
|
group = mkOption {
|
||||||
type = lib.types.path;
|
type = types.str;
|
||||||
|
default = "hermes";
|
||||||
|
description = "System group running the gateway.";
|
||||||
|
};
|
||||||
|
|
||||||
|
createUser = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Create the user/group automatically.";
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── Directories ──────────────────────────────────────────────────────
|
||||||
|
stateDir = mkOption {
|
||||||
|
type = types.str;
|
||||||
default = "/var/lib/hermes";
|
default = "/var/lib/hermes";
|
||||||
description = "Home directory for Hermes Agent state.";
|
description = "State directory (HERMES_HOME base). Contains .hermes/ subdir.";
|
||||||
};
|
};
|
||||||
|
|
||||||
workDir = lib.mkOption {
|
workingDirectory = mkOption {
|
||||||
type = lib.types.path;
|
type = types.str;
|
||||||
default = "${cfg.homeDir}/workspace";
|
default = "${cfg.stateDir}/workspace";
|
||||||
defaultText = lib.literalExpression ''"''${cfg.homeDir}/workspace"'';
|
defaultText = literalExpression ''"''${cfg.stateDir}/workspace"'';
|
||||||
description = "Working directory for Hermes Agent.";
|
description = "Working directory for the agent (MESSAGING_CWD).";
|
||||||
};
|
};
|
||||||
|
|
||||||
environmentFile = lib.mkOption {
|
# ── Config (declarative YAML) ────────────────────────────────────────
|
||||||
type = lib.types.nullOr lib.types.path;
|
configFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Path to an environment file containing secrets (API keys, tokens).
|
Path to an existing cli-config.yaml. If set, takes precedence over
|
||||||
This file should contain lines like:
|
the declarative `config` option.
|
||||||
ANTHROPIC_API_KEY=sk-...
|
|
||||||
TELEGRAM_TOKEN=...
|
|
||||||
OPENROUTER_API_KEY=...
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
extraEnvironment = lib.mkOption {
|
config = mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = deepConfigType;
|
||||||
default = { };
|
default = { };
|
||||||
description = "Extra environment variables for the Hermes Agent service.";
|
description = ''
|
||||||
example = lib.literalExpression ''
|
Declarative Hermes config (attrset). Deep-merged across module definitions
|
||||||
|
and rendered as cli-config.yaml. See upstream cli-config.yaml.example for
|
||||||
|
all available keys.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
{
|
{
|
||||||
HERMES_DEFAULT_MODEL = "anthropic/claude-sonnet-4-20250514";
|
model = {
|
||||||
|
default = "anthropic/claude-sonnet-4-20250514";
|
||||||
|
provider = "openrouter";
|
||||||
|
};
|
||||||
|
terminal = {
|
||||||
|
backend = "local";
|
||||||
|
timeout = 180;
|
||||||
|
};
|
||||||
|
agent = {
|
||||||
|
max_turns = 60;
|
||||||
|
reasoning_effort = "medium";
|
||||||
|
};
|
||||||
|
memory = {
|
||||||
|
memory_enabled = true;
|
||||||
|
user_profile_enabled = true;
|
||||||
|
};
|
||||||
|
toolsets = [ "all" ];
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
extraArgs = lib.mkOption {
|
# ── Secrets / environment ────────────────────────────────────────────
|
||||||
type = lib.types.listOf lib.types.str;
|
environmentFiles = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "Extra command-line arguments to pass to hermes gateway.";
|
description = ''
|
||||||
|
Paths to environment files containing secrets (API keys, tokens).
|
||||||
|
These are passed as systemd EnvironmentFile= entries.
|
||||||
|
Use leading '-' to ignore missing files.
|
||||||
|
|
||||||
|
Example file contents:
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
OPENROUTER_API_KEY=sk-or-...
|
||||||
|
TELEGRAM_TOKEN=123456:ABC...
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
extraPackages = lib.mkOption {
|
environment = mkOption {
|
||||||
type = lib.types.listOf lib.types.package;
|
type = types.attrsOf types.str;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Non-secret environment variables for Hermes. These are written to
|
||||||
|
an env file (visible in the Nix store — do NOT put secrets here).
|
||||||
|
Use `environmentFiles` for secrets.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
LLM_MODEL = "anthropic/claude-opus-4.6";
|
||||||
|
MESSAGING_CWD = "/var/lib/hermes/workspace";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── Documents (SOUL.md, AGENTS.md, etc.) ─────────────────────────────
|
||||||
|
documents = mkOption {
|
||||||
|
type = types.attrsOf (types.either types.str types.path);
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Workspace document files. Keys are filenames, values are either
|
||||||
|
inline strings or paths to files. These are symlinked into the
|
||||||
|
workspace directory on activation.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
"SOUL.md" = '''
|
||||||
|
# SOUL.md
|
||||||
|
You are a helpful AI assistant.
|
||||||
|
''';
|
||||||
|
"AGENTS.md" = ./my-agents.md;
|
||||||
|
"USER.md" = '''
|
||||||
|
# USER.md
|
||||||
|
Name: Fay
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── Service behavior ─────────────────────────────────────────────────
|
||||||
|
logPath = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${cfg.stateDir}/logs/gateway.log";
|
||||||
|
defaultText = literalExpression ''"''${cfg.stateDir}/logs/gateway.log"'';
|
||||||
|
description = "Log file path.";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraArgs = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Extra command-line arguments for `hermes gateway`.";
|
||||||
|
};
|
||||||
|
|
||||||
|
execStart = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Override ExecStart command. If unset, runs: hermes gateway.";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraPackages = mkOption {
|
||||||
|
type = types.listOf types.package;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "Extra packages to make available on PATH.";
|
description = "Extra packages to make available on PATH.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
restart = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "always";
|
||||||
|
description = "systemd Restart= policy.";
|
||||||
|
};
|
||||||
|
|
||||||
|
restartSec = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 5;
|
||||||
|
description = "systemd RestartSec= value.";
|
||||||
|
};
|
||||||
|
|
||||||
|
# ── MCP servers ──────────────────────────────────────────────────────
|
||||||
|
mcpServers = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
command = mkOption { type = types.str; description = "MCP server command."; };
|
||||||
|
args = mkOption { type = types.listOf types.str; default = [ ]; };
|
||||||
|
env = mkOption { type = types.attrsOf types.str; default = { }; };
|
||||||
|
timeout = mkOption { type = types.nullOr types.int; default = null; };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = { };
|
||||||
|
description = "MCP server configurations (merged into config.mcp_servers).";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
users.users.${cfg.user} = {
|
# ── Merge MCP servers into config ────────────────────────────────────
|
||||||
isSystemUser = true;
|
services.hermes-agent.config = lib.mkIf (cfg.mcpServers != { }) {
|
||||||
group = cfg.group;
|
mcp_servers = lib.mapAttrs (_name: srv:
|
||||||
home = cfg.homeDir;
|
{ inherit (srv) command args; }
|
||||||
createHome = true;
|
// lib.optionalAttrs (srv.env != { }) { inherit (srv) env; }
|
||||||
description = "Hermes Agent service user";
|
// lib.optionalAttrs (srv.timeout != null) { inherit (srv) timeout; }
|
||||||
|
) cfg.mcpServers;
|
||||||
};
|
};
|
||||||
|
|
||||||
users.groups.${cfg.group} = { };
|
# ── User / group ─────────────────────────────────────────────────────
|
||||||
|
users.groups.${cfg.group} = lib.mkIf cfg.createUser { };
|
||||||
|
users.users.${cfg.user} = lib.mkIf cfg.createUser {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
home = cfg.stateDir;
|
||||||
|
createHome = true;
|
||||||
|
shell = pkgs.bashInteractive;
|
||||||
|
};
|
||||||
|
|
||||||
systemd.services.hermes-agent = {
|
# ── Directories ──────────────────────────────────────────────────────
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.hermes 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.workingDirectory} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${builtins.dirOf cfg.logPath} 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# ── Activation: link config + documents into state dir ───────────────
|
||||||
|
system.activationScripts."hermes-agent-setup" = lib.stringAfter [ "users" ] ''
|
||||||
|
# Link config file
|
||||||
|
install -o ${cfg.user} -g ${cfg.group} -m 0640 -D ${configFile} ${cfg.stateDir}/.hermes/cli-config.yaml
|
||||||
|
|
||||||
|
# Link documents into workspace
|
||||||
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _value: ''
|
||||||
|
install -o ${cfg.user} -g ${cfg.group} -m 0644 ${documentDerivation}/${name} ${cfg.workingDirectory}/${name}
|
||||||
|
'') cfg.documents)}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# ── systemd service ──────────────────────────────────────────────────
|
||||||
|
systemd.services.${cfg.unitName} = {
|
||||||
description = "Hermes Agent Gateway";
|
description = "Hermes Agent Gateway";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network-online.target" ];
|
after = [ "network-online.target" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
HOME = cfg.homeDir;
|
HOME = cfg.stateDir;
|
||||||
HERMES_HOME = "${cfg.homeDir}/.hermes";
|
HERMES_HOME = "${cfg.stateDir}/.hermes";
|
||||||
} // cfg.extraEnvironment;
|
MESSAGING_CWD = cfg.workingDirectory;
|
||||||
|
};
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
WorkingDirectory = cfg.workDir;
|
WorkingDirectory = cfg.workingDirectory;
|
||||||
ExecStart = lib.concatStringsSep " " ([
|
|
||||||
"${cfg.package}/bin/hermes"
|
EnvironmentFile =
|
||||||
"gateway"
|
[ generatedEnvFile ]
|
||||||
] ++ cfg.extraArgs);
|
++ cfg.environmentFiles;
|
||||||
Restart = "on-failure";
|
|
||||||
RestartSec = 5;
|
ExecStart =
|
||||||
|
if cfg.execStart != null then cfg.execStart
|
||||||
|
else lib.concatStringsSep " " ([
|
||||||
|
"${cfg.package}/bin/hermes"
|
||||||
|
"gateway"
|
||||||
|
] ++ cfg.extraArgs);
|
||||||
|
|
||||||
|
Restart = cfg.restart;
|
||||||
|
RestartSec = cfg.restartSec;
|
||||||
|
|
||||||
|
StandardOutput = "append:${cfg.logPath}";
|
||||||
|
StandardError = "append:${cfg.logPath}";
|
||||||
|
|
||||||
# Hardening
|
# Hardening
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
ProtectHome = false;
|
ProtectHome = false;
|
||||||
ReadWritePaths = [ cfg.homeDir ];
|
ReadWritePaths = [ cfg.stateDir ];
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
} // lib.optionalAttrs (cfg.environmentFile != null) {
|
|
||||||
EnvironmentFile = cfg.environmentFile;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
path = [ cfg.package ] ++ cfg.extraPackages;
|
path = [
|
||||||
|
cfg.package
|
||||||
|
pkgs.bash
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.git
|
||||||
|
] ++ cfg.extraPackages;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue