{ pkgs, hostConfig, ... }: { imports = [ hostConfig.inputs.hermes-agent.nixosModules.default ]; services.hermes-agent = { enable = true; addToSystemPackages = true; container.enable = false; # Run the daemon as the same user that owns workspace files so the agent # can read/write the project tree without permission gymnastics. user = "googlebot"; group = "users"; createUser = false; # Share the user's workspace tree so the agent operates on the same files # you see when SSH'd in. Codex's workspace-write sandbox keeps writes scoped # to this dir. workingDirectory = "/home/googlebot/workspace"; extraPackages = with pkgs; [ nix git ripgrep fd jq codex ]; # Pulls in hindsight-client (the HTTP client lib the memory plugin uses). extraDependencyGroups = [ "hindsight" ]; environment = { SIGNAL_HTTP_URL = "http://127.0.0.1:8080"; CODEX_HOME = "/var/lib/hermes/.codex"; }; # Bind-mounted from /run/agenix/hermes-env on fry (host decrypts via agenix). # Lives at /etc/... rather than /run/... because the workspace's systemd # mounts a fresh tmpfs over /run at boot, which would shadow the incus mount. # Codex OAuth is NOT here — it lives per-instance in /var/lib/hermes. environmentFiles = [ "/etc/hermes-env" ]; settings = { model = { provider = "openai-codex"; default = "gpt-5.4"; # Delegate openai/* turns to the codex CLI subprocess so the agent gets # codex's sandbox + tooling. Codex CLI must be on PATH and authenticated # via `codex login` (separate from `hermes auth`). openai_runtime = "codex_app_server"; }; toolsets = [ "all" ]; terminal.backend = "local"; # Memory lives in a sibling hindsight-api process (see systemd unit # below) backed by system postgres. Plugin talks HTTP to it locally. memory.provider = "hindsight"; plugins.hermes-hindsight = { mode = "local_external"; api_url = "http://127.0.0.1:8888"; bank_id = "hermes"; memory_mode = "hybrid"; }; }; }; # Align googlebot's interactive `hermes` / `codex` invocations with the # daemon's persisted state dirs so logins from a shell land where the # service expects to read them. home-manager.users.googlebot.home.sessionVariables = { HERMES_HOME = "/var/lib/hermes/.hermes"; CODEX_HOME = "/var/lib/hermes/.codex"; }; home-manager.users.googlebot.home.packages = with pkgs; [ codex signal-cli ]; # Declarative codex CLI defaults — seeded into $CODEX_HOME on first boot, # then codex owns the file (it rewrites config.toml on `codex login`, project # trust grants, etc.). To re-seed after changing the defaults below, delete # the persisted /var/lib/hermes/.codex/config.toml and restart hermes-agent. environment.etc."hermes-codex-config.toml".text = '' model = "gpt-5.4" model_reasoning_effort = "medium" sandbox_mode = "danger-full-access" approval_policy = "never" ''; systemd.tmpfiles.rules = [ "d /var/lib/hermes/.codex 0700 googlebot users -" "C+ /var/lib/hermes/.codex/config.toml 0644 googlebot users - /etc/hermes-codex-config.toml" "d /var/lib/hermes/hindsight 0700 googlebot users -" "d /var/lib/hermes/postgresql 0750 postgres postgres -" ]; # signal-cli runs an HTTP JSON-RPC daemon on localhost; hermes-agent talks to # it via SIGNAL_HTTP_URL. Registered as the PRIMARY device on a dedicated # number (GV / JMP.chat / prepaid SIM) — the account identity lives entirely # in /var/lib/hermes/signal-cli/, which is bind-mounted from the host. If # that tree is lost, the Signal account is irrecoverable, so back it up. systemd.services.signal-cli = { description = "signal-cli JSON-RPC daemon for Hermes"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; before = [ "hermes-agent.service" ]; serviceConfig = { Type = "simple"; User = "googlebot"; Group = "users"; ExecStart = "${pkgs.signal-cli}/bin/signal-cli --config /var/lib/hermes/signal-cli daemon --http 127.0.0.1:8080"; Restart = "always"; RestartSec = "5"; }; }; # --------------------------------------------------------------------------- # Hindsight memory backend # # Architecture: # hermes-agent --HTTP--> hindsight-api (port 8888) --asyncpg--> postgres # hindsight-worker (background async retain/reflect) # # The api + worker run as googlebot with HOME=/var/lib/hermes so the # `openai-codex` provider's hardcoded Path.home() / ".codex" / "auth.json" # resolves to the persisted codex tokens. # --------------------------------------------------------------------------- services.postgresql = { enable = true; package = pkgs.postgresql_17; extensions = ps: [ ps.pgvector ]; # Persist DB state on the /var/lib/hermes bind mount so the hindsight # memory bank survives `nixos-rebuild switch` (which recreates the # incus container and wipes its writable layer). dataDir = "/var/lib/hermes/postgresql/17"; ensureDatabases = [ "hindsight" ]; ensureUsers = [{ name = "googlebot"; # Superuser so the hindsight-api alembic migrations can `CREATE EXTENSION` # and own the schema. Acceptable here because postgres is local-only to # the sandboxed workspace and only talks to hindsight-api over the unix # socket. ensureClauses.superuser = true; }]; }; systemd.services.hindsight-api = let env = { # Connection — postgres is on the local unix socket; sqlalchemy gets # an asyncpg URL via the asyncpg+host= trick. HINDSIGHT_API_DATABASE_URL = "postgresql+asyncpg:///hindsight?host=/run/postgresql"; HINDSIGHT_API_HOST = "127.0.0.1"; HINDSIGHT_API_PORT = "8888"; # LLM + embeddings both ride codex OAuth — no separate keys. HINDSIGHT_API_LLM_PROVIDER = "openai-codex"; HINDSIGHT_API_EMBEDDINGS_PROVIDER = "openai-codex"; # We did not install [local-ml] extras, so disable the reranker # (default is "local" which requires sentence-transformers). HINDSIGHT_API_RERANKER_PROVIDER = "none"; # HOME drives where the codex auth file is read from # (CodexLLM/CodexOAuthEmbeddings: Path.home() / ".codex" / "auth.json"). HOME = "/var/lib/hermes"; }; in { description = "Hindsight memory API server"; wantedBy = [ "multi-user.target" ]; after = [ "postgresql.service" "network-online.target" ]; wants = [ "network-online.target" ]; requires = [ "postgresql.service" ]; before = [ "hermes-agent.service" ]; environment = env; serviceConfig = { Type = "simple"; User = "googlebot"; Group = "users"; ExecStart = "${pkgs.hindsight-api}/bin/hindsight-api"; Restart = "always"; RestartSec = "5"; WorkingDirectory = "/var/lib/hermes/hindsight"; }; }; systemd.services.hindsight-worker = let env = { HINDSIGHT_API_DATABASE_URL = "postgresql+asyncpg:///hindsight?host=/run/postgresql"; HINDSIGHT_API_LLM_PROVIDER = "openai-codex"; HINDSIGHT_API_EMBEDDINGS_PROVIDER = "openai-codex"; HINDSIGHT_API_RERANKER_PROVIDER = "none"; HOME = "/var/lib/hermes"; }; in { description = "Hindsight background worker (async retain / reflect)"; wantedBy = [ "multi-user.target" ]; after = [ "hindsight-api.service" ]; requires = [ "hindsight-api.service" ]; environment = env; serviceConfig = { Type = "simple"; User = "googlebot"; Group = "users"; ExecStart = "${pkgs.hindsight-api}/bin/hindsight-worker"; Restart = "always"; RestartSec = "5"; WorkingDirectory = "/var/lib/hermes/hindsight"; }; }; }