{ config, lib, pkgs, ... }: with lib; let cfg = config.services.honcho; in { options.services.honcho = { enable = mkEnableOption "Honcho memory server for AI agents"; package = mkOption { type = types.package; default = pkgs.callPackage ../../../pkgs/honcho {}; description = "The Honcho package to use."; }; user = mkOption { type = types.str; default = "honcho"; description = "User under which Honcho runs."; }; group = mkOption { type = types.str; default = "honcho"; description = "Group under which Honcho runs."; }; dataDir = mkOption { type = types.str; default = "/var/lib/honcho"; description = "Data directory for Honcho."; }; port = mkOption { type = types.port; default = 8000; description = "Port for the Honcho API server."; }; # ── Core Settings ── settings = mkOption { type = types.submodule { freeformType = types.attrsOf types.anything; options = { LOG_LEVEL = mkOption { type = types.str; default = "INFO"; description = "Log level."; }; NAMESPACE = mkOption { type = types.str; default = "honcho"; description = "Namespace prefix for vector stores and cache."; }; AUTH_USE_AUTH = mkOption { type = types.bool; default = false; description = "Enable JWT auth (not needed behind Traefik BasicAuth)."; }; CACHE_ENABLED = mkOption { type = types.bool; default = true; description = "Enable Redis caching."; }; VECTOR_STORE_TYPE = mkOption { type = types.str; default = "pgvector"; description = "Vector store backend."; }; SENTRY_ENABLED = mkOption { type = types.bool; default = false; description = "Enable Sentry."; }; METRICS_ENABLED = mkOption { type = types.bool; default = false; description = "Enable Prometheus metrics."; }; TELEMETRY_ENABLED = mkOption { type = types.bool; default = false; description = "Enable CloudEvents telemetry."; }; }; }; default = {}; description = "Honcho environment variables (env vars)."; }; # ── Database ── database = mkOption { type = types.submodule { options = { createLocally = mkOption { type = types.bool; default = true; description = "Create local PostgreSQL DB and user."; }; host = mkOption { type = types.str; default = "localhost"; }; port = mkOption { type = types.port; default = 5432; }; name = mkOption { type = types.str; default = "honcho"; }; user = mkOption { type = types.str; default = "honcho"; }; passwordFile = mkOption { type = types.nullOr types.path; default = null; description = "File containing DB password (exported as DB_CONNECTION_URI)."; }; }; }; default = {}; }; # ── Redis ── redis = mkOption { type = types.submodule { options = { url = mkOption { type = types.str; default = "redis://localhost:6380/0"; description = "Redis URL for Honcho caching."; }; }; }; default = {}; }; # ── LLM Provider ── llm = mkOption { type = types.submodule { options = { openaiApiKeyFile = mkOption { type = types.nullOr types.path; default = null; description = "File exporting LLM_OPENAI_API_KEY."; }; anthropicApiKeyFile = mkOption { type = types.nullOr types.path; default = null; description = "File exporting LLM_ANTHROPIC_API_KEY."; }; geminiApiKeyFile = mkOption { type = types.nullOr types.path; default = null; description = "File exporting LLM_GEMINI_API_KEY."; }; }; }; default = {}; }; # ── Deriver (Background Reasoning) ── deriver = mkOption { type = types.submodule { options = { enable = mkOption { type = types.bool; default = true; }; workers = mkOption { type = types.ints.positive; default = 1; }; model = mkOption { type = types.str; default = "gpt-4.1-mini"; description = "Model for deriver reasoning."; }; transport = mkOption { type = types.str; default = "openai"; description = "LLM transport (openai, anthropic, gemini)."; }; baseUrl = mkOption { type = types.nullOr types.str; default = null; description = "OpenAI-compatible base URL for self-hosted."; }; apiKeyFile = mkOption { type = types.nullOr types.path; default = null; description = "File exporting DERIVER_MODEL_CONFIG__OVERRIDES__API_KEY."; }; }; }; default = {}; }; # ── Dialectic ── dialectic = mkOption { type = types.submodule { options = { model = mkOption { type = types.str; default = "gpt-4.1-mini"; }; transport = mkOption { type = types.str; default = "openai"; }; baseUrl = mkOption { type = types.nullOr types.str; default = null; }; }; }; default = {}; }; # ── Embedding ── embedding = mkOption { type = types.submodule { options = { model = mkOption { type = types.str; default = "text-embedding-3-small"; }; transport = mkOption { type = types.str; default = "openai"; }; baseUrl = mkOption { type = types.nullOr types.str; default = null; }; }; }; default = {}; }; }; config = let # Build the DB connection URI dbUri = if cfg.database.passwordFile != null then "postgresql+psycopg://${cfg.database.user}@${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}" else "postgresql+psycopg://${cfg.database.user}@${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}"; # Common env vars for both API and Deriver commonEnv = cfg.settings // { DB_CONNECTION_URI = dbUri; CACHE_URL = cfg.redis.url; PYTHON_DOTENV_DISABLED = "true"; HONCHO_CONFIG_TOML_DISABLED = "true"; }; # Shared EnvironmentFile list envFiles = (optional (cfg.llm.openaiApiKeyFile != null) cfg.llm.openaiApiKeyFile) ++ (optional (cfg.llm.anthropicApiKeyFile != null) cfg.llm.anthropicApiKeyFile) ++ (optional (cfg.llm.geminiApiKeyFile != null) cfg.llm.geminiApiKeyFile) ++ (optional (cfg.deriver.apiKeyFile != null) cfg.deriver.apiKeyFile); in mkIf cfg.enable { # ── User & Group ── users.users.${cfg.user} = mkIf (cfg.user == "honcho") { isSystemUser = true; group = cfg.group; home = cfg.dataDir; createHome = true; }; users.groups.${cfg.group} = mkIf (cfg.group == "honcho") {}; # ── API Server ── systemd.services.honcho-api = { description = "Honcho Memory API Server"; after = ["network.target" "postgresql.service"]; requires = ["postgresql.service"]; wantedBy = ["multi-user.target"]; environment = commonEnv; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; WorkingDirectory = cfg.package; ExecStartPre = "${cfg.package}/bin/honcho-migrate"; ExecStart = "${cfg.package}/bin/honcho-api --port ${toString cfg.port}"; Restart = "on-failure"; RestartSec = "5"; EnvironmentFile = envFiles; # Security NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [cfg.dataDir]; PrivateTmp = true; }; }; # ── Deriver Worker ── systemd.services.honcho-deriver = mkIf cfg.deriver.enable { description = "Honcho Deriver (Background Reasoning)"; after = ["network.target" "postgresql.service" "honcho-api.service"]; requires = ["postgresql.service"]; wants = ["honcho-api.service"]; wantedBy = ["multi-user.target"]; environment = commonEnv // { DERIVER_ENABLED = "true"; DERIVER_WORKERS = toString cfg.deriver.workers; DERIVER_MODEL_CONFIG__TRANSPORT = cfg.deriver.transport; DERIVER_MODEL_CONFIG__MODEL = cfg.deriver.model; } // optionalAttrs (cfg.deriver.baseUrl != null) { DERIVER_MODEL_CONFIG__OVERRIDES__BASE_URL = cfg.deriver.baseUrl; } // ( builtins.listToAttrs ( map (level: { name = "DIALECTIC_LEVELS__${level}__MODEL_CONFIG__MODEL"; value = cfg.dialectic.model; }) ["minimal" "low" "medium" "high" "max"] ) ) // ( builtins.listToAttrs ( map (level: { name = "DIALECTIC_LEVELS__${level}__MODEL_CONFIG__TRANSPORT"; value = cfg.dialectic.transport; }) ["minimal" "low" "medium" "high" "max"] ) ) // optionalAttrs (cfg.dialectic.baseUrl != null) ( builtins.listToAttrs ( map (level: { name = "DIALECTIC_LEVELS__${level}__MODEL_CONFIG__OVERRIDES__BASE_URL"; value = cfg.dialectic.baseUrl; }) ["minimal" "low" "medium" "high" "max"] ) ) // { EMBEDDING_MODEL_CONFIG__MODEL = cfg.embedding.model; EMBEDDING_MODEL_CONFIG__TRANSPORT = cfg.embedding.transport; } // optionalAttrs (cfg.embedding.baseUrl != null) { EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL = cfg.embedding.baseUrl; }; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; WorkingDirectory = cfg.package; ExecStart = "${cfg.package}/bin/honcho-deriver"; Restart = "on-failure"; RestartSec = "10"; EnvironmentFile = envFiles; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [cfg.dataDir]; PrivateTmp = true; }; }; # ── Local PostgreSQL ── services.postgresql = mkIf cfg.database.createLocally { ensureDatabases = [cfg.database.name]; ensureUsers = [ { name = cfg.database.user; ensureDBOwnership = true; } ]; }; }; }