flake/modules/nixos/server/hermes.nix
2026-06-01 14:15:44 +00:00

136 lines
4 KiB
Nix

{
pkgs,
lib,
config,
...
}:
let
cfg = config.mods.server.hermes;
auth2apiCfg = config.mods.server.auth2api;
# auth2api exposes an OpenAI-compatible endpoint; point Hermes at it.
auth2apiUrl = "http://${auth2apiCfg.host}:${toString auth2apiCfg.port}/v1";
in
with lib; {
options.mods.server.hermes = {
enable = mkEnableOption "Hermes AI agent (NousResearch)";
model = mkOption {
type = types.str;
default = "claude-sonnet-4-6";
description = ''
Model identifier as exposed by auth2api. Available aliases:
sonnet, haiku, opus, claude-sonnet-4-6, claude-haiku-4-5, claude-opus-4-6, etc.
Run: curl http://127.0.0.1:8317/v1/models
'';
};
container = {
enable = mkOption {
type = types.bool;
default = true;
description = "Run Hermes inside a persistent Ubuntu container.";
};
hostUsers = mkOption {
type = types.listOf types.str;
default = [ config.mods.user.name ];
description = ''
Users that get a ~/.hermes symlink to the service stateDir and are
added to the hermes group for shared file access.
'';
};
extraVolumes = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Extra volume mounts (host:container:mode) for the container.";
};
};
addToSystemPackages = mkOption {
type = types.bool;
default = true;
description = ''
Put the hermes CLI on the system PATH and set HERMES_HOME so the
interactive CLI shares state with the gateway service.
'';
};
extraPackages = mkOption {
type = types.listOf types.package;
default = [ ];
description = "Extra Nix packages available to the agent at runtime.";
};
stateDir = mkOption {
type = types.str;
default = "/var/lib/hermes";
description = "State directory (HERMES_HOME parent).";
};
};
config = mkIf cfg.enable {
# Hermes needs auth2api running to have a backend to call.
assertions = [
{
assertion = auth2apiCfg.enable;
message = "mods.server.hermes requires mods.server.auth2api.enable = true";
}
];
services.hermes-agent = {
enable = true;
inherit (cfg) stateDir addToSystemPackages extraPackages;
# auth2api runs unauthenticated on localhost (api-keys: []), but Hermes's
# provider resolver requires a non-empty key env var for any provider.
# AUTH2API_KEY is a dummy — auth2api ignores Bearer tokens when api-keys: [].
environment.AUTH2API_KEY = "auth2api";
# ── Model ──────────────────────────────────────────────────────────────
# Declare auth2api as a named custom provider in the providers: section.
# Hermes resolves it via resolve_user_provider, which accepts arbitrary
# base_url + key_env without needing a built-in provider slug.
settings = {
providers.auth2api = {
name = "auth2api (Claude via OAuth)";
api = auth2apiUrl;
key_env = "AUTH2API_KEY";
transport = "openai_chat";
};
model = {
default = cfg.model;
provider = "auth2api";
};
toolsets = [ "all" ];
max_turns = 100;
terminal = {
backend = "local";
timeout = 180;
};
memory = {
memory_enabled = true;
user_profile_enabled = true;
};
display = {
compact = false;
show_reasoning = true;
};
};
# ── Container ──────────────────────────────────────────────────────────
container = mkIf cfg.container.enable {
enable = true;
backend = "docker";
inherit (cfg.container) hostUsers extraVolumes;
};
};
};
}