diff --git a/.github/workflows/update-pins.yml b/.github/workflows/update-pins.yml new file mode 100644 index 0000000..c022445 --- /dev/null +++ b/.github/workflows/update-pins.yml @@ -0,0 +1,30 @@ +name: Update Hermes Agent Pins + +on: + schedule: + # Check every 6 hours — hermes-agent doesn't release as often as openclaw + - cron: "15 */6 * * *" + workflow_dispatch: {} + +permissions: + contents: write + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v13 + + - name: Run updater + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "nix-hermes-agent-bot" + git config user.email "bot@nix-hermes-agent.local" + scripts/update-pins.sh diff --git a/docs/CONFIG-MAP.md b/docs/CONFIG-MAP.md new file mode 100644 index 0000000..6454021 --- /dev/null +++ b/docs/CONFIG-MAP.md @@ -0,0 +1,335 @@ +# Hermes Agent Configuration Map + +**Source:** `hermes_cli/config.py` + `gateway/config.py` (v0.2.0) + +Everything below can be set declaratively via the NixOS module's `services.hermes-agent.config` option (rendered as `cli-config.yaml`), `environmentFiles` (secrets), or `environment` (non-secret env vars). + +--- + +## 1. Model & Provider + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `model` | string | `"anthropic/claude-opus-4.6"` | Default model (`provider/model` format) | +| `toolsets` | list | `["hermes-cli"]` | Enabled toolsets (e.g. `["all"]`, `["hermes-cli", "browser"]`) | + +**Auth providers** (resolved automatically based on env vars / OAuth): +- Nous Portal (OAuth) → `hermes login --provider nous` +- OpenAI Codex (OAuth) → `hermes login --provider openai-codex` +- OpenRouter → `OPENROUTER_API_KEY` +- Z.AI / GLM → `GLM_API_KEY` / `ZAI_API_KEY` +- Kimi → `KIMI_API_KEY` +- MiniMax → `MINIMAX_API_KEY` +- Anthropic → `ANTHROPIC_API_KEY` or Claude Code OAuth + +--- + +## 2. Agent Behavior + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `agent.max_turns` | int | `90` | Max tool-calling iterations per conversation | + +--- + +## 3. Terminal / Sandbox + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `terminal.backend` | string | `"local"` | `local` / `docker` / `singularity` / `modal` / `daytona` / `ssh` | +| `terminal.cwd` | string | `"."` | Working directory for commands | +| `terminal.timeout` | int | `180` | Command timeout (seconds) | +| `terminal.docker_image` | string | `"nikolaik/python-nodejs:..."` | Docker image | +| `terminal.singularity_image` | string | ... | Singularity image | +| `terminal.modal_image` | string | ... | Modal image | +| `terminal.daytona_image` | string | ... | Daytona image | +| `terminal.container_cpu` | int | `1` | Container CPU limit | +| `terminal.container_memory` | int | `5120` | Container memory (MB) | +| `terminal.container_disk` | int | `51200` | Container disk (MB) | +| `terminal.container_persistent` | bool | `true` | Persist filesystem across sessions | +| `terminal.docker_volumes` | list | `[]` | Docker volume mounts (`host:container`) | + +**SSH backend env vars:** +- `TERMINAL_SSH_HOST`, `TERMINAL_SSH_USER`, `TERMINAL_SSH_KEY` + +--- + +## 4. Context Compression + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `compression.enabled` | bool | `true` | Auto-compress long conversations | +| `compression.threshold` | float | `0.50` | Context usage % to trigger compression | +| `compression.summary_model` | string | `"google/gemini-3-flash-preview"` | Model for summaries | +| `compression.summary_provider` | string | `"auto"` | Provider for compression model | + +--- + +## 5. Auxiliary Models + +Side-task models (vision, web extraction, etc). Each has the same schema: + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `auxiliary..provider` | string | `"auto"` | Provider override | +| `auxiliary..model` | string | `""` | Model override | +| `auxiliary..base_url` | string | `""` | Direct endpoint URL | +| `auxiliary..api_key` | string | `""` | API key for endpoint | + +**Tasks:** `vision`, `web_extract`, `compression`, `session_search`, `skills_hub`, `mcp`, `flush_memories` + +--- + +## 6. Delegation (Subagents) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `delegation.model` | string | `""` | Model for subagents (empty = inherit parent) | +| `delegation.provider` | string | `""` | Provider for subagents | +| `delegation.base_url` | string | `""` | Direct endpoint for subagents | +| `delegation.api_key` | string | `""` | API key for delegation endpoint | + +--- + +## 7. Display + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `display.compact` | bool | `false` | Compact output mode | +| `display.personality` | string | `"kawaii"` | UI personality | +| `display.resume_display` | string | `"full"` | Session resume display | +| `display.bell_on_complete` | bool | `false` | Bell sound on completion | +| `display.show_reasoning` | bool | `false` | Show model reasoning | +| `display.skin` | string | `"default"` | UI skin | + +--- + +## 8. Memory + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `memory.memory_enabled` | bool | `true` | Persistent curated memory | +| `memory.user_profile_enabled` | bool | `true` | User profile memory | +| `memory.memory_char_limit` | int | `2200` | Max chars for memory context | +| `memory.user_char_limit` | int | `1375` | Max chars for user profile | + +--- + +## 9. Browser + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `browser.inactivity_timeout` | int | `120` | Browser auto-close (seconds) | +| `browser.record_sessions` | bool | `false` | Record browser sessions as WebM | + +--- + +## 10. Checkpoints (Filesystem Snapshots) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `checkpoints.enabled` | bool | `false` | Auto-snapshot before destructive ops | +| `checkpoints.max_snapshots` | int | `50` | Max checkpoints per directory | + +--- + +## 11. TTS (Text-to-Speech) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `tts.provider` | string | `"edge"` | `edge` (free) / `elevenlabs` (premium) / `openai` | +| `tts.edge.voice` | string | `"en-US-AriaNeural"` | Edge TTS voice | +| `tts.elevenlabs.voice_id` | string | `"pNInz6obpgDQGcFmaJgB"` | ElevenLabs voice ID | +| `tts.elevenlabs.model_id` | string | `"eleven_multilingual_v2"` | ElevenLabs model | +| `tts.openai.model` | string | `"gpt-4o-mini-tts"` | OpenAI TTS model | +| `tts.openai.voice` | string | `"alloy"` | OpenAI TTS voice | + +--- + +## 12. STT (Speech-to-Text) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `stt.enabled` | bool | `true` | Enable voice transcription | +| `stt.provider` | string | `"local"` | `local` (faster-whisper) / `groq` / `openai` | +| `stt.local.model` | string | `"base"` | Local model size: tiny/base/small/medium/large-v3 | +| `stt.openai.model` | string | `"whisper-1"` | OpenAI STT model | + +--- + +## 13. Voice (Interactive) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `voice.record_key` | string | `"ctrl+b"` | Push-to-talk key | +| `voice.max_recording_seconds` | int | `120` | Max recording length | +| `voice.auto_tts` | bool | `false` | Auto-speak responses | +| `voice.silence_threshold` | int | `200` | RMS silence threshold | +| `voice.silence_duration` | float | `3.0` | Seconds of silence → auto-stop | + +--- + +## 14. Human Delay (Anti-Detection) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `human_delay.mode` | string | `"off"` | `off` / `on` / `adaptive` | +| `human_delay.min_ms` | int | `800` | Min delay (ms) | +| `human_delay.max_ms` | int | `2500` | Max delay (ms) | + +--- + +## 15. Security + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `security.redact_secrets` | bool | `true` | Redact API keys from tool output | +| `security.tirith_enabled` | bool | `true` | Pre-exec scanning via tirith | +| `security.tirith_path` | string | `"tirith"` | Path to tirith binary | +| `security.tirith_timeout` | int | `5` | Tirith scan timeout (seconds) | +| `security.tirith_fail_open` | bool | `true` | Allow exec if tirith fails | + +--- + +## 16. Discord + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `discord.require_mention` | bool | `true` | Require @mention to respond in channels | +| `discord.free_response_channels` | string | `""` | Comma-separated channel IDs for free response | +| `discord.auto_thread` | bool | `true` | Auto-create threads on @mention | + +--- + +## 17. Session Reset Policy + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `session_reset.mode` | string | `"both"` | `daily` / `idle` / `both` / `none` | +| `session_reset.at_hour` | int | `4` | Daily reset hour (0-23, local time) | +| `session_reset.idle_minutes` | int | `1440` | Idle timeout before reset (minutes) | + +--- + +## 18. Miscellaneous + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `timezone` | string | `""` | IANA timezone (empty = server-local) | +| `command_allowlist` | list | `[]` | Permanently allowed dangerous commands | +| `quick_commands` | dict | `{}` | User-defined slash commands (exec type) | +| `personalities` | dict | `{}` | Custom personality prompts | +| `prefill_messages_file` | string | `""` | Path to JSON prefill messages | +| `honcho` | dict | `{}` | Honcho AI-native memory overrides | + +--- + +## 19. MCP Servers + +Defined via `services.hermes-agent.mcpServers` in the NixOS module: + +```nix +mcpServers = { + my-server = { + command = "npx"; + args = [ "-y" "my-mcp-server" ]; + env = { API_KEY = "..."; }; + timeout = 30; + }; +}; +``` + +Merged into `config.mcp_servers` in the rendered YAML. + +--- + +## 20. Fallback Model (Failover) + +Not in defaults — must be explicitly configured: + +```yaml +fallback_model: + provider: openrouter # openrouter | nous | openai-codex | zai | kimi-coding | minimax + model: anthropic/claude-sonnet-4 + # Optional for custom endpoints: + # base_url: "http://..." + # api_key_env: "MY_CUSTOM_KEY" +``` + +Triggers on: 429 (rate limit), 529 (overload), 503 (service error), connection failures. + +--- + +## 21. Environment Variables (Secrets via `environmentFiles`) + +### Provider Keys +| Variable | Provider | +|----------|----------| +| `OPENROUTER_API_KEY` | OpenRouter (default fallback) | +| `GLM_API_KEY` / `ZAI_API_KEY` | Z.AI / GLM | +| `KIMI_API_KEY` | Kimi / Moonshot | +| `MINIMAX_API_KEY` | MiniMax (international) | +| `MINIMAX_CN_API_KEY` | MiniMax (China) | +| `ANTHROPIC_API_KEY` | Anthropic direct | +| `ANTHROPIC_TOKEN` | Anthropic OAuth/setup token | + +### Tool Keys +| Variable | Tool | +|----------|------| +| `FIRECRAWL_API_KEY` | Web search/scraping | +| `BROWSERBASE_API_KEY` | Cloud browser | +| `FAL_KEY` | Image generation | +| `ELEVENLABS_API_KEY` | Premium TTS | +| `VOICE_TOOLS_OPENAI_KEY` | Whisper STT + OpenAI TTS | +| `GITHUB_TOKEN` | Skills Hub | +| `HONCHO_API_KEY` | AI-native memory | +| `TINKER_API_KEY` | RL training | +| `WANDB_API_KEY` | Experiment tracking | + +### Messaging Tokens +| Variable | Platform | +|----------|----------| +| `TELEGRAM_BOT_TOKEN` | Telegram | +| `TELEGRAM_ALLOWED_USERS` | Telegram user whitelist | +| `DISCORD_BOT_TOKEN` | Discord | +| `DISCORD_ALLOWED_USERS` | Discord user whitelist | +| `SLACK_BOT_TOKEN` | Slack | +| `SLACK_APP_TOKEN` | Slack (Socket Mode) | +| `SIGNAL_HTTP_URL` | Signal | +| `SIGNAL_ACCOUNT` | Signal | +| `HASS_TOKEN` | Home Assistant | +| `EMAIL_ADDRESS` / `EMAIL_PASSWORD` / `EMAIL_IMAP_HOST` / `EMAIL_SMTP_HOST` | Email | +| `GATEWAY_ALLOW_ALL_USERS` | Allow all users (global) | + +### Agent Settings +| Variable | Description | +|----------|-------------| +| `MESSAGING_CWD` | Working directory for messaging | +| `SUDO_PASSWORD` | Sudo password for terminal | +| `HERMES_MAX_ITERATIONS` | Max iterations override | + +--- + +## 22. Custom Providers (config.yaml) + +```yaml +custom_providers: + - name: "local-vllm" + base_url: "http://localhost:8000/v1" + api_key: "" # optional +``` + +Use with: `hermes chat --provider custom:local-vllm` + +--- + +## Summary: What Goes Where + +| What | Where | NixOS Module Option | +|------|-------|---------------------| +| Model, toolsets, display, terminal, etc. | `cli-config.yaml` | `config = { ... }` | +| API keys, tokens, passwords | `.env` / env vars | `environmentFiles = [ "/run/secrets/hermes.env" ]` | +| OAuth tokens (Nous, Codex) | `auth.json` | Runtime via `hermes login` (persistent) | +| MCP servers | `cli-config.yaml` | `mcpServers = { ... }` | +| SOUL.md, AGENTS.md, USER.md | Workspace files | `documents = { ... }` | +| Gateway platform config | env vars or `gateway.json` | `environmentFiles` | diff --git a/package.nix b/package.nix index af89874..2a56e01 100644 --- a/package.nix +++ b/package.nix @@ -72,14 +72,14 @@ let pythonImportsCheck = [ "acp" ]; }; - version = "0.2.0"; - rev = "64d333204bb2e32cc90a58b5ec5a4db127396dfc"; + version = "0.3.0"; + rev = "6ebb816e5611aaf1f3f7187ba8b10e985e899c75"; src = fetchFromGitHub { owner = "NousResearch"; repo = "hermes-agent"; inherit rev; - hash = "sha256-Li8jPEFDthj/AKmlwJhLWxItc34qcTrmJUDQ4kaSxVg="; + hash = "sha256-JGjusff/jGjvCCdUtl9IErBTGmpIq6BVA5Gj8mwqVYg="; fetchSubmodules = true; }; diff --git a/scripts/update-pins.sh b/scripts/update-pins.sh new file mode 100755 index 0000000..5aa06cc --- /dev/null +++ b/scripts/update-pins.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Auto-update nix-hermes-agent to track latest stable release from NousResearch/hermes-agent. +# Designed to run in GitHub Actions (see .github/workflows/update-pins.yml). +# Similar to nix-openclaw's update-pins.sh but tracks releases instead of HEAD. + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +package_file="$repo_root/package.nix" + +log() { + printf '>> %s\n' "$*" +} + +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required but not installed." >&2 + exit 1 +fi + +# --- Resolve latest stable release --- +log "Fetching latest release from NousResearch/hermes-agent" +release_json=$(gh api /repos/NousResearch/hermes-agent/releases/latest 2>/dev/null || true) +if [[ -z "$release_json" ]]; then + echo "Failed to fetch latest release" >&2 + exit 1 +fi + +release_tag=$(printf '%s' "$release_json" | jq -r '.tag_name // empty') +release_name=$(printf '%s' "$release_json" | jq -r '.name // empty') +if [[ -z "$release_tag" ]]; then + echo "No release tag found" >&2 + exit 1 +fi +log "Latest release: $release_tag ($release_name)" + +# Extract version from release name or tag (e.g. "Hermes Agent v0.3.0 (v2026.3.17)" → "0.3.0") +upstream_version=$(printf '%s' "$release_name" | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) +if [[ -z "$upstream_version" ]]; then + # Fallback: try tag itself + upstream_version=$(printf '%s' "$release_tag" | grep -oP 'v?\K[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) +fi +if [[ -z "$upstream_version" ]]; then + echo "Could not parse version from release tag=$release_tag name=$release_name" >&2 + exit 1 +fi +log "Parsed version: $upstream_version" + +# --- Compare with current --- +current_version=$(awk -F'"' '/^ version = /{print $2}' "$package_file" | head -1) +log "Current pinned version: $current_version" + +if [[ "$current_version" == "$upstream_version" ]]; then + log "Already up to date ($current_version). Nothing to do." + exit 0 +fi +log "Update available: $current_version → $upstream_version" + +# --- Resolve the commit SHA for the release tag --- +tag_sha=$(gh api "/repos/NousResearch/hermes-agent/git/ref/tags/${release_tag}" --jq '.object.sha' 2>/dev/null || true) +if [[ -z "$tag_sha" ]]; then + # Maybe it's an annotated tag — dereference + tag_sha=$(gh api "/repos/NousResearch/hermes-agent/git/ref/tags/${release_tag}" --jq '.object.sha' 2>/dev/null || true) + if [[ -n "$tag_sha" ]]; then + # Check if it's an annotated tag (type=tag) and dereference + tag_type=$(gh api "/repos/NousResearch/hermes-agent/git/tags/${tag_sha}" --jq '.object.type // empty' 2>/dev/null || true) + if [[ "$tag_type" == "commit" ]]; then + tag_sha=$(gh api "/repos/NousResearch/hermes-agent/git/tags/${tag_sha}" --jq '.object.sha' 2>/dev/null || true) + fi + fi +fi + +if [[ -z "$tag_sha" ]]; then + # Last resort: ls-remote + tag_sha=$(git ls-remote https://github.com/NousResearch/hermes-agent.git "refs/tags/${release_tag}" | awk '{print $1}' || true) +fi + +if [[ -z "$tag_sha" ]]; then + echo "Failed to resolve commit SHA for tag $release_tag" >&2 + exit 1 +fi +log "Release commit SHA: $tag_sha" + +# --- Prefetch source --- +source_url="https://github.com/NousResearch/hermes-agent/archive/${tag_sha}.tar.gz" +log "Prefetching source tarball (with submodules via fetchFromGitHub)..." + +# Use nix-prefetch-url for the base archive, but we need fetchFromGitHub hash (includes submodules). +# Best approach: temporarily update package.nix with empty hash and let nix build tell us the right one. +# Or use nix store prefetch-file for the tarball (no submodules). +# Since the package uses fetchSubmodules = true, we need the fetchFromGitHub hash. + +# Strategy: use nix to evaluate the hash by building with a fake hash +log "Computing fetchFromGitHub hash (with submodules)..." + +# Save original +cp "$package_file" "$package_file.bak" + +# Update version, rev, and set hash to empty +perl -0pi -e "s|version = \"[^\"]+\";|version = \"${upstream_version}\";|" "$package_file" +perl -0pi -e "s|rev = \"[^\"]+\";|rev = \"${tag_sha}\";|" "$package_file" +perl -0pi -e 's|hash = "sha256-[^"]+";|hash = "";|' "$package_file" + +# Build and capture the correct hash from the error +build_log=$(mktemp) +log "Running nix build to get correct hash..." +if nix build .#hermes-agent --accept-flake-config >"$build_log" 2>&1; then + log "Build succeeded with empty hash?! Unexpected, but OK." + source_hash="" +else + source_hash=$(grep -oP 'got: *\Ksha256-[A-Za-z0-9+/=]+' "$build_log" | head -1 || true) + if [[ -z "$source_hash" ]]; then + log "Build failed but couldn't extract hash. Build log:" + tail -50 "$build_log" >&2 + cp "$package_file.bak" "$package_file" + rm -f "$build_log" "$package_file.bak" + exit 1 + fi +fi +rm -f "$build_log" +log "Source hash: $source_hash" + +# Update with the correct hash +if [[ -n "$source_hash" ]]; then + perl -0pi -e "s|hash = \"[^\"]*\";|hash = \"${source_hash}\";|" "$package_file" +fi + +# --- Validate build --- +build_log=$(mktemp) +log "Validating full build..." +if ! nix build .#hermes-agent --accept-flake-config >"$build_log" 2>&1; then + log "Build validation FAILED. This likely means dependencies changed upstream." + log "Build log (last 100 lines):" + tail -100 "$build_log" >&2 + cp "$package_file.bak" "$package_file" + rm -f "$build_log" "$package_file.bak" + exit 1 +fi +rm -f "$build_log" "$package_file.bak" +log "Build validation PASSED ✅" + +# --- Commit and push --- +if git diff --quiet "$package_file"; then + log "No changes to commit (shouldn't happen)" + exit 0 +fi + +log "Committing update" +git add "$package_file" +git commit -m "🤖 bump hermes-agent ${current_version} → ${upstream_version} (${release_tag})" \ + -m "Upstream: https://github.com/NousResearch/hermes-agent/releases/tag/${release_tag}" \ + -m "Tests: nix build .#hermes-agent (passed)" + +log "Pushing to main" +git fetch origin main +git rebase origin/main +git push origin HEAD:main + +log "Done! Updated hermes-agent to ${upstream_version} (${release_tag})"