feat: nix flake, uv2nix build, dev shell and home manager

This commit is contained in:
balyan.sid@gmail.com 2026-03-11 17:40:35 +05:30 • committed by alt-glitch
parent e5691eed38
commit 63b583aa2f
9 changed files with 654 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

4
.gitignore vendored
View file

@ -54,3 +54,7 @@ environments/benchmarks/evals/
# Release script temp files
.release_notes.md
mini-swe-agent/
# Nix
.direnv/
result

195
docs/nixos-setup.md Normal file
View file

@ -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
```

181
flake.lock generated Normal file
View file

@ -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
}

34
flake.nix Normal file
View file

@ -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
];
};
}

51
nix/devShell.nix Normal file
View file

@ -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."
'';
};
};
}

93
nix/homeModules.nix Normal file
View file

@ -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" ];
};
};
};
};
}

67
nix/packages.nix Normal file
View file

@ -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;
};
};
};
}

28
nix/python.nix Normal file
View file

@ -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" ];
}