Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
40 changes: 40 additions & 0 deletions .github/workflows/nix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Nix

on:
push:
branches: [main]
pull_request:
paths:
- 'flake.nix'
- 'flake.lock'
- 'nix/**'
- 'pyproject.toml'
- 'uv.lock'
- 'hermes_cli/**'
- 'run_agent.py'
- 'acp_adapter/**'

concurrency:
group: nix-${{ github.ref }}
cancel-in-progress: true

jobs:
nix:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Check flake
if: runner.os == 'Linux'
run: nix flake check --print-build-logs
- name: Build package
if: runner.os == 'Linux'
run: nix build --print-build-logs
- name: Evaluate flake (macOS)
if: runner.os == 'macOS'
run: nix flake show --json > /dev/null
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ environments/benchmarks/evals/
# Release script temp files
.release_notes.md
mini-swe-agent/

# Nix
.direnv/
result
181 changes: 181 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
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/nixosModules.nix
./nix/checks.nix
./nix/devShell.nix
];
};
}
38 changes: 38 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,32 @@
from hermes_cli.default_soul import DEFAULT_SOUL_MD


# =============================================================================
# Managed mode (NixOS declarative config)
# =============================================================================

def is_managed() -> bool:
"""Check if hermes is running in Nix-managed mode.

Two signals: the HERMES_MANAGED env var (set by the systemd service),
or a .managed marker file in HERMES_HOME (set by the NixOS activation
script, so interactive shells also see it).
"""
if os.getenv("HERMES_MANAGED", "").lower() in ("true", "1", "yes"):
return True
managed_marker = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))) / ".managed"
return managed_marker.exists()

def managed_error(action: str = "modify configuration"):
"""Print user-friendly error for managed mode."""
print(
f"Cannot {action}: configuration is managed by NixOS (HERMES_MANAGED=true).\n"
"Edit services.hermes-agent.settings in your configuration.nix and run:\n"
" sudo nixos-rebuild switch",
file=sys.stderr,
)


# =============================================================================
# Config paths
# =============================================================================
Expand Down Expand Up @@ -1340,6 +1366,9 @@ def load_config() -> Dict[str, Any]:

def save_config(config: Dict[str, Any]):
"""Save configuration to ~/.hermes/config.yaml."""
if is_managed():
managed_error("save configuration")
return
from utils import atomic_yaml_write

ensure_hermes_home()
Expand Down Expand Up @@ -1481,6 +1510,9 @@ def sanitize_env_file() -> int:

def save_env_value(key: str, value: str):
"""Save or update a value in ~/.hermes/.env."""
if is_managed():
managed_error(f"set {key}")
return
if not _ENV_VAR_NAME_RE.match(key):
raise ValueError(f"Invalid environment variable name: {key!r}")
value = value.replace("\n", "").replace("\r", "")
Expand Down Expand Up @@ -1737,6 +1769,9 @@ def show_config():

def edit_config():
"""Open config file in user's editor."""
if is_managed():
managed_error("edit configuration")
return
config_path = get_config_path()

# Ensure config exists
Expand Down Expand Up @@ -1766,6 +1801,9 @@ def edit_config():

def set_config_value(key: str, value: str):
"""Set a configuration value."""
if is_managed():
managed_error("set configuration values")
return
# Check if it's an API key (goes to .env)
api_keys = [
'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'VOICE_TOOLS_OPENAI_KEY',
Expand Down
11 changes: 10 additions & 1 deletion hermes_cli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

PROJECT_ROOT = Path(__file__).parent.parent.resolve()

from hermes_cli.config import get_env_value, get_hermes_home, save_env_value
from hermes_cli.config import get_env_value, get_hermes_home, save_env_value, is_managed, managed_error
from hermes_cli.setup import (
print_header, print_info, print_success, print_warning, print_error,
prompt, prompt_choice, prompt_yes_no,
Expand Down Expand Up @@ -1562,6 +1562,9 @@ def _setup_signal():

def gateway_setup():
"""Interactive setup for messaging platforms + gateway service."""
if is_managed():
managed_error("run gateway setup")
return

print()
print(color("┌─────────────────────────────────────────────────────────┐", Colors.MAGENTA))
Expand Down Expand Up @@ -1716,6 +1719,9 @@ def gateway_command(args):

# Service management commands
if subcmd == "install":
if is_managed():
managed_error("install gateway service (managed by NixOS)")
return
force = getattr(args, 'force', False)
system = getattr(args, 'system', False)
run_as_user = getattr(args, 'run_as_user', None)
Expand All @@ -1729,6 +1735,9 @@ def gateway_command(args):
sys.exit(1)

elif subcmd == "uninstall":
if is_managed():
managed_error("uninstall gateway service (managed by NixOS)")
return
system = getattr(args, 'system', False)
if is_linux():
systemd_uninstall(system=system)
Expand Down
4 changes: 4 additions & 0 deletions hermes_cli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3106,6 +3106,10 @@ def run_setup_wizard(args):
hermes setup tools — just tool configuration
hermes setup agent — just agent settings
"""
from hermes_cli.config import is_managed, managed_error
if is_managed():
managed_error("run setup wizard")
return
ensure_hermes_home()

config = load_config()
Expand Down
Loading
Loading