commit f70dc0f39b22f11c0b15bbfc34c022cefaf7a450 Author: Ciphercat <78522797+0xrsydn@users.noreply.github.com> Date: Sun Mar 15 23:49:39 2026 +0000 feat: initial nix package and NixOS module for hermes-agent v0.2.0 - buildPythonApplication with Python 3.12 - All extras included (messaging, cron, tts, voice, mcp, honcho, acp) - Custom PyPI packages: fal-client, honcho-ai, agent-client-protocol - NixOS module with systemd service for gateway mode - Runtime deps wrapped: nodejs 22, ripgrep, ffmpeg, git - Flake-based with overlay support diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0aabf8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +result +result-* +.direnv diff --git a/README.md b/README.md new file mode 100644 index 0000000..6520247 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# nix-hermes + +Nix package and NixOS module for [Hermes Agent](https://github.com/NousResearch/hermes-agent) by Nous Research. + +## Quick Start + +### Flake usage + +```nix +{ + inputs.nix-hermes.url = "github:0xrsydn/nix-hermes"; + + outputs = { self, nixpkgs, nix-hermes, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + nix-hermes.nixosModules.hermes-agent + { + services.hermes-agent = { + enable = true; + environmentFile = "/run/secrets/hermes-env"; # API keys + }; + } + ]; + }; + }; +} +``` + +### Just the package + +```bash +nix run github:0xrsydn/nix-hermes -- --help +``` + +### NixOS module options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enable` | bool | `false` | Enable the Hermes Agent gateway service | +| `user` | string | `"hermes"` | Service user | +| `group` | string | `"hermes"` | Service group | +| `homeDir` | path | `/var/lib/hermes` | State directory | +| `workDir` | path | `${homeDir}/workspace` | Working directory | +| `environmentFile` | path | `null` | Secrets file (API keys, tokens) | +| `extraEnvironment` | attrs | `{}` | Extra env vars | +| `extraArgs` | list | `[]` | Extra CLI args for `hermes gateway` | +| `extraPackages` | list | `[]` | Extra packages on PATH | + +### Environment file example + +```bash +ANTHROPIC_API_KEY=sk-ant-... +OPENROUTER_API_KEY=sk-or-... +TELEGRAM_TOKEN=123456:ABC... +OPENAI_API_KEY=sk-... +``` + +## What's included + +- **`hermes`** — Interactive CLI +- **`hermes-agent`** — Agent runner +- **`hermes-acp`** — ACP adapter +- **NixOS module** — systemd service for gateway mode +- Runtime deps wrapped: Node.js 22, ripgrep, ffmpeg, git + +## Development + +```bash +nix develop # Enter dev shell with hermes on PATH +nix build # Build the package +``` + +## License + +MIT (same as upstream) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..25ec39a --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1773389992, + "narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..deb3555 --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + description = "Nix package and NixOS module for Hermes Agent by Nous Research"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + packages = { + hermes-agent = pkgs.callPackage ./package.nix { }; + default = self.packages.${system}.hermes-agent; + }; + + devShells.default = pkgs.mkShell { + packages = [ self.packages.${system}.hermes-agent ]; + }; + } + ) // { + nixosModules = { + hermes-agent = import ./module.nix self; + default = self.nixosModules.hermes-agent; + }; + + overlays.default = final: prev: { + hermes-agent = final.callPackage ./package.nix { }; + }; + }; +} diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..c5a9e12 --- /dev/null +++ b/module.nix @@ -0,0 +1,125 @@ +self: +{ config, lib, pkgs, ... }: + +let + cfg = config.services.hermes-agent; + hermes-agent = self.packages.${pkgs.system}.hermes-agent; +in +{ + options.services.hermes-agent = { + enable = lib.mkEnableOption "Hermes Agent gateway service"; + + package = lib.mkOption { + type = lib.types.package; + default = hermes-agent; + description = "The hermes-agent package to use."; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "hermes"; + description = "User under which Hermes Agent runs."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "hermes"; + description = "Group under which Hermes Agent runs."; + }; + + homeDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/hermes"; + description = "Home directory for Hermes Agent state."; + }; + + workDir = lib.mkOption { + type = lib.types.path; + default = "${cfg.homeDir}/workspace"; + defaultText = lib.literalExpression ''"''${cfg.homeDir}/workspace"''; + description = "Working directory for Hermes Agent."; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to an environment file containing secrets (API keys, tokens). + This file should contain lines like: + ANTHROPIC_API_KEY=sk-... + TELEGRAM_TOKEN=... + OPENROUTER_API_KEY=... + ''; + }; + + extraEnvironment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Extra environment variables for the Hermes Agent service."; + example = lib.literalExpression '' + { + HERMES_DEFAULT_MODEL = "anthropic/claude-sonnet-4-20250514"; + } + ''; + }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Extra command-line arguments to pass to hermes gateway."; + }; + + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Extra packages to make available on PATH."; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.homeDir; + createHome = true; + description = "Hermes Agent service user"; + }; + + users.groups.${cfg.group} = { }; + + systemd.services.hermes-agent = { + description = "Hermes Agent Gateway"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + HOME = cfg.homeDir; + HERMES_HOME = "${cfg.homeDir}/.hermes"; + } // cfg.extraEnvironment; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.workDir; + ExecStart = lib.concatStringsSep " " ([ + "${cfg.package}/bin/hermes" + "gateway" + ] ++ cfg.extraArgs); + Restart = "on-failure"; + RestartSec = 5; + # Hardening + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = false; + ReadWritePaths = [ cfg.homeDir ]; + PrivateTmp = true; + } // lib.optionalAttrs (cfg.environmentFile != null) { + EnvironmentFile = cfg.environmentFile; + }; + + path = [ cfg.package ] ++ cfg.extraPackages; + }; + }; +} diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..cf3272e --- /dev/null +++ b/package.nix @@ -0,0 +1,170 @@ +{ lib +, python312Packages +, python312 +, fetchFromGitHub +, fetchPypi +, makeWrapper +, nodejs_22 +, ripgrep +, ffmpeg +, git +}: + +let + python = python312; + pythonPackages = python312Packages; + + # --- Missing PyPI packages --- + + fal-client = pythonPackages.buildPythonPackage rec { + pname = "fal-client"; + version = "0.13.1"; + pyproject = true; + src = fetchPypi { + pname = "fal_client"; + inherit version; + hash = "sha256-nhwH0KYbRSqP+0jBmd5fJUPXVG8SMPYxI3BEMSfF6Tc="; + }; + build-system = with pythonPackages; [ setuptools setuptools-scm ]; + dependencies = with pythonPackages; [ + httpx + httpx-sse + msgpack + websockets + ]; + doCheck = false; + pythonImportsCheck = [ "fal_client" ]; + }; + + honcho-ai = pythonPackages.buildPythonPackage rec { + pname = "honcho-ai"; + version = "2.0.1"; + pyproject = true; + src = fetchPypi { + pname = "honcho_ai"; + inherit version; + hash = "sha256-b97r+UVOYrxSPVeIjlA1nme6r9sh9oYh+cFOCNwAYjo="; + }; + build-system = with pythonPackages; [ setuptools wheel ]; + dependencies = with pythonPackages; [ + httpx + pydantic + typing-extensions + ]; + doCheck = false; + pythonImportsCheck = [ "honcho" ]; + }; + + agent-client-protocol = pythonPackages.buildPythonPackage rec { + pname = "agent-client-protocol"; + version = "0.8.1"; + pyproject = true; + src = fetchPypi { + pname = "agent_client_protocol"; + inherit version; + hash = "sha256-G78VZjv1H2SUJZf2OOMqYoTF2pGAVdlnLTUQ6WUUPb0="; + }; + build-system = [ pythonPackages.pdm-backend ]; + dependencies = with pythonPackages; [ + pydantic + ]; + doCheck = false; + pythonImportsCheck = [ "acp" ]; + }; + + version = "0.2.0"; + rev = "64d333204bb2e32cc90a58b5ec5a4db127396dfc"; + + src = fetchFromGitHub { + owner = "NousResearch"; + repo = "hermes-agent"; + inherit rev; + hash = "sha256-Li8jPEFDthj/AKmlwJhLWxItc34qcTrmJUDQ4kaSxVg="; + fetchSubmodules = true; + }; + +in +pythonPackages.buildPythonApplication { + pname = "hermes-agent"; + inherit version src; + pyproject = true; + + build-system = [ pythonPackages.setuptools ]; + + dependencies = with pythonPackages; [ + # Core + openai + anthropic + python-dotenv + fire + httpx + rich + tenacity + pyyaml + requests + jinja2 + pydantic + prompt-toolkit + # Tools + firecrawl-py + fal-client + # TTS + edge-tts + faster-whisper + # mini-swe-agent deps + litellm + typer + platformdirs + # Skills Hub + pyjwt + # Messaging + python-telegram-bot + discordpy + aiohttp + slack-bolt + slack-sdk + # Cron + croniter + # CLI + simple-term-menu + # TTS premium + elevenlabs + # Voice + sounddevice + numpy + # PTY + ptyprocess + # Honcho + honcho-ai + # MCP + mcp + # ACP + agent-client-protocol + ]; + + nativeBuildInputs = [ makeWrapper ]; + + # Don't run tests during build + doCheck = false; + + # mini-swe-agent is included via fetchSubmodules and referenced at runtime + + postFixup = '' + # Wrap binaries with runtime deps on PATH + for bin in $out/bin/hermes $out/bin/hermes-agent $out/bin/hermes-acp; do + if [ -f "$bin" ]; then + wrapProgram "$bin" \ + --prefix PATH : ${lib.makeBinPath [ nodejs_22 ripgrep ffmpeg git ]} + fi + done + ''; + + meta = with lib; { + description = "The self-improving AI agent by Nous Research"; + homepage = "https://github.com/NousResearch/hermes-agent"; + license = licenses.mit; + maintainers = [ ]; + mainProgram = "hermes"; + platforms = platforms.linux ++ platforms.darwin; + }; +}