Files
nixos-config/hosts/m3-hermes/services/hermes-agent.nix
T

402 lines
13 KiB
Nix
Raw Normal View History

{
config,
2026-05-31 14:10:15 +02:00
lib,
pkgs,
inputs,
...
}: let
# Edge TTS: Seraphina — friendly, multilingual German female voice (free, no API key)
edgeVoice = "de-DE-SeraphinaMultilingualNeural";
2026-06-06 13:15:27 +02:00
agentSkillExclusions = {
m3ta-agents = [];
anthropic = ["pdf" "skill-creator" "xlsx"];
basecamp = [];
kestra = [];
mattpocock = ["grill-me" "caveman"];
superpowers = ["brainstorming" "systematic-debugging"];
vercel = [];
};
2026-05-31 14:10:15 +02:00
2026-06-06 13:15:27 +02:00
agentLibSharedSkillsDir = ".agents/skills";
agentLibHomeManagerStub = {lib, ...}: {
options.home.homeDirectory = lib.mkOption {
type = lib.types.str;
default = "/var/lib/hermes";
2026-05-31 14:10:15 +02:00
};
2026-06-06 13:15:27 +02:00
options.home.file = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({name, ...}: {
options = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
executable = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
};
force = lib.mkOption {
type = lib.types.bool;
default = false;
};
ignorelinks = lib.mkOption {
type = lib.types.bool;
default = false;
};
onChange = lib.mkOption {
type = lib.types.lines;
default = "";
};
recursive = lib.mkOption {
type = lib.types.bool;
default = false;
};
source = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
};
target = lib.mkOption {
type = lib.types.str;
default = name;
};
text = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
};
};
}));
default = {};
};
options.home.packages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [];
};
options.assertions = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
default = [];
};
};
2026-05-31 14:10:15 +02:00
2026-06-06 13:15:27 +02:00
agentLibSourceSelections =
lib.mapAttrs (_sourceName: exclude: {
skills = {
all = true;
inherit exclude;
};
})
agentSkillExclusions;
2026-05-31 14:10:15 +02:00
2026-06-06 13:15:27 +02:00
# Evaluate agent-lib's Hermes target renderer with a minimal Home Manager
# surface, then expose its selected shared-skill outputs as a single Nix store
# directory for hermes-agent's native `skills.external_dirs` setting. The full
# Home Manager module is not imported into this NixOS host because Hermes runs
# as a system service user rather than a managed login user's Home Manager
# generation.
agentLibEval = lib.evalModules {
specialArgs = {inherit pkgs;};
modules = [
agentLibHomeManagerStub
inputs.agent-lib.homeManagerModules.default
{
home.homeDirectory = "/var/lib/hermes";
programs.agent-lib = {
enable = true;
lockFile = ../../../agent-sources.lock.json;
shared.skillsDir = agentLibSharedSkillsDir;
targets.hermes.enable = true;
profiles.default.sources = agentLibSourceSelections;
};
}
];
};
agentLibFailedAssertions = lib.filter (assertion: !assertion.assertion) agentLibEval.config.assertions;
agentLibHomeFiles =
if agentLibFailedAssertions != []
then throw (builtins.head agentLibFailedAssertions).message
else agentLibEval.config.home.file;
hermesSkillHomeFiles =
lib.filterAttrs (
targetPath: file:
lib.hasPrefix "${agentLibSharedSkillsDir}/" targetPath
&& file ? source
&& file.source != null
)
agentLibHomeFiles;
linkHermesSkill = targetPath: file: let
skillName = lib.removePrefix "${agentLibSharedSkillsDir}/" targetPath;
2026-05-31 14:10:15 +02:00
in ''
2026-06-06 13:15:27 +02:00
ln -s ${file.source} "$out"/${lib.escapeShellArg skillName}
2026-05-31 14:10:15 +02:00
'';
2026-06-06 13:15:27 +02:00
# Deterministic store renderer consumed directly by Hermes. Each entry is a
# symlink to the immutable skill directory selected by agent-lib, so
# `$out/<skill>/SKILL.md` exists without a mutable copy service.
hermesSkills =
if hermesSkillHomeFiles == {}
then throw "agent-lib: Hermes skill selection produced no skills"
else
pkgs.runCommand "hermes-agent-lib-skills" {} ''
mkdir -p $out
${lib.concatMapAttrsStringSep "\n" linkHermesSkill hermesSkillHomeFiles}
'';
2026-04-07 06:19:05 +02:00
in {
2026-04-26 10:48:52 +02:00
virtualisation.docker.enable = true;
systemd.tmpfiles.rules = [
2026-05-23 08:55:05 +02:00
"d /var/lib/hermes/.config 0755 hermes hermes -"
"d /var/lib/hermes/.config/tea 0755 hermes hermes -"
"L+ /var/lib/hermes/.config/tea/yml - - - - ${pkgs.writeText "tea-yml" ''
2026-04-26 10:48:52 +02:00
logins:
- name: m3ta
url: https://code.m3ta.dev
token:
ssh_host: code.m3ta.dev
user: m3ta-chiron
default: true
''}"
];
2026-06-06 13:15:27 +02:00
systemd.services.hermes-agent.restartTriggers = [hermesSkills];
2026-04-07 06:19:05 +02:00
services.hermes-agent = {
enable = true;
addToSystemPackages = true;
2026-05-23 08:55:05 +02:00
# v0.14 lazy-installs heavy optional backends by default. In the sealed
# Nix package, include the backends this host config actively uses so the
2026-05-23 10:32:11 +02:00
# gateway, Matrix bridge, memory, web search, TTS, and local STT work
2026-05-23 08:55:05 +02:00
# without runtime pip/uv mutation.
extraDependencyGroups = [
"matrix"
"honcho"
"exa"
"edge-tts"
2026-05-23 10:32:11 +02:00
"voice"
2026-05-23 08:55:05 +02:00
];
2026-04-07 06:19:05 +02:00
2026-05-11 19:01:17 +02:00
extraPackages = with pkgs; [
docker
git
2026-05-23 08:55:05 +02:00
curl
jq
2026-05-11 19:01:17 +02:00
tea
nix
2026-05-23 08:55:05 +02:00
python3Minimal
uv
2026-05-11 19:01:17 +02:00
zellij
];
2026-04-26 10:48:52 +02:00
2026-04-07 06:19:05 +02:00
# Secrets via agenix
environmentFiles = [
config.age.secrets."hermes-env".path
config.age.secrets."hermes-cloud-env".path
config.age.secrets."hermes-api-server-key".path
];
2026-04-07 06:19:05 +02:00
# Non-secret environment variables
2026-05-11 19:27:11 +02:00
# Git identity is set entirely via env vars (GIT_AUTHOR_*, GIT_COMMITTER_*,
# GIT_INIT_DEFAULT_BRANCH) — no .gitconfig file needed. Env vars take
# precedence over any gitconfig, and the hermes gateway injects them into
# all terminal sessions via .env.
2026-04-07 06:19:05 +02:00
environment = {
GLM_BASE_URL = "https://api.z.ai/api/coding/paas/v4/";
2026-04-26 10:48:52 +02:00
GIT_AUTHOR_NAME = "m3ta-chiron";
GIT_AUTHOR_EMAIL = "m3ta-chiron@agentmail.to";
GIT_COMMITTER_NAME = "m3ta-chiron";
GIT_COMMITTER_EMAIL = "m3ta-chiron@agentmail.to";
2026-05-11 19:27:11 +02:00
GIT_INIT_DEFAULT_BRANCH = "master";
# ── API Server (OpenAI-compatible, for Hermes Desktop App) ─────────
# Accessible via Netbird mesh VPN — not exposed to the public internet.
# Bind to 0.0.0.0 so the Netbird interface can reach it.
API_SERVER_ENABLED = "true";
API_SERVER_HOST = "0.0.0.0";
2026-05-23 08:55:05 +02:00
API_SERVER_PORT = toString (config.m3ta.ports.get "hermes-api");
2026-04-07 06:19:05 +02:00
};
# ── Container mode (podman) ──────────────────────────────────────────
container = {
2026-04-26 10:48:52 +02:00
enable = false;
2026-04-07 06:19:05 +02:00
backend = "podman";
extraVolumes = ["/home/m3tam3re/p:/projects:rw"];
2026-05-23 08:06:01 +02:00
extraOptions = [];
2026-04-07 06:19:05 +02:00
};
settings = {
# ── Model ──────────────────────────────────────────────────────────
model = {
2026-05-23 09:19:38 +02:00
default = "gpt-5.5";
provider = "openai-codex";
2026-04-07 06:19:05 +02:00
};
2026-05-23 09:19:38 +02:00
fallback_providers = [
{
provider = "zai";
model = "glm-5.1";
}
{
provider = "minimax";
model = "MiniMax-M2.7";
}
];
2026-04-07 06:19:05 +02:00
credential_pool_strategies = {
zai = "fill_first";
};
toolsets = ["all"];
# ── Agent ──────────────────────────────────────────────────────────
agent = {
max_turns = 90;
gateway_timeout = 1800;
tool_use_enforcement = "auto";
2026-05-29 17:38:20 +02:00
reasoning_effort = "high";
2026-04-07 06:19:05 +02:00
};
# ── Skills ─────────────────────────────────────────────────────────
skills = {
external_dirs = [
2026-06-06 13:15:27 +02:00
hermesSkills
];
};
2026-04-07 06:19:05 +02:00
# ── Terminal ───────────────────────────────────────────────────────
terminal = {
2026-04-26 10:48:52 +02:00
backend = "local";
2026-04-07 06:19:05 +02:00
modal_mode = "auto";
cwd = ".";
timeout = 180;
persistent_shell = true;
};
# ── Browser ────────────────────────────────────────────────────────
browser = {
inactivity_timeout = 120;
command_timeout = 30;
cloud_provider = "local";
};
# ── Checkpoints v2 ─────────────────────────────────────────────────
# v0.13.0: Single-store rewrite with real pruning + disk guardrails.
2026-04-07 06:19:05 +02:00
checkpoints = {
enabled = true;
max_snapshots = 50;
};
file_read_max_chars = 100000;
compression = {
enabled = true;
threshold = 0.5;
target_ratio = 0.2;
protect_last_n = 20;
};
# ── Display ────────────────────────────────────────────────────────
display = {
compact = false;
personality = "kawaii";
resume_display = "full";
busy_input_mode = "interrupt";
inline_diffs = true;
skin = "default";
tool_progress = "all";
};
# ── TTS / STT / Voice ──────────────────────────────────────────────
tts = {
provider = "edge";
edge = {
voice = edgeVoice;
2026-04-07 06:19:05 +02:00
};
};
stt = {
enabled = true;
provider = "local";
local = {model = "base";};
};
voice = {
record_key = "ctrl+b";
max_recording_seconds = 120;
silence_threshold = 200;
silence_duration = 3.0;
};
# ── Memory ─────────────────────────────────────────────────────────
memory = {
2026-04-26 10:48:52 +02:00
provider = "honcho";
2026-04-07 06:19:05 +02:00
memory_enabled = true;
user_profile_enabled = true;
memory_char_limit = 2200;
user_char_limit = 1375;
};
# ── Delegation / Orchestrator ────────────────────────────────────────
2026-04-07 06:19:05 +02:00
delegation = {
max_iterations = 50;
orchestrator_enabled = true;
max_spawn_depth = 2;
2026-04-07 06:19:05 +02:00
};
# ── Kanban (v0.13.0 — Multi-Agent Board) ──────────────────────────
# Durable task board with embedded dispatcher in gateway process.
# Workers are full OS processes with identity, heartbeat, reclaim,
# zombie detection, and hallucination gate.
kanban = {
dispatch_in_gateway = true;
dispatch_interval_seconds = 60;
};
2026-04-13 16:44:18 +02:00
# ── Matrix ────────────────────────────────────────────────────────
matrix = {
homeserver = "https://matrix.m3ta.dev";
user_id = "@chiron:m3ta.dev";
allowed_users = ["@m3tam3re:m3ta.dev"];
encryption = true;
2026-05-11 19:01:17 +02:00
group_sessions_per_user = true;
auto_thread = true;
dm_mention_threads = true;
2026-04-07 06:19:05 +02:00
};
# ── Approvals / Security ───────────────────────────────────────────
approvals = {
mode = "manual";
timeout = 60;
};
security = {
redact_secrets = true;
tirith_enabled = true;
tirith_fail_open = true;
};
# ── Cron / Session ─────────────────────────────────────────────────
cron = {wrap_response = true;};
session_reset = {
mode = "both";
idle_minutes = 1440;
at_hour = 4;
};
# ── Web ────────────────────────────────────────────────────────────
web = {backend = "exa";};
};
};
users.users.hermes = {
isNormalUser = false;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICAVF7jGP1S6vc5CxeBFD/UxiImHOgbPlKg8WYyNtOA3"
];
};
2026-04-07 06:19:05 +02:00
}