From 63b583aa2f277736d5c4de471df5ac49006a09b6 Mon Sep 17 00:00:00 2001 From: "balyan.sid@gmail.com" Date: Wed, 11 Mar 2026 17:40:35 +0530 Subject: [PATCH] feat: nix flake, uv2nix build, dev shell and home manager --- .envrc | 1 + .gitignore | 4 + docs/nixos-setup.md | 195 ++++++++++++++++++++++++++++++++++++++++++++ flake.lock | 181 ++++++++++++++++++++++++++++++++++++++++ flake.nix | 34 ++++++++ nix/devShell.nix | 51 ++++++++++++ nix/homeModules.nix | 93 +++++++++++++++++++++ nix/packages.nix | 67 +++++++++++++++ nix/python.nix | 28 +++++++ 9 files changed, 654 insertions(+) create mode 100644 .envrc create mode 100644 docs/nixos-setup.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/devShell.nix create mode 100644 nix/homeModules.nix create mode 100644 nix/packages.nix create mode 100644 nix/python.nix diff --git a/.envrc b/.envrc new file mode 100644 index 000000000000..3550a30f2de3 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 77ca54f3a5c1..baa31a543c18 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,7 @@ environments/benchmarks/evals/ # Release script temp files .release_notes.md mini-swe-agent/ + +# Nix +.direnv/ +result diff --git a/docs/nixos-setup.md b/docs/nixos-setup.md new file mode 100644 index 000000000000..fc1b923c3d85 --- /dev/null +++ b/docs/nixos-setup.md @@ -0,0 +1,195 @@ +# Nix Setup Guide for Hermes Agent + +## Prerequisites + +- Nix with flakes enabled (we recommend [Determinate Nix](https://install.determinate.systems) which enables flakes by default) +- API keys for the services you want to use (at minimum: OpenRouter) + +## Quick Start: `nix run` + +```bash +nix run github:NousResearch/hermes-agent -- setup +nix run github:NousResearch/hermes-agent -- chat +``` + +No clone needed. Nix fetches and builds everything. + +## Install + +All Python dependencies are pre-built as Nix derivations via uv2nix — no runtime pip install. + +```bash +# From a clone +git clone --recurse-submodules https://github.com/NousResearch/hermes-agent.git +cd hermes-agent +nix build +./result/bin/hermes setup +./result/bin/hermes chat + +# Or install into your profile +nix profile install github:NousResearch/hermes-agent +hermes setup +``` + +## Development (`nix develop`) + +For hacking on hermes-agent locally: + +```bash +cd hermes-agent +nix develop +# Shell automatically: +# - Creates .venv with Python 3.11 +# - Installs all Python deps via uv +# - Installs npm deps (agent-browser) +# - Puts ripgrep, git, node on PATH + +hermes setup +hermes +``` + +### Using direnv (recommended) + +If you have [direnv](https://direnv.net/) installed, the included `.envrc` will +automatically activate the dev shell when you `cd` into the repo: + +```bash +cd hermes-agent +direnv allow # one-time approval + +# From now on, entering the directory activates the environment automatically. +# On repeat entry, the stamp file check skips dependency installation (~instant). +``` + +## Persistent Messaging Gateway + +To run hermes-agent as a **always-on service** for Telegram, Discord, or Slack (with built-in cron scheduler), use the [home-manager](https://github.com/nix-community/home-manager) module. This is how messaging platforms are meant to be run — the gateway stays up, receives messages, and responds. Works on any Linux distribution with Nix, not just NixOS. + +> This assumes you already have home-manager set up. If you don't, see the [home-manager docs](https://nix-community.github.io/home-manager/) first. + +### Step 1: Add the flake input + +```nix +# ~/.config/home-manager/flake.nix (or wherever your HM flake lives) +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager = { + url = "github:nix-community/home-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + hermes-agent.url = "github:NousResearch/hermes-agent"; + }; + + outputs = { nixpkgs, home-manager, hermes-agent, ... }: { + homeConfigurations."your-username" = home-manager.lib.homeManagerConfiguration { + pkgs = nixpkgs.legacyPackages.x86_64-linux; + modules = [ + hermes-agent.homeManagerModules.default + ./home.nix + ]; + }; + }; +} +``` + +### Step 2: Enable in home.nix + +```nix +# home.nix +{ + services.hermes-agent = { + enable = true; + gateway.enable = true; + + # All options with defaults: + # hermesHome = "~/.hermes"; # config, sessions, memories + # environmentFile = "~/.hermes/.env"; # API keys + # messagingCwd = "~"; # gateway working directory + # addToPATH = true; # adds `hermes` CLI to PATH + }; +} +``` + +### Step 3: API keys + +```bash +mkdir -p ~/.hermes +cat > ~/.hermes/.env << 'EOF' +OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxx +TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrSTUvwxYZ +TELEGRAM_ALLOWED_USERS=123456789 +EOF +chmod 600 ~/.hermes/.env +``` + +### Step 4: Enable linger + activate + +```bash +# Lets user services survive logout +sudo loginctl enable-linger $USER + +# Activate — all deps are pre-built, this is fast +home-manager switch +``` + +### Step 5: Verify + +```bash +systemctl --user status hermes-agent-gateway +journalctl --user -u hermes-agent-gateway -f +hermes doctor +``` + +## Directory layout + +``` +~/.hermes/ # Config & data +├── .env # API keys +├── config.yaml # Agent configuration +├── sessions/ # Messaging sessions +├── memories/ # Agent memories +├── skills/ # Knowledge documents +├── cron/ # Scheduled jobs +└── logs/ # Session logs +``` + +## Customizing + +```bash +$EDITOR ~/.hermes/config.yaml +``` + +Key settings: +- `model.default` — Which LLM to use (default: `anthropic/claude-opus-4.6`) +- `terminal.env_type` — Terminal backend: `local`, `docker`, `ssh` +- `toolsets` — Which tools to enable (default: all) + +After editing, restart the service: + +```bash +systemctl --user restart hermes-agent-gateway +``` + +## Updating + +```bash +nix flake update hermes-agent --flake ~/.config/home-manager +home-manager switch +``` + +## Troubleshooting + +```bash +# Gateway logs +journalctl --user -u hermes-agent-gateway -f + +# Check CLI + deps +hermes doctor + +# Restart gateway +systemctl --user restart hermes-agent-gateway + +# Full rebuild (if something is really wrong) +nix build --rebuild +``` diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000000..628e492f65fd --- /dev/null +++ b/flake.lock @@ -0,0 +1,181 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1772408722, + "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": "pyproject-nix", + "uv2nix": "uv2nix" + }, + "locked": { + "lastModified": 1772555609, + "narHash": "sha256-3BA3HnUvJSbHJAlJj6XSy0Jmu7RyP2gyB/0fL7XuEDo=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "c37f66a953535c394244888598947679af231863", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "pyproject-build-systems", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769936401, + "narHash": "sha256-kwCOegKLZJM9v/e/7cqwg1p/YjjTAukKPqmxKnAZRgA=", + "owner": "nix-community", + "repo": "pyproject.nix", + "rev": "b0d513eeeebed6d45b4f2e874f9afba2021f7812", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "pyproject.nix", + "type": "github" + } + }, + "pyproject-nix_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1772865871, + "narHash": "sha256-/ZTSg97aouL0SlPHaokA4r3iuH9QzHVuWPACD2CUCFY=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "e537db02e72d553cea470976b9733581bcf5b3ed", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "pyproject-nix_3": { + "inputs": { + "nixpkgs": [ + "uv2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1771518446, + "narHash": "sha256-nFJSfD89vWTu92KyuJWDoTQJuoDuddkJV3TlOl1cOic=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "eb204c6b3335698dec6c7fc1da0ebc3c6df05937", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix_2", + "uv2nix": "uv2nix_2" + } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "pyproject-build-systems", + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-build-systems", + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1770770348, + "narHash": "sha256-A2GzkmzdYvdgmMEu5yxW+xhossP+txrYb7RuzRaqhlg=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "5d1b2cb4fe3158043fbafbbe2e46238abbc954b0", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + }, + "uv2nix_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": "pyproject-nix_3" + }, + "locked": { + "lastModified": 1773039484, + "narHash": "sha256-+boo33KYkJDw9KItpeEXXv8+65f7hHv/earxpcyzQ0I=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "b68be7cfeacbed9a3fa38a2b5adc0cfb81d9bb1f", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000000..a08f9584ad4a --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + description = "Hermes Agent - AI agent framework by Nous Research"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + pyproject-nix = { + url = "github:pyproject-nix/pyproject.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + uv2nix = { + url = "github:pyproject-nix/uv2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + pyproject-build-systems = { + url = "github:pyproject-nix/build-system-pkgs"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ]; + + imports = [ + ./nix/packages.nix + ./nix/devShell.nix + ./nix/homeModules.nix + ]; + }; +} diff --git a/nix/devShell.nix b/nix/devShell.nix new file mode 100644 index 000000000000..7f8b5a1b0359 --- /dev/null +++ b/nix/devShell.nix @@ -0,0 +1,51 @@ +# nix/devShell.nix — Fast dev shell with stamp-file optimization +{ inputs, ... }: { + perSystem = { pkgs, ... }: + let + python = pkgs.python311; + in { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + python uv nodejs_20 ripgrep git openssh ffmpeg + ]; + + shellHook = '' + echo "Hermes Agent dev shell" + + # Composite stamp: changes when nix python or uv change + STAMP_VALUE="${python}:${pkgs.uv}" + STAMP_FILE=".venv/.nix-stamp" + + # Create venv if missing + if [ ! -d .venv ]; then + echo "Creating Python 3.11 venv..." + uv venv .venv --python ${python}/bin/python3 + fi + + source .venv/bin/activate + + # Only install if stamp is stale or missing + if [ ! -f "$STAMP_FILE" ] || [ "$(cat "$STAMP_FILE")" != "$STAMP_VALUE" ]; then + echo "Installing Python dependencies..." + uv pip install -e ".[all]" + if [ -d mini-swe-agent ]; then + uv pip install -e ./mini-swe-agent 2>/dev/null || true + fi + if [ -d tinker-atropos ]; then + uv pip install -e ./tinker-atropos 2>/dev/null || true + fi + + # Install npm deps + if [ -f package.json ] && [ ! -d node_modules ]; then + echo "Installing npm dependencies..." + npm install + fi + + echo "$STAMP_VALUE" > "$STAMP_FILE" + fi + + echo "Ready. Run 'hermes' to start." + ''; + }; + }; +} diff --git a/nix/homeModules.nix b/nix/homeModules.nix new file mode 100644 index 000000000000..88ae5c9adc3e --- /dev/null +++ b/nix/homeModules.nix @@ -0,0 +1,93 @@ +# nix/homeModules.nix — Home-manager module for hermes-agent +{ inputs, ... }: { + flake.homeManagerModules.default = { config, lib, pkgs, ... }: + let + cfg = config.services.hermes-agent; + in { + options.services.hermes-agent = { + enable = lib.mkEnableOption "Hermes Agent AI assistant"; + + package = lib.mkOption { + type = lib.types.package; + default = inputs.self.packages.${pkgs.system}.default; + defaultText = lib.literalExpression "hermes-agent.packages.\${pkgs.system}.default"; + description = "The hermes-agent package."; + }; + + hermesHome = lib.mkOption { + type = lib.types.str; + default = "${config.home.homeDirectory}/.hermes"; + description = "Hermes config/sessions/memories directory."; + }; + + environmentFile = lib.mkOption { + type = lib.types.str; + default = "${config.home.homeDirectory}/.hermes/.env"; + description = "Path to environment file containing API keys."; + }; + + messagingCwd = lib.mkOption { + type = lib.types.str; + default = config.home.homeDirectory; + description = "Working directory for gateway messaging."; + }; + + gateway = { + enable = lib.mkEnableOption "Hermes Agent messaging gateway"; + }; + + addToPATH = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Add hermes CLI to PATH via home.packages."; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = lib.mkIf cfg.addToPATH [ cfg.package ]; + + # Lightweight activation: just ensure directory structure exists + home.activation.hermesAgentSetup = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + mkdir -p "${cfg.hermesHome}"/{sessions,cron/output,logs,memories,skills} + + if [ ! -f "${cfg.hermesHome}/config.yaml" ]; then + cat > "${cfg.hermesHome}/config.yaml" << 'YAML' +_config_version: 1 +model: + default: "anthropic/claude-opus-4.6" +terminal: + env_type: "local" +YAML + fi + ''; + + # Gateway systemd service + systemd.user.services.hermes-agent-gateway = lib.mkIf cfg.gateway.enable { + Unit = { + Description = "Hermes Agent messaging gateway"; + After = [ "network-online.target" ]; + Wants = [ "network-online.target" ]; + StartLimitIntervalSec = 300; + StartLimitBurst = 10; + }; + + Service = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/hermes gateway run --replace"; + Restart = "on-failure"; + RestartSec = 15; + Environment = [ + "HOME=${config.home.homeDirectory}" + "HERMES_HOME=${cfg.hermesHome}" + "MESSAGING_CWD=${cfg.messagingCwd}" + ]; + EnvironmentFile = cfg.environmentFile; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + }; + }; +} diff --git a/nix/packages.nix b/nix/packages.nix new file mode 100644 index 000000000000..6e327477bcfc --- /dev/null +++ b/nix/packages.nix @@ -0,0 +1,67 @@ +# nix/packages.nix — Hermes Agent package built with uv2nix +{ inputs, ... }: { + perSystem = { pkgs, system, ... }: + let + hermesVenv = pkgs.callPackage ./python.nix { + inherit (inputs) uv2nix pyproject-nix pyproject-build-systems; + }; + + runtimeDeps = with pkgs; [ + nodejs_20 ripgrep git openssh ffmpeg + ]; + + runtimePath = pkgs.lib.makeBinPath runtimeDeps; + + # Submodules fetched declaratively (git deps break Nix sandbox) + mini-swe-agent-src = pkgs.fetchFromGitHub { + owner = "SWE-agent"; + repo = "mini-swe-agent"; + rev = "07aa6a738556e44b30d7b5c3bbd5063dac871d25"; + hash = "sha256-7+8dvi49iQMO4bXK5VYcem1+Tub5vMCrrZeNcEojAUQ="; + }; + + tinker-atropos-src = pkgs.fetchFromGitHub { + owner = "nousresearch"; + repo = "tinker-atropos"; + rev = "65f084ee8054a5d02aeac76e24ed60388511c82b"; + hash = "sha256-tD1VyUfMin+KnkQD+eyEibeJNe6d4dgB1b6wFe+3gKs="; + }; + in { + packages.default = pkgs.stdenv.mkDerivation { + pname = "hermes-agent"; + version = "0.1.0"; + + dontUnpack = true; + dontBuild = true; + nativeBuildInputs = [ pkgs.makeWrapper ]; + + installPhase = '' + runHook preInstall + + # Place submodule sources for runtime import + mkdir -p $out/share/hermes-agent + cp -r ${mini-swe-agent-src}/src/minisweagent $out/share/hermes-agent/minisweagent + cp -r ${tinker-atropos-src} $out/share/hermes-agent/tinker-atropos + + # Wrap entry points from the uv2nix venv + mkdir -p $out/bin + makeWrapper ${hermesVenv}/bin/hermes $out/bin/hermes \ + --prefix PATH : "${runtimePath}" \ + --prefix PYTHONPATH : $out/share/hermes-agent + + makeWrapper ${hermesVenv}/bin/hermes-agent $out/bin/hermes-agent \ + --prefix PATH : "${runtimePath}" \ + --prefix PYTHONPATH : $out/share/hermes-agent + + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "AI agent with advanced tool-calling capabilities"; + homepage = "https://github.com/NousResearch/hermes-agent"; + license = licenses.mit; + platforms = platforms.unix; + }; + }; + }; +} diff --git a/nix/python.nix b/nix/python.nix new file mode 100644 index 000000000000..406e7aee5347 --- /dev/null +++ b/nix/python.nix @@ -0,0 +1,28 @@ +# nix/python.nix — uv2nix virtual environment builder +{ + python311, + lib, + callPackage, + uv2nix, + pyproject-nix, + pyproject-build-systems, +}: +let + workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./..; }; + + overlay = workspace.mkPyprojectOverlay { + sourcePreference = "wheel"; + }; + + pythonSet = + (callPackage pyproject-nix.build.packages { + python = python311; + }).overrideScope + (lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + ]); +in +pythonSet.mkVirtualEnv "hermes-agent-env" { + hermes-agent = [ "all" ]; +}