From 1a17b852a18076e0bea5c4aba9897b8a42c21750 Mon Sep 17 00:00:00 2001 From: m3ta-chiron Date: Mon, 11 May 2026 19:05:13 +0200 Subject: [PATCH] refactor: remove redundant .gitconfig tmpfiles rule, use GIT_INIT_DEFAULT_BRANCH env var The .gitconfig at /home/hermes/.gitconfig was never read because the hermes-agent service sets HOME=/var/lib/hermes. Git identity was already fully covered by GIT_AUTHOR_* and GIT_COMMITTER_* env vars via .env. Replace the dead .gitconfig's init.defaultBranch with the env var GIT_INIT_DEFAULT_BRANCH (supported since Git 2.28). --- hosts/m3-atlas/services/honcho.nix | 422 ++++++++++++++++++++++ hosts/m3-atlas/services/redis.nix | 16 + hosts/m3-hermes/services/hermes-agent.nix | 12 +- pkgs/honcho/default.nix | 153 ++++++++ 4 files changed, 596 insertions(+), 7 deletions(-) create mode 100644 hosts/m3-atlas/services/honcho.nix create mode 100644 hosts/m3-atlas/services/redis.nix create mode 100644 pkgs/honcho/default.nix diff --git a/hosts/m3-atlas/services/honcho.nix b/hosts/m3-atlas/services/honcho.nix new file mode 100644 index 0000000..6ca6c64 --- /dev/null +++ b/hosts/m3-atlas/services/honcho.nix @@ -0,0 +1,422 @@ +{ + 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; + } + ]; + }; + }; +} diff --git a/hosts/m3-atlas/services/redis.nix b/hosts/m3-atlas/services/redis.nix new file mode 100644 index 0000000..6bdc80f --- /dev/null +++ b/hosts/m3-atlas/services/redis.nix @@ -0,0 +1,16 @@ +{config, ...}: { + services.redis.servers.honcho = { + enable = true; + port = 6380; # Separate from default Redis (6379) to avoid conflicts + bind = "127.0.0.1"; + save = [ + [900 1] + [300 10] + [60 10000] + ]; + }; + + networking.firewall.extraCommands = '' + iptables -A INPUT -p tcp -s 127.0.0.1 --dport 6380 -j ACCEPT + ''; +} diff --git a/hosts/m3-hermes/services/hermes-agent.nix b/hosts/m3-hermes/services/hermes-agent.nix index d67bcd7..91e7e92 100644 --- a/hosts/m3-hermes/services/hermes-agent.nix +++ b/hosts/m3-hermes/services/hermes-agent.nix @@ -49,13 +49,6 @@ in { user: m3ta-chiron default: true ''}" - "f /home/hermes/.gitconfig 0644 hermes hermes - ${pkgs.writeText "gitconfig" '' - [user] - name = m3ta-chiron - email = m3ta-chiron@agentmail.to - [init] - defaultBranch = master - ''}" ]; systemd.services.copy-hermes-skills = { @@ -95,12 +88,17 @@ in { ]; # Non-secret environment variables + # 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. environment = { GLM_BASE_URL = "https://api.z.ai/api/coding/paas/v4/"; GIT_AUTHOR_NAME = "m3ta-chiron"; GIT_AUTHOR_EMAIL = "m3ta-chiron@agentmail.to"; GIT_COMMITTER_NAME = "m3ta-chiron"; GIT_COMMITTER_EMAIL = "m3ta-chiron@agentmail.to"; + GIT_INIT_DEFAULT_BRANCH = "master"; # ── API Server (OpenAI-compatible, for Hermes Desktop App) ───────── # Accessible via Netbird mesh VPN — not exposed to the public internet. diff --git a/pkgs/honcho/default.nix b/pkgs/honcho/default.nix new file mode 100644 index 0000000..a4df1d2 --- /dev/null +++ b/pkgs/honcho/default.nix @@ -0,0 +1,153 @@ +{ + lib, + stdenv, + fetchFromGitHub, + python3, +}: +# NOTE: First build will fail with a hash mismatch error. +# Copy the "got: sha256-XXX..." from the error and replace fakeHash below. +let + version = "3.0.6"; + + src = fetchFromGitHub { + owner = "plastic-labs"; + repo = "honcho"; + tag = "v${version}"; + hash = lib.fakeHash; + }; + + pythonEnv = python3.withPackages (ps: + with ps; [ + # Core web framework + fastapi + uvicorn + httptools + python-dotenv + sqlalchemy + fastapi-pagination + + # Database & vector + pgvector + psycopg + greenlet + + # LLM providers + openai + google-genai + + # Utilities + httpx + rich + nanoid + alembic + pyjwt + tenacity + tiktoken + langfuse + pydantic + pydantic-settings + pdfplumber + typing-extensions + json-repair + scikit-learn + prometheus-client + cloudevents + + # Vector store backends + turbopuffer + lancedb + pyarrow + + # Cache + redis + cashews + + # Observability + sentry-sdk + ]); +in + stdenv.mkDerivation { + pname = "honcho"; + inherit version src; + + buildInputs = [pythonEnv]; + + buildPhase = '' + rm -rf sdks honcho-cli + ''; + + installPhase = '' + mkdir -p $out/{src,migrations,scripts,bin} + + cp -r src/* $out/src/ + cp -r migrations/* $out/migrations/ + cp scripts/provision_db.py $out/scripts/ + cp alembic.ini $out/ + + # API wrapper + cat > $out/bin/honcho-api << 'WRAPPER' + #!/bin/sh + exec ${pythonEnv}/bin/python -c " + import sys, os + app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + os.chdir(app_dir) + sys.path.insert(0, app_dir) + os.environ.setdefault('PYTHON_DOTENV_DISABLED', 'true') + os.environ.setdefault('HONCHO_CONFIG_TOML_DISABLED', 'true') + import uvicorn + port = int(os.environ.get('PORT', '8000')) + for i, arg in enumerate(sys.argv[1:]): + if arg.startswith('--port='): + port = int(arg.split('=',1)[1]) + elif arg == '--port': + nxt = sys.argv[i+2:i+3] + if nxt: port = int(nxt[0]) + uvicorn.run('src.main:app', host='0.0.0.0', port=port) + " "$@" + WRAPPER + chmod +x $out/bin/honcho-api + + # Deriver wrapper + cat > $out/bin/honcho-deriver << 'WRAPPER' + #!/bin/sh + exec ${pythonEnv}/bin/python -c " + import sys, os + app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + os.chdir(app_dir) + sys.path.insert(0, app_dir) + os.environ.setdefault('PYTHON_DOTENV_DISABLED', 'true') + os.environ.setdefault('HONCHO_CONFIG_TOML_DISABLED', 'true') + from src.deriver.__main__ import * + " "$@" + WRAPPER + chmod +x $out/bin/honcho-deriver + + # Migration wrapper + cat > $out/bin/honcho-migrate << 'WRAPPER' + #!/bin/sh + exec ${pythonEnv}/bin/python -c " + import sys, os, asyncio + app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + os.chdir(app_dir) + sys.path.insert(0, app_dir) + os.environ.setdefault('PYTHON_DOTENV_DISABLED', 'true') + os.environ.setdefault('HONCHO_CONFIG_TOML_DISABLED', 'true') + from src.db import init_db + asyncio.run(init_db()) + " "$@" + WRAPPER + chmod +x $out/bin/honcho-migrate + ''; + + passthru = { + inherit pythonEnv; + python = pythonEnv; + }; + + meta = { + description = "Honcho — Memory system for stateful AI agents"; + homepage = "https://honcho.dev"; + license = lib.licenses.agpl3Only; + platforms = lib.platforms.linux; + }; + } -- 2.54.0