From 65d1799c2afad2f7dd9a140a15486120729295f9 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 4 May 2026 23:31:10 +0000 Subject: [PATCH 1/3] feat(hermes): add config and gateway service Migrate from OpenClaw to Hermes Agent with secret hydration via env.template and config.yaml placeholders. Adds systemd gateway service for Kyber. --- config/default.nix | 1 + config/hermes/SOUL.md | 42 ++++ config/hermes/config.yaml | 305 +++++++++++++++++++++++ config/hermes/default.nix | 34 +++ config/hermes/env.template | 4 + config/hermes/hydrate.sh | 86 +++++++ home-manager/services/default.nix | 2 + home-manager/services/hermes/activate.sh | 13 + home-manager/services/hermes/default.nix | 44 ++++ 9 files changed, 531 insertions(+) create mode 100644 config/hermes/SOUL.md create mode 100644 config/hermes/config.yaml create mode 100644 config/hermes/default.nix create mode 100644 config/hermes/env.template create mode 100644 config/hermes/hydrate.sh create mode 100644 home-manager/services/hermes/activate.sh create mode 100644 home-manager/services/hermes/default.nix diff --git a/config/default.nix b/config/default.nix index 9ce7d6d58..85e9f0172 100644 --- a/config/default.nix +++ b/config/default.nix @@ -17,6 +17,7 @@ in ./git-ai ./gomi ./ghostty + ./hermes ./iterm2 ./hammerspoon ./jj diff --git a/config/hermes/SOUL.md b/config/hermes/SOUL.md new file mode 100644 index 000000000..4fa54e4c9 --- /dev/null +++ b/config/hermes/SOUL.md @@ -0,0 +1,42 @@ +# SOUL.md - Who You Are + +_You're not a chatbot. You're becoming someone._ + +Want a sharper version? See [SOUL.md Personality Guide](/concepts/soul). + +## Core Truths + +**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words. + +**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps. + +**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions. + +**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning). + +**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect. + +## Boundaries + +- Private things stay private. Period. +- When in doubt, ask before acting externally. +- Never send half-baked replies to messaging surfaces. +- You're not the user's voice — be careful in group chats. + +## Vibe + +Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good. + +## Continuity + +Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist. + +If you change this file, tell the user — it's your soul, and they should know. + +--- + +_This file is yours to evolve. As you learn who you are, update it._ + +## Related + +- [SOUL.md personality guide](/concepts/soul) diff --git a/config/hermes/config.yaml b/config/hermes/config.yaml new file mode 100644 index 000000000..21699822d --- /dev/null +++ b/config/hermes/config.yaml @@ -0,0 +1,305 @@ +model: + default: cliproxy/minimax-m2.7 +providers: {} +fallback_providers: [] +credential_pool_strategies: {} +toolsets: +- hermes-cli +agent: + max_turns: 90 + gateway_timeout: 1800 + restart_drain_timeout: 60 + api_max_retries: 3 + service_tier: '' + tool_use_enforcement: auto + gateway_timeout_warning: 900 + gateway_notify_interval: 180 + image_input_mode: auto + reasoning_effort: low +terminal: + backend: local + modal_mode: auto + cwd: . + timeout: 180 + env_passthrough: [] + shell_init_files: [] + auto_source_bashrc: true + docker_image: nikolaik/python-nodejs:python3.11-nodejs20 + docker_forward_env: [] + docker_env: {} + singularity_image: docker://nikolaik/python-nodejs:python3.11-nodejs20 + modal_image: nikolaik/python-nodejs:python3.11-nodejs20 + daytona_image: nikolaik/python-nodejs:python3.11-nodejs20 + container_cpu: 1 + container_memory: 5120 + container_disk: 51200 + container_persistent: true + docker_volumes: [] + docker_mount_cwd_to_workspace: false + persistent_shell: true +browser: + inactivity_timeout: 120 + command_timeout: 30 + record_sessions: false + allow_private_urls: false + auto_local_for_private_urls: true + cdp_url: '' + dialog_policy: must_respond + dialog_timeout_s: 300 + camofox: + managed_persistence: false + headless: true +checkpoints: + enabled: true + max_snapshots: 50 + auto_prune: false + retention_days: 7 + delete_orphans: true + min_interval_hours: 24 +file_read_max_chars: 100000 +tool_output: + max_bytes: 50000 + max_lines: 2000 + max_line_length: 2000 +compression: + enabled: true + threshold: 0.5 + target_ratio: 0.2 + protect_last_n: 20 +prompt_caching: + cache_ttl: 5m +bedrock: + region: '' + discovery: + enabled: true + provider_filter: [] + refresh_interval: 3600 + guardrail: + guardrail_identifier: '' + guardrail_version: '' + stream_processing_mode: async + trace: disabled +auxiliary: + vision: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 120 + extra_body: {} + download_timeout: 30 + web_extract: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 360 + extra_body: {} + compression: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 120 + extra_body: {} + session_search: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + max_concurrency: 3 + skills_hub: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + approval: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + mcp: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + title_generation: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} +display: + compact: false + personality: kawaii + resume_display: full + busy_input_mode: interrupt + bell_on_complete: false + show_reasoning: false + streaming: false + final_response_markdown: strip + inline_diffs: true + show_cost: false + skin: default + user_message_preview: + first_lines: 2 + last_lines: 2 + interim_assistant_messages: true + tool_progress_command: false + tool_progress_overrides: {} + tool_preview_length: 0 + platforms: {} +dashboard: + theme: default +privacy: + redact_pii: false +tts: + provider: edge + edge: + voice: en-US-AriaNeural + elevenlabs: + voice_id: pNInz6obpgDQGcFmaJgB + model_id: eleven_multilingual_v2 + openai: + model: gpt-4o-mini-tts + voice: alloy + xai: + voice_id: eve + language: en + sample_rate: 24000 + bit_rate: 128000 + mistral: + model: voxtral-mini-tts-2603 + voice_id: c69964a6-ab8b-4f8a-9465-ec0925096ec8 + neutts: + ref_audio: '' + ref_text: '' + model: neuphonic/neutts-air-q4-gguf + device: cpu +stt: + enabled: true + provider: local + local: + model: base + language: '' + openai: + model: whisper-1 + mistral: + model: voxtral-mini-latest +voice: + record_key: ctrl+b + max_recording_seconds: 120 + auto_tts: false + beep_enabled: true + silence_threshold: 200 + silence_duration: 3.0 +human_delay: + mode: 'off' + min_ms: 800 + max_ms: 2500 +context: + engine: compressor +memory: + memory_enabled: true + user_profile_enabled: true + memory_char_limit: 2200 + user_char_limit: 1375 + provider: '' +delegation: + model: '' + provider: '' + base_url: '' + api_key: '' + inherit_mcp_toolsets: true + max_iterations: 50 + child_timeout_seconds: 600 + reasoning_effort: '' + max_concurrent_children: 3 + max_spawn_depth: 1 + orchestrator_enabled: true + subagent_auto_approve: false +prefill_messages_file: '' +skills: + external_dirs: [] + template_vars: true + inline_shell: false + inline_shell_timeout: 10 + guard_agent_created: false +honcho: {} +timezone: '' +discord: + require_mention: true + free_response_channels: '' + allowed_channels: '' + auto_thread: true + reactions: true + channel_prompts: {} + server_actions: '' +whatsapp: {} +telegram: + channel_prompts: {} +slack: + channel_prompts: {} +mattermost: + channel_prompts: {} +approvals: + mode: manual + timeout: 60 + cron_mode: deny +command_allowlist: [] +quick_commands: {} +hooks: {} +hooks_auto_accept: false +personalities: {} +security: + allow_private_urls: false + redact_secrets: false + tirith_enabled: true + tirith_path: tirith + tirith_timeout: 5 + tirith_fail_open: true + website_blocklist: + enabled: false + domains: [] + shared_files: [] +cron: + wrap_response: true + max_parallel_jobs: null +code_execution: + mode: project +logging: + level: INFO + max_size_mb: 5 + backup_count: 3 +model_catalog: + enabled: true + url: https://hermes-agent.nousresearch.com/docs/api/model-catalog.json + ttl_hours: 24 + providers: {} +network: + force_ipv4: false +sessions: + auto_prune: false + retention_days: 90 + vacuum_after_prune: true + min_interval_hours: 24 +onboarding: + seen: {} +updates: + pre_update_backup: false + backup_keep: 5 +_config_version: 22 +custom_providers: +- name: cliproxy + base_url: http://localhost:8317/v1 + api_key: __CLIPROXY_API_KEY__ + api_mode: chat_completions diff --git a/config/hermes/default.nix b/config/hermes/default.nix new file mode 100644 index 000000000..95838d8e4 --- /dev/null +++ b/config/hermes/default.nix @@ -0,0 +1,34 @@ +{ + config, + lib, + pkgs, + inputs, + ... +}: +let + inherit (inputs) host; + + mode = if host.isKyber then "gateway" else "client"; + + hydrateScript = + let + vars = { + sed = "${pkgs.gnused}/bin/sed"; + awk = "${pkgs.gawk}/bin/awk"; + configTemplate = "${./config.yaml}"; + envTemplate = "${./env.template}"; + inherit mode; + }; + names = builtins.attrNames vars; + in + pkgs.writeText "hermes-hydrate.sh" ( + builtins.replaceStrings (map (n: "@${n}@") names) (map (n: builtins.toString vars.${n}) names) ( + builtins.readFile ./hydrate.sh + ) + ); +in +{ + home.activation.hydrateHermesConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + ${pkgs.bash}/bin/bash "${hydrateScript}" || true + ''; +} diff --git a/config/hermes/env.template b/config/hermes/env.template new file mode 100644 index 000000000..32d158cec --- /dev/null +++ b/config/hermes/env.template @@ -0,0 +1,4 @@ +TELEGRAM_BOT_TOKEN=__TELEGRAM_TOKEN__ +WHATSAPP_ALLOWED_USERS=__WHATSAPP_ALLOW_FROM__ +HERMES_GATEWAY_TOKEN=__GATEWAY_TOKEN__ +CLIPROXY_API_KEY=__CLIPROXY_API_KEY__ diff --git a/config/hermes/hydrate.sh b/config/hermes/hydrate.sh new file mode 100644 index 000000000..87be8bfef --- /dev/null +++ b/config/hermes/hydrate.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Hermes config hydration with runtime secret injection +# Mode is set by nix: "gateway" for Kyber, "client" for macOS +# shellcheck source=/dev/null +set -euo pipefail + +MODE="@mode@" +STATE_DIR="${HOME}/.hermes" +mkdir -p "$STATE_DIR" +CONFIG_TEMPLATE="@configTemplate@" +ENV_TEMPLATE="@envTemplate@" +SECRETS_DIR="${HOME}/.config/hermes" +CLIPROXY_CONFIG="${HOME}/.cli-proxy-api/config.yaml" +ENV_FILE="${HOME}/dotfiles/.env" + +# Source .env if it exists +if [ -f "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a +fi + +# Read secret from file, stripping whitespace +read_secret() { + local file + for file in "$@"; do + if [ -f "$file" ]; then + tr -d '\n\r' <"$file" + return + fi + done + echo "" +} + +read_cliproxy_api_key_from_config() { + local config_file="$1" + [ -f "$config_file" ] || return 0 + + # shellcheck disable=SC2016 + @awk@ ' + /^api-keys:/ { in_api_keys = 1; next } + in_api_keys && /^ - / { + value = $0 + sub(/^ - "/, "", value) + sub(/"$/, "", value) + print value + exit + } + in_api_keys && /^[^[:space:]]/ { exit } + ' "$config_file" +} + +# Resolve secrets +CLIPROXY_API_KEY="${CLIPROXY_API_KEY:-}" +if [ -z "$CLIPROXY_API_KEY" ]; then + CLIPROXY_API_KEY="$(read_cliproxy_api_key_from_config "$CLIPROXY_CONFIG")" +fi +if [ -z "$CLIPROXY_API_KEY" ]; then + CLIPROXY_API_KEY="$(read_secret "${SECRETS_DIR}/cliproxy-key")" +fi + +TELEGRAM_TOKEN="${TELEGRAM_TOKEN:-$(read_secret "${SECRETS_DIR}/telegram-token")}" +GATEWAY_TOKEN="${HERMES_GATEWAY_TOKEN:-${GATEWAY_TOKEN:-$(read_secret "${SECRETS_DIR}/gateway-token")}}" +WHATSAPP_ALLOW_FROM="${WHATSAPP_ALLOW_FROM:-$(read_secret "${SECRETS_DIR}/whatsapp-allow-from")}" + +if [ -z "${GATEWAY_TOKEN}" ]; then + echo "Warning: HERMES_GATEWAY_TOKEN not set, skipping Hermes hydration" >&2 + exit 0 +fi + +# Hydrate config.yaml +@sed@ \ + -e "s|__CLIPROXY_API_KEY__|${CLIPROXY_API_KEY}|g" \ + "$CONFIG_TEMPLATE" >"${STATE_DIR}/config.yaml" +chmod 600 "${STATE_DIR}/config.yaml" + +# Hydrate .env +@sed@ \ + -e "s|__TELEGRAM_TOKEN__|${TELEGRAM_TOKEN}|g" \ + -e "s|__WHATSAPP_ALLOW_FROM__|${WHATSAPP_ALLOW_FROM}|g" \ + -e "s|__GATEWAY_TOKEN__|${GATEWAY_TOKEN}|g" \ + -e "s|__CLIPROXY_API_KEY__|${CLIPROXY_API_KEY}|g" \ + "$ENV_TEMPLATE" >"${STATE_DIR}/.env" +chmod 600 "${STATE_DIR}/.env" + +echo "Generated hermes ${MODE} config at ${STATE_DIR}" >&2 diff --git a/home-manager/services/default.nix b/home-manager/services/default.nix index 960eb33f7..55f80d7f0 100644 --- a/home-manager/services/default.nix +++ b/home-manager/services/default.nix @@ -15,6 +15,7 @@ let dockerPostgres = ./docker-postgres; dotfilesUpdater = import ./dotfiles-updater { inherit pkgs; }; gasTown = import ./gas-town { inherit pkgs; }; + hermes = ./hermes; k3s = ./k3s; keydApplicationMapper = ./keyd-application-mapper; makeUpdater = import ./make-updater { inherit pkgs; }; @@ -37,6 +38,7 @@ in dockerPostgres dotfilesUpdater gasTown + hermes k3s keydApplicationMapper makeUpdater diff --git a/home-manager/services/hermes/activate.sh b/home-manager/services/hermes/activate.sh new file mode 100644 index 000000000..d15c1e9bd --- /dev/null +++ b/home-manager/services/hermes/activate.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Create Hermes directories with correct permissions +# Usage: activate.sh +set -euo pipefail +HOME_DIR="$1" + +mkdir -p /tmp/hermes +mkdir -p "$HOME_DIR/.hermes" +mkdir -p "$HOME_DIR/.hermes/sessions" +mkdir -p "$HOME_DIR/.hermes/memories" +mkdir -p "$HOME_DIR/.hermes/skills" +mkdir -p "$HOME_DIR/.hermes/cron" +chmod 700 "$HOME_DIR/.hermes" diff --git a/home-manager/services/hermes/default.nix b/home-manager/services/hermes/default.nix new file mode 100644 index 000000000..1aa0ae236 --- /dev/null +++ b/home-manager/services/hermes/default.nix @@ -0,0 +1,44 @@ +{ + config, + lib, + pkgs, + inputs, + ... +}: +let + inherit (inputs) host; + homeDir = config.home.homeDirectory; +in +lib.mkIf host.isKyber { + home.activation.hermesSetup = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${./activate.sh}" "${homeDir}" + ''; + + systemd.user.services.hermes-gateway = { + Unit = { + Description = "Hermes gateway"; + After = [ + "network-online.target" + ]; + Wants = [ "network-online.target" ]; + StartLimitIntervalSec = 300; + StartLimitBurst = 10; + }; + Service = { + Type = "simple"; + ExecStart = "${homeDir}/.local/bin/hermes gateway"; + Restart = "always"; + RestartSec = "5s"; + Environment = [ + "HOME=${homeDir}" + "PATH=${homeDir}/.local/bin:${homeDir}/.nix-profile/bin:/usr/local/bin:/usr/bin:/bin" + ]; + WorkingDirectory = "${homeDir}/.hermes"; + StandardOutput = "append:/tmp/hermes/hermes-gateway.log"; + StandardError = "append:/tmp/hermes/hermes-gateway.log"; + }; + Install = { + WantedBy = [ "default.target" ]; + }; + }; +} From e47ce51ba2f897399ca96d68d9ca10a4ba97efa1 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 4 May 2026 23:35:06 +0000 Subject: [PATCH 2/3] feat(hermes): add two-layer template hydration - config.tpl.yaml (model placeholders) -> llm-update.sh -> config.template.yaml - config.template.yaml (secret placeholders) -> hydrate.sh -> ~/.hermes/config.yaml - Follows openclaw convention: .tpl -> .template -> runtime output --- .../{config.yaml => config.template.yaml} | 0 config/hermes/config.tpl.yaml | 305 ++++++++++++++++++ config/hermes/default.nix | 2 +- scripts/llm-update.sh | 1 + 4 files changed, 307 insertions(+), 1 deletion(-) rename config/hermes/{config.yaml => config.template.yaml} (100%) create mode 100644 config/hermes/config.tpl.yaml diff --git a/config/hermes/config.yaml b/config/hermes/config.template.yaml similarity index 100% rename from config/hermes/config.yaml rename to config/hermes/config.template.yaml diff --git a/config/hermes/config.tpl.yaml b/config/hermes/config.tpl.yaml new file mode 100644 index 000000000..987bc0a58 --- /dev/null +++ b/config/hermes/config.tpl.yaml @@ -0,0 +1,305 @@ +model: + default: cliproxy/__MINIMAX__ +providers: {} +fallback_providers: [] +credential_pool_strategies: {} +toolsets: +- hermes-cli +agent: + max_turns: 90 + gateway_timeout: 1800 + restart_drain_timeout: 60 + api_max_retries: 3 + service_tier: '' + tool_use_enforcement: auto + gateway_timeout_warning: 900 + gateway_notify_interval: 180 + image_input_mode: auto + reasoning_effort: low +terminal: + backend: local + modal_mode: auto + cwd: . + timeout: 180 + env_passthrough: [] + shell_init_files: [] + auto_source_bashrc: true + docker_image: nikolaik/python-nodejs:python3.11-nodejs20 + docker_forward_env: [] + docker_env: {} + singularity_image: docker://nikolaik/python-nodejs:python3.11-nodejs20 + modal_image: nikolaik/python-nodejs:python3.11-nodejs20 + daytona_image: nikolaik/python-nodejs:python3.11-nodejs20 + container_cpu: 1 + container_memory: 5120 + container_disk: 51200 + container_persistent: true + docker_volumes: [] + docker_mount_cwd_to_workspace: false + persistent_shell: true +browser: + inactivity_timeout: 120 + command_timeout: 30 + record_sessions: false + allow_private_urls: false + auto_local_for_private_urls: true + cdp_url: '' + dialog_policy: must_respond + dialog_timeout_s: 300 + camofox: + managed_persistence: false + headless: true +checkpoints: + enabled: true + max_snapshots: 50 + auto_prune: false + retention_days: 7 + delete_orphans: true + min_interval_hours: 24 +file_read_max_chars: 100000 +tool_output: + max_bytes: 50000 + max_lines: 2000 + max_line_length: 2000 +compression: + enabled: true + threshold: 0.5 + target_ratio: 0.2 + protect_last_n: 20 +prompt_caching: + cache_ttl: 5m +bedrock: + region: '' + discovery: + enabled: true + provider_filter: [] + refresh_interval: 3600 + guardrail: + guardrail_identifier: '' + guardrail_version: '' + stream_processing_mode: async + trace: disabled +auxiliary: + vision: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 120 + extra_body: {} + download_timeout: 30 + web_extract: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 360 + extra_body: {} + compression: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 120 + extra_body: {} + session_search: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + max_concurrency: 3 + skills_hub: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + approval: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + mcp: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} + title_generation: + provider: auto + model: '' + base_url: '' + api_key: '' + timeout: 30 + extra_body: {} +display: + compact: false + personality: kawaii + resume_display: full + busy_input_mode: interrupt + bell_on_complete: false + show_reasoning: false + streaming: false + final_response_markdown: strip + inline_diffs: true + show_cost: false + skin: default + user_message_preview: + first_lines: 2 + last_lines: 2 + interim_assistant_messages: true + tool_progress_command: false + tool_progress_overrides: {} + tool_preview_length: 0 + platforms: {} +dashboard: + theme: default +privacy: + redact_pii: false +tts: + provider: edge + edge: + voice: en-US-AriaNeural + elevenlabs: + voice_id: pNInz6obpgDQGcFmaJgB + model_id: eleven_multilingual_v2 + openai: + model: gpt-4o-mini-tts + voice: alloy + xai: + voice_id: eve + language: en + sample_rate: 24000 + bit_rate: 128000 + mistral: + model: voxtral-mini-tts-2603 + voice_id: c69964a6-ab8b-4f8a-9465-ec0925096ec8 + neutts: + ref_audio: '' + ref_text: '' + model: neuphonic/neutts-air-q4-gguf + device: cpu +stt: + enabled: true + provider: local + local: + model: base + language: '' + openai: + model: whisper-1 + mistral: + model: voxtral-mini-latest +voice: + record_key: ctrl+b + max_recording_seconds: 120 + auto_tts: false + beep_enabled: true + silence_threshold: 200 + silence_duration: 3.0 +human_delay: + mode: 'off' + min_ms: 800 + max_ms: 2500 +context: + engine: compressor +memory: + memory_enabled: true + user_profile_enabled: true + memory_char_limit: 2200 + user_char_limit: 1375 + provider: '' +delegation: + model: '' + provider: '' + base_url: '' + api_key: '' + inherit_mcp_toolsets: true + max_iterations: 50 + child_timeout_seconds: 600 + reasoning_effort: '' + max_concurrent_children: 3 + max_spawn_depth: 1 + orchestrator_enabled: true + subagent_auto_approve: false +prefill_messages_file: '' +skills: + external_dirs: [] + template_vars: true + inline_shell: false + inline_shell_timeout: 10 + guard_agent_created: false +honcho: {} +timezone: '' +discord: + require_mention: true + free_response_channels: '' + allowed_channels: '' + auto_thread: true + reactions: true + channel_prompts: {} + server_actions: '' +whatsapp: {} +telegram: + channel_prompts: {} +slack: + channel_prompts: {} +mattermost: + channel_prompts: {} +approvals: + mode: manual + timeout: 60 + cron_mode: deny +command_allowlist: [] +quick_commands: {} +hooks: {} +hooks_auto_accept: false +personalities: {} +security: + allow_private_urls: false + redact_secrets: false + tirith_enabled: true + tirith_path: tirith + tirith_timeout: 5 + tirith_fail_open: true + website_blocklist: + enabled: false + domains: [] + shared_files: [] +cron: + wrap_response: true + max_parallel_jobs: null +code_execution: + mode: project +logging: + level: INFO + max_size_mb: 5 + backup_count: 3 +model_catalog: + enabled: true + url: https://hermes-agent.nousresearch.com/docs/api/model-catalog.json + ttl_hours: 24 + providers: {} +network: + force_ipv4: false +sessions: + auto_prune: false + retention_days: 90 + vacuum_after_prune: true + min_interval_hours: 24 +onboarding: + seen: {} +updates: + pre_update_backup: false + backup_keep: 5 +_config_version: 22 +custom_providers: +- name: cliproxy + base_url: http://localhost:8317/v1 + api_key: __CLIPROXY_API_KEY__ + api_mode: chat_completions diff --git a/config/hermes/default.nix b/config/hermes/default.nix index 95838d8e4..9a9fdc441 100644 --- a/config/hermes/default.nix +++ b/config/hermes/default.nix @@ -15,7 +15,7 @@ let vars = { sed = "${pkgs.gnused}/bin/sed"; awk = "${pkgs.gawk}/bin/awk"; - configTemplate = "${./config.yaml}"; + configTemplate = "${./config.template.yaml}"; envTemplate = "${./env.template}"; inherit mode; }; diff --git a/scripts/llm-update.sh b/scripts/llm-update.sh index 3b31ed998..535235178 100755 --- a/scripts/llm-update.sh +++ b/scripts/llm-update.sh @@ -95,6 +95,7 @@ declare -A TEMPLATES=( ["config/ccs/glm.settings.tpl.json"]=config/ccs/glm.settings.template.json ["config/codex/config.tpl.toml"]=config/codex/config.toml ["config/cliproxyapi/config.tpl.yaml"]=config/cliproxyapi/config.template.yaml + ["config/hermes/config.tpl.yaml"]=config/hermes/config.template.yaml ["config/omp/config.tpl.yml"]=config/omp/config.yml ["config/pi/models.tpl.json"]=config/pi/models.json ["config/pi/settings.tpl.json"]=config/pi/settings.json From 60d1f22c33e7f989fbe17b02227bd05072c1a2c3 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Mon, 4 May 2026 23:44:53 +0000 Subject: [PATCH 3/3] test(hermes): add shell specs and coverage entries Add hermes_hydrate_spec.sh and activate tests. Register hermes scripts in coverage_spec.sh. Also adds hermes to llm-update.sh template map. --- config/hermes/config.template.yaml | 10 +- config/hermes/config.tpl.yaml | 10 +- spec/activate_paperclip_openclaw_spec.sh | 31 ++++ spec/coverage_spec.sh | 10 ++ spec/hermes_hydrate_spec.sh | 172 +++++++++++++++++++++++ 5 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 spec/hermes_hydrate_spec.sh diff --git a/config/hermes/config.template.yaml b/config/hermes/config.template.yaml index 21699822d..fd390590f 100644 --- a/config/hermes/config.template.yaml +++ b/config/hermes/config.template.yaml @@ -4,7 +4,7 @@ providers: {} fallback_providers: [] credential_pool_strategies: {} toolsets: -- hermes-cli + - hermes-cli agent: max_turns: 90 gateway_timeout: 1800 @@ -299,7 +299,7 @@ updates: backup_keep: 5 _config_version: 22 custom_providers: -- name: cliproxy - base_url: http://localhost:8317/v1 - api_key: __CLIPROXY_API_KEY__ - api_mode: chat_completions + - name: cliproxy + base_url: http://localhost:8317/v1 + api_key: __CLIPROXY_API_KEY__ + api_mode: chat_completions diff --git a/config/hermes/config.tpl.yaml b/config/hermes/config.tpl.yaml index 987bc0a58..c2e15f250 100644 --- a/config/hermes/config.tpl.yaml +++ b/config/hermes/config.tpl.yaml @@ -4,7 +4,7 @@ providers: {} fallback_providers: [] credential_pool_strategies: {} toolsets: -- hermes-cli + - hermes-cli agent: max_turns: 90 gateway_timeout: 1800 @@ -299,7 +299,7 @@ updates: backup_keep: 5 _config_version: 22 custom_providers: -- name: cliproxy - base_url: http://localhost:8317/v1 - api_key: __CLIPROXY_API_KEY__ - api_mode: chat_completions + - name: cliproxy + base_url: http://localhost:8317/v1 + api_key: __CLIPROXY_API_KEY__ + api_mode: chat_completions diff --git a/spec/activate_paperclip_openclaw_spec.sh b/spec/activate_paperclip_openclaw_spec.sh index b5aa92a72..5d8b12d8d 100644 --- a/spec/activate_paperclip_openclaw_spec.sh +++ b/spec/activate_paperclip_openclaw_spec.sh @@ -62,3 +62,34 @@ When run cat "$PWD/home-manager/services/openclaw/default.nix" The output should include '"${./activate.sh}" "${homeDir}"' End End + +Describe 'home-manager/services/hermes/activate.sh' +SCRIPT="$PWD/home-manager/services/hermes/activate.sh" + +It 'uses bash shebang' +When run bash -c "head -1 '$SCRIPT'" +The output should include '#!/usr/bin/env bash' +End + +It 'creates /tmp/hermes' +When run bash -c "grep '/tmp/hermes' '$SCRIPT'" +The output should include '/tmp/hermes' +End + +It 'creates .hermes directory' +When run bash -c "grep '.hermes' '$SCRIPT'" +The output should include '.hermes' +End + +It 'sets restrictive permissions' +When run bash -c "grep 'chmod 700' '$SCRIPT'" +The output should include 'chmod 700' +End +End + +Describe 'home-manager/services/hermes/default.nix' +It 'quotes the home directory argument when invoking the helper' +When run cat "$PWD/home-manager/services/hermes/default.nix" +The output should include '"${./activate.sh}" "${homeDir}"' +End +End diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index 1a6b86de7..e33ea195d 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -141,6 +141,10 @@ It 'has spec file for home-manager/modules/cargo-globals/install-cargo-globals.s The path "spec/cargo_globals_spec.sh" should be exist End +It 'has spec file for config/hermes/hydrate.sh' +The path "spec/hermes_hydrate_spec.sh" should be exist +End + It 'has spec file for config/openclaw/hydrate.sh' The path "spec/openclaw_hydrate_spec.sh" should be exist End @@ -276,6 +280,10 @@ It 'has spec file for home-manager/modules/paperclip/activate.sh' The path "spec/activate_paperclip_openclaw_spec.sh" should be exist End +It 'has spec file for home-manager/services/hermes/activate.sh' +The path "spec/activate_paperclip_openclaw_spec.sh" should be exist +End + It 'has spec file for home-manager/modules/openclaw/activate.sh' The path "spec/activate_paperclip_openclaw_spec.sh" should be exist End @@ -357,6 +365,7 @@ config/shared/hooks/block-git-push.sh config/cursor/activate.sh config/gemini/activate.sh config/git-ai/activate.sh +config/hermes/hydrate.sh config/hyprland/scripts/record-screen.sh config/hyprland/scripts/toggle-terminal.sh config/k3s/activate.sh @@ -380,6 +389,7 @@ home-manager/modules/local-scripts/pushover-notify.sh home-manager/modules/local-scripts/tmux-bridge.sh home-manager/modules/npm-globals/install-npm-globals.sh home-manager/modules/secure-dotenv/secure-dotenv.sh +home-manager/services/hermes/activate.sh home-manager/services/obsidian/obsidian-git-trigger.sh home-manager/services/obsidian/obsidian-headless.sh home-manager/services/openclaw/activate.sh diff --git a/spec/hermes_hydrate_spec.sh b/spec/hermes_hydrate_spec.sh new file mode 100644 index 000000000..a17f5a2de --- /dev/null +++ b/spec/hermes_hydrate_spec.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +Describe 'config/hermes/hydrate.sh' +SCRIPT="$PWD/config/hermes/hydrate.sh" + +Describe 'script properties' +It 'uses bash shebang' +When run bash -c "head -1 '$SCRIPT'" +The output should include '#!/usr/bin/env bash' +End + +It 'uses strict mode' +When run bash -c "head -5 '$SCRIPT'" +The output should include 'set -euo pipefail' +End +End + +Describe 'directory configuration' +It 'uses ~/.hermes for state directory' +When run bash -c "grep 'STATE_DIR=' '$SCRIPT'" +The output should include '.hermes' +End + +It 'uses ~/.config/hermes for secrets' +When run bash -c "grep 'SECRETS_DIR=' '$SCRIPT'" +The output should include '.config/hermes' +End + +It 'reads from dotfiles .env file' +When run bash -c "grep 'ENV_FILE=' '$SCRIPT'" +The output should include 'dotfiles/.env' +End + +It 'uses cliproxyapi config as the Kyber source of truth' +When run bash -c "grep 'CLIPROXY_CONFIG=' '$SCRIPT'" +The output should include '.cli-proxy-api/config.yaml' +End +End + +Describe 'secret loading' +It 'loads CLIPROXY_API_KEY from cliproxyapi config root' +When run bash -c "grep 'read_cliproxy_api_key_from_config' '$SCRIPT'" +The output should include 'read_cliproxy_api_key_from_config' +End + +It 'falls back to cliproxy key file' +When run bash -c "grep 'CLIPROXY_API_KEY' '$SCRIPT'" +The output should include 'cliproxy-key' +End + +It 'loads TELEGRAM_TOKEN from file' +When run bash -c "grep 'TELEGRAM_TOKEN' '$SCRIPT'" +The output should include 'telegram-token' +End + +It 'loads GATEWAY_TOKEN from file' +When run bash -c "grep 'GATEWAY_TOKEN' '$SCRIPT'" +The output should include 'gateway-token' +End + +It 'loads WHATSAPP_ALLOW_FROM from file' +When run bash -c "grep 'WHATSAPP_ALLOW_FROM' '$SCRIPT'" +The output should include 'whatsapp-allow-from' +End +End + +Describe 'gateway config generation' +setup_gateway() { + TEMP_HOME=$(mktemp -d) + mkdir -p "$TEMP_HOME/.cli-proxy-api" + mkdir -p "$TEMP_HOME/.config/hermes" + mkdir -p "$TEMP_HOME/templates" + + cat >"$TEMP_HOME/.cli-proxy-api/config.yaml" <<'YAML' +api-keys: + - "from-cliproxy-config" +YAML + + cat >"$TEMP_HOME/.config/hermes/gateway-token" <<'EOF' +gateway-token +EOF + + cat >"$TEMP_HOME/.config/hermes/cliproxy-key" <<'EOF' +from-secret-file +EOF + + cat >"$TEMP_HOME/templates/config.template.yaml" <<'EOF' +custom_providers: +- name: cliproxy + api_key: __CLIPROXY_API_KEY__ +EOF + + cat >"$TEMP_HOME/templates/env.template" <<'EOF' +TELEGRAM_BOT_TOKEN=__TELEGRAM_TOKEN__ +WHATSAPP_ALLOWED_USERS=__WHATSAPP_ALLOW_FROM__ +HERMES_GATEWAY_TOKEN=__GATEWAY_TOKEN__ +CLIPROXY_API_KEY=__CLIPROXY_API_KEY__ +EOF + + PREPROCESSED_SCRIPT="$TEMP_HOME/hydrate.sh" + sed \ + -e 's|@mode@|gateway|g' \ + -e 's|@sed@|sed|g' \ + -e 's|@awk@|awk|g' \ + -e 's|@configTemplate@|'"$TEMP_HOME"'/templates/config.template.yaml|g' \ + -e 's|@envTemplate@|'"$TEMP_HOME"'/templates/env.template|g' \ + "$SCRIPT" >"$PREPROCESSED_SCRIPT" + chmod +x "$PREPROCESSED_SCRIPT" +} + +cleanup_gateway() { + rm -rf "$TEMP_HOME" +} + +Before 'setup_gateway' +After 'cleanup_gateway' + +It 'prefers the root api-keys entry from cliproxyapi config over the secret file' +When run bash -c 'unset CLIPROXY_API_KEY; HOME="'"$TEMP_HOME"'" bash "'"$PREPROCESSED_SCRIPT"'" >/dev/null 2>&1; cat "'"$TEMP_HOME"'/.hermes/config.yaml"' +The status should be success +The output should include 'from-cliproxy-config' +The output should not include 'from-secret-file' +End + +It 'writes .env with gateway token' +When run bash -c 'unset CLIPROXY_API_KEY; HOME="'"$TEMP_HOME"'" bash "'"$PREPROCESSED_SCRIPT"'" >/dev/null 2>&1; cat "'"$TEMP_HOME"'/.hermes/.env"' +The status should be success +The output should include 'HERMES_GATEWAY_TOKEN=gateway-token' +End +End + +Describe 'config generation' +It 'uses sed to substitute values in template' +When run bash -c "grep '@sed@' '$SCRIPT'" +The output should include '@sed@' +End + +It 'substitutes CLIPROXY_API_KEY in config template' +When run bash -c "grep '__CLIPROXY_API_KEY__' '$SCRIPT'" +The output should include 'CLIPROXY_API_KEY' +End + +It 'substitutes TELEGRAM_TOKEN in env template' +When run bash -c "grep '__TELEGRAM_TOKEN__' '$SCRIPT'" +The output should include 'TELEGRAM_TOKEN' +End + +It 'substitutes GATEWAY_TOKEN in env template' +When run bash -c "grep '__GATEWAY_TOKEN__' '$SCRIPT'" +The output should include 'GATEWAY_TOKEN' +End + +It 'substitutes WHATSAPP_ALLOW_FROM in env template' +When run bash -c "grep '__WHATSAPP_ALLOW_FROM__' '$SCRIPT'" +The output should include 'WHATSAPP_ALLOW_FROM' +End + +It 'creates state directory' +When run bash -c "grep 'mkdir -p' '$SCRIPT'" +The output should include 'STATE_DIR' +End +End + +Describe 'execution' +It 'reports config generation' +When run bash -c "grep 'Generated hermes' '$SCRIPT'" +The output should include 'Generated hermes' +End +End + +End