{ config, pkgs, lib, hermes-agent, ... }: let inherit (lib) mkEnableOption mkIf mkMerge; cfg = config.foundation.agent; agentPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAppvnIQdnwggxcsNtm7ij85qjABRHe2b4/NGwCfzTr5 agent"; # the set of tools the agent has access to by default. # it should also always be able to call `nix-shell` to gain access to more tools. agentPackages = with pkgs; [ nix nixfmt-rfc-style openssh tailscale docker_29 jq yq-go ripgrep fd tree file unzip gnused gawk curl wget htop glances lsof iproute2 iputils inetutils dig tcpdump nmap git vim ]; in { imports = [ hermes-agent.nixosModules.default ]; options.foundation.agent = { home = mkEnableOption "the 'renard' agent to live on this machine"; neighbor = mkEnableOption "letting the 'renard' agent access this machine"; }; config = mkMerge [ # both the home and each neighbor have a user for the agent, # the identity of the user is the same on the entire fleet. (mkIf (cfg.home || cfg.neighbor) { users = { groups.renard = { }; users.renard = { isSystemUser = true; group = "renard"; home = "/home/renard"; createHome = true; shell = pkgs.bashInteractive; packages = agentPackages; openssh.authorizedKeys.keys = [ agentPublicKey ]; }; }; systemd.tmpfiles.rules = [ "d /home/renard/.ssh 0700 renard renard - -" ]; age.secrets.renard-agent-private-key = { file = ../../secrets/agent/private-key.age; path = "/home/renard/.ssh/id_ed25519"; owner = "renard"; group = "renard"; mode = "0600"; }; }) # the agent lives at home. (mkIf cfg.home { age.secrets.renard-agent-environment = { file = ../../secrets/agent/environment.age; owner = "renard"; group = "renard"; mode = "0440"; }; services.hermes-agent = { enable = true; user = "renard"; group = "renard"; stateDir = "/home/renard"; createUser = false; extraDependencyGroups = [ "messaging" ]; # both the user and the path house the same package set extraPackages = agentPackages; environmentFiles = [ # required secrets: # * OPENROUTER_API_KEY # * DISCORD_BOT_TOKEN # * DISCORD_ALLOWED_USERS config.age.secrets.renard-agent-environment.path ]; documents = { "SOUL.md" = ../../assets/agent/SOUL.md; }; settings = { model = { provider = "openrouter"; # my current favorite budget model, with recent price # changes by far the cheapest for the performance, # with other chinese models also being close. # only modality is text, requires auxiliary vision model! # native price: $0.44/m in, $0.87/m out. default = "deepseek/deepseek-v4-pro"; }; agent = { reasoning_effort = "high"; # 'high' rated highest in benchmarks, tune to taste! max_turns = 90; }; auxiliary.vision = { # auxiliary vision model for image comprehension. # this qwen model is built for this exact use-case and is extremely # cheap for the performance (though below gemini), however, # it does not do any reasoning on the visual input! # possible upgrade path for reasoning: qwen3-vl-30b-a3b-thinking # native price: $0.13/m in, $1.56/m out. provider = "openrouter"; model = "qwen/qwen3-vl-32b-instruct"; timeout = 120; }; toolsets = [ "all" ]; terminal = { backend = "local"; persistent_shell = true; timeout = 180; }; memory = { memory_enabled = true; user_profile_enabled = true; }; # slightly higher limits and targets than default for better # long session preservation. compression = { enabled = true; threshold = 0.75; target_ratio = 0.35; protect_last_n = 40; }; # progressive message editing, a little silly in discord but looks like # the classic llm chats. streaming = { enabled = true; }; display = { streaming = true; # small regression with streaming.enabled, keep both on. tool_progress = "new"; # only show fresh new tool calls, not every call. }; discord = { require_mention = false; thread_require_mention = false; auto_thread = false; reactions = true; history_backfill = true; allow_mentions = { everyone = false; roles = false; users = true; replied_user = true; }; }; unauthorized_dm_behavior = "ignore"; # ignore strangers timezone = "Europe/Berlin"; group_sessions_per_user = false; }; }; }) ]; }