Force-refresh now also clears env vars that were previously injected by the
keystore but no longer exist in the current injectable secret set. This lets
credential deletion/revocation propagate in long-lived gateway processes
without restart, while still preserving external env precedence.
Adds a regression test covering deletion of a keystore-backed OPENAI_API_KEY
followed by gateway refresh.
Refines force-refresh semantics so rotated keystore secrets only overwrite
variables that were previously injected by the keystore. Externally supplied
env vars (shell/Docker/systemd) remain authoritative across the life of the
process, matching startup precedence.
Also adds a mixed-precedence regression test covering the case where an
external OPENAI_API_KEY is present alongside an initialized keystore.
The gateway refresh path now calls keystore injection with force=True so
rotated secrets replace stale in-process env vars without requiring a
restart. Startup paths still keep the default non-overwriting behavior so
shell exports and explicitly supplied env vars win on boot.
Also tighten the regression test to require the rotated keystore secret to
replace a stale env value during refresh, instead of accepting either old
or new values.
Addresses final gateway keystore gap:
- move keystore injection outside the config.yaml existence branch so
gateway/headless installs with only a keystore (and a stubbed .env)
still receive credentials on import/startup
- re-run keystore injection in the long-lived gateway credential refresh
path so rotated keystore secrets can take effect without restart
- fix keystore store methods to use short-lived sqlite connections instead
of a persistent connection, avoiding database-locked failures during
injectable secret reads from fresh processes
- add gateway regression tests for startup without config.yaml and refresh-
path reinjection of keystore-backed secrets
Validation: targeted suite now 136 passing
Addresses follow-up review findings:
- Cross-process persistence now uses locked read/modify/write helpers
(wallet/file_state.py) instead of load-once/overwrite-whole-file writes.
Wallet tx history and policy state refresh from disk and merge updates
across CLI/gateway processes.
- Hard-block policies now run before require_approval. User wallets can no
longer bypass spending limits, blocklists, daily caps, or cooldowns just
by requesting owner approval.
- Duplicate wallets for the same chain/address are rejected on create/import.
delete_wallet() now removes key material only when no remaining metadata
references that address.
- Wallet export remains explicit via cli_export requester.
- Keystore docs/code now consistently describe SecretBox as XSalsa20-Poly1305.
Regression coverage added for:
- no insecure credential-store fallback
- tx history merge across manager instances
- policy state merge across engine instances
- user-wallet hard-block precedence over require_approval
- duplicate-wallet rejection and shared-key deletion safety
Validation: 134 targeted tests passing
Addresses review findings:
- Remove insecure automatic encrypted-file credential-store fallback.
now only uses real OS/keyctl-backed stores,
or remains unavailable. Headless users must use explicit
HERMES_KEYSTORE_PASSPHRASE if desired.
- Add shared wallet runtime so tools/CLI/approval use the same configured
providers and persisted policy state.
- Inject keystore-backed secrets into gateway/headless startup too, so
migrated .env stubs don't break messaging deployments.
- Persist wallet policy state (freeze, daily totals, rate-limit timestamps,
cooldown timestamps) across invocations.
- Persist transaction history to disk across invocations.
- Make owner-approved sends execute through the same runtime/policy path and
record policy state after successful approved sends.
- Fix wallet export by allowing explicit CLI export reads of sealed keys via
dedicated requester path () instead of generic CLI reads.
- Make CLI wallet sends evaluate policy before execution and honor freeze.
- Align docs with actual crypto primitive (XSalsa20-Poly1305 via SecretBox)
and current policy-config scope.
Validation:
- 129 tests passing
- freeze persistence verified manually
- wallet export verified manually
New agent-facing tools:
- wallet_address: Get a wallet's deposit address for receiving funds
- wallet_networks: List all supported chains (mainnet + testnet) and
which ones have active wallets
Improvements:
- RPC endpoint overrides from config.yaml (wallet.rpc_endpoints section)
- Better module docstring with full tool inventory
- Toolset updated with all 7 tools
Tested end-to-end with Hermes agent on Solana mainnet:
- Agent correctly discovers and uses all 7 wallet tools
- Policy engine properly gates user wallet sends (require_approval)
- Balance checks, address sharing, network listing all working
New installs get a cleaner structure:
cache/images/ (was image_cache/)
cache/audio/ (was audio_cache/)
cache/documents/ (was document_cache/)
cache/screenshots/ (was browser_screenshots/)
platforms/whatsapp/session/ (was whatsapp/session/)
platforms/matrix/store/ (was matrix/store/)
platforms/pairing/ (was pairing/)
Existing installs are unaffected -- get_hermes_dir() checks for the
old path first and uses it if present. No migration needed.
Adds get_hermes_dir(new_subpath, old_name) helper to hermes_constants.py
for reuse by any future subsystem.
Add the gstack community skills repo to the default tap list and fix
skill_identifier construction for repos with an empty path prefix.
Co-authored-by: Tugrul Guner <tugrulguner@users.noreply.github.com>
Users intuitively write model: { model: my-model } instead of
model: { default: my-model } and it silently falls back to the
hardcoded default. Now both spellings work across all three config
consumers: runtime_provider, CLI, and gateway.
Co-authored-by: ygd58 <ygd58@users.noreply.github.com>
Salvage of PR #3533 (binhnt92). Follow-up to #3480 — applies min(100, ...) to 5 remaining unclamped percentage display sites in context_compressor, cli /stats, gateway /stats, and memory tool. Defensive clamps now that the root cause (estimation heuristic) was already removed in #3480.
Co-Authored-By: binhnt92 <binhnt92@users.noreply.github.com>
When exiting a session that has a title (auto-generated or manual),
the exit summary now also shows:
hermes -c "Session Title"
alongside the existing hermes --resume <id> command.
Also adds the title to the session info block.
Some environments lose pip inside the venv. Before invoking pip install,
check pip --version and bootstrap with ensurepip if missing. Applied to
both update code paths (_update_via_zip and cmd_update).
Salvaged from PR #3359.
Co-authored-by: Git-on-my-level <Git-on-my-level@users.noreply.github.com>
* fix(gateway): filter automated/noreply senders in email adapter
Fixes#3453
Adds noreply/automated sender filtering to the email adapter. Drops emails from noreply, mailer-daemon, postmaster addresses and bulk mail headers (Auto-Submitted, Precedence, List-Unsubscribe) before dispatching. Prevents pairing codes and AI responses being sent to automated senders.
* fix: remove redundant seen_uids add + trailing whitespace cleanup
---------
Co-authored-by: devorun <130918800+devorun@users.noreply.github.com>
The lockfile was still pinned to hermes-agent 0.4.0 after the v0.5.0
release, causing downstream consumers (e.g. the Nix package built via
uv2nix) to report the wrong version. Also drops stale transitive deps
(bashlex, boto3, swe-rex) that were carried over from the removed
swe-rex integration.
When the plist is deleted (manual cleanup, failed upgrade),
hermes gateway start now regenerates it automatically instead of
failing. Also simplifies the returncode==3 error path since the
plist is guaranteed to exist at that point.
Co-authored-by: Bartok9 <Bartok9@users.noreply.github.com>
Local/custom OpenAI-compatible providers (Ollama, LM Studio, vLLM) that
don't require auth were hitting empty api_key rejections from the OpenAI
SDK, especially when used as smart model routing targets.
Uses the same 'no-key-required' placeholder already used in
_resolve_openrouter_runtime() for the identical scenario.
Salvaged from PR #3543.
Co-authored-by: scottlowry <scottlowry@users.noreply.github.com>
* fix(github-auth): check ~/.hermes/.env before ~/.git-credentials for token extraction
Users who configured their token via `hermes setup` have it stored in
~/.hermes/.env (GITHUB_TOKEN=...), not in ~/.git-credentials. On macOS
with osxkeychain as the default git credential helper, ~/.git-credentials
may not exist at all, causing silent 401 failures in all GitHub skills.
Add ~/.hermes/.env as the first fallback in the auth detection block and
the inline "Extracting the Token from Git Credentials" example.
Priority order: env var → ~/.hermes/.env → ~/.git-credentials → none
Part of fix for NousResearch/hermes-agent#3464
* fix(github-auth): check ~/.hermes/.env before ~/.git-credentials
Fixes#3464
* fix(github-auth): check ~/.hermes/.env before ~/.git-credentials
Fixes#3464
* fix(github-auth): check ~/.hermes/.env before ~/.git-credentials
Fixes#3464
* fix(github-auth): check ~/.hermes/.env before ~/.git-credentials
Fixes#3464
* fix(github-auth): check ~/.hermes/.env before ~/.git-credentials
Fixes#3464
* fix(github-auth): check ~/.hermes/.env before ~/.git-credentials
Fixes#3464
One-shot local execution built `printf FENCE; <cmd>; __hermes_rc=...`, so a
command ending in a heredoc produced a closing line like `EOF; __hermes_rc=...`,
which is not a valid delimiter. Bash then treated the rest of the wrapper as
heredoc body, leaking it into tool output (e.g. gh issue/PR flows).
Use newline-separated wrapper lines so the delimiter stays alone and the
trailer runs after the heredoc completes.
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
Add per-task timeout settings under auxiliary.{task}.timeout in config.yaml
instead of hardcoded values. Users with slow local models (Ollama, llama.cpp)
can now increase timeouts for compression, vision, session search, etc.
Defaults:
- auxiliary.compression.timeout: 120s (was hardcoded 45s)
- auxiliary.vision.timeout: 30s (unchanged)
- all other aux tasks: 30s (was hardcoded 30s)
- title_generator: 30s (was hardcoded 15s)
call_llm/async_call_llm now auto-resolve timeout from config when not
explicitly passed. Callers can still override with an explicit timeout arg.
Based on PR #3406 by alanfwilliams. Converted from env vars to config.yaml
per project conventions.
Co-authored-by: alanfwilliams <alanfwilliams@users.noreply.github.com>
Prevent the agent from accidentally killing its own process with
pkill -f gateway, killall hermes, etc. Adds a dangerous command
pattern that triggers the approval flow.
Co-authored-by: arasovic <arasovic@users.noreply.github.com>
Adds 'hermes webhook' CLI subcommand and a skill — zero new model tools.
CLI commands (require webhook platform to be enabled):
hermes webhook subscribe <name> [--events, --prompt, --deliver, ...]
hermes webhook list
hermes webhook remove <name>
hermes webhook test <name>
All commands gate on webhook platform being enabled in config. If not
configured, prints setup instructions (gateway setup wizard, manual
config.yaml, or env vars).
The agent uses these via terminal tool, guided by the webhook-subscriptions
skill which documents setup, common patterns (GitHub, Stripe, CI/CD,
monitoring), prompt template syntax, security, and troubleshooting.
Adapter enhancement: webhook.py hot-reloads dynamic subscriptions from
~/.hermes/webhook_subscriptions.json on each incoming request (mtime-gated).
Static config.yaml routes always take precedence.
Docs: updated webhooks.md with Dynamic Subscriptions section, added
hermes webhook to cli-commands.md reference.
No new model tools. No toolset changes.
24 new tests for CLI CRUD, persistence, enabled-gate, and adapter
dynamic route loading.
Two fixes for /skills install and /skills uninstall slash commands:
1. input() hangs indefinitely inside prompt_toolkit's TUI event loop,
soft-locking the CLI. The user typing the slash command is already
implicit consent, so confirmation is now always skipped.
2. Cache invalidation was unconditional — installing or uninstalling a
skill mid-session silently broke the prompt cache, increasing costs.
The slash handler now defers cache invalidation by default (skill
takes effect next session). Pass --now to invalidate immediately,
with a message explaining the cost tradeoff. The CLI argparse path
(hermes skills install) is unaffected and still invalidates.
Fixes#3474
Salvaged from PR #3496 by dlkakbs.
hermes update hangs on input() when run from cron, scripts, or piped
contexts. Check both stdin and stdout isatty(), catch EOFError as a
fallback, and print guidance to run 'hermes config migrate' later.
Co-authored-by: phippsbot-byte <phippsbot-byte@users.noreply.github.com>
When all messaging platforms exhaust retries and get queued for background
reconnection, exit with code 1 so systemd Restart=on-failure can restart
the process. Previously the gateway stayed alive as a zombie with no
connected platforms and exit code 0.
Salvaged from PR #3567 by kelsia14. Test updates added.
Co-authored-by: kelsia14 <kelsia14@users.noreply.github.com>
* fix: keep gateway running through telegram proxy failures
- continue gateway startup in degraded mode when Telegram cannot connect yet
- ensure Telegram fallback transport also honors proxy env vars
- support reconnect retries without taking down the whole gateway
* test(telegram): cover proxy env handling in fallback transport
---------
Co-authored-by: kufufu9 <pi@local>
Salvage of PR #2173 (hanai) and PR #3432 (timknip).
Injects PATH, VIRTUAL_ENV, and HERMES_HOME into the macOS launchd plist so gateway subprocesses find user-installed tools (node, ffmpeg, etc.). Matches systemd unit parity with venv/bin, node_modules/.bin, and resolved node dir in PATH. Includes 7 new tests and docs updates across 4 pages.
Co-Authored-By: Han <ihanai1991@gmail.com>
Co-Authored-By: timknip <timknip@users.noreply.github.com>
Without enabling the Messages Tab in App Home settings, users see
"Sending messages to this app has been turned off" when trying to DM
the bot — even with all correct scopes and event subscriptions.
Add Step 5 (Enable the Messages Tab) between Event Subscriptions and
Install App, with a danger admonition. Also add troubleshooting entry
for this specific error message. Renumber subsequent steps (6→7→8→9).
Co-authored-by: Alberto Leal <mail4alberto@gmail.com>
Use atomic_json_write() from utils.py instead of plain open()/json.dump()
for the models.dev disk cache. Prevents corrupted cache if the process is
killed mid-write — _load_disk_cache() silently returns {} on corrupt JSON,
losing all model metadata until the next successful API fetch.
Co-authored-by: memosr <memosr@users.noreply.github.com>
Ollama reuses index 0 for every tool call in a parallel batch,
distinguishing them only by id. The streaming accumulator now
detects a new non-empty id at an already-active index and redirects
it to a fresh slot, preventing names and arguments from being
concatenated into a single tool call.
No-op for normal providers that use incrementing indices.
Co-authored-by: dmater01 <dmater01@users.noreply.github.com>
* Fixing mattermost configuration parsing bugs
* fix: add homeassistant to skills_config + platform consistency tests
Follow-up for cherry-picked #3512:
- Add homeassistant to skills_config.py PLATFORMS (was in tools_config
but missing from skills_config)
- Add 3 consistency tests that verify all platforms in tools_config have
matching toolset definitions, gateway includes, and skills_config entries
— prevents this class of bug from recurring
---------
Co-authored-by: DaneelV3 <dannel@v3rtical.tech>
Commands sent directly to the bot in groups include @botname suffix
(e.g. /compress@TigerNanoBot). get_command() now strips the @anything
part before lookup, matching how Telegram bot menu generates commands.
Fixes all slash commands silently doing nothing when sent with @mention.
Co-authored-by: MacroAnarchy <MacroAnarchy@users.noreply.github.com>
Add X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer
to all API server responses via a new security_headers_middleware.
Co-authored-by: Oktay Aydin <aydnOktay@users.noreply.github.com>
Adds Access-Control-Max-Age: 600 to CORS preflight responses, telling
browsers to cache the preflight for 10 minutes. Reduces redundant OPTIONS
requests and improves perceived latency for browser-based API clients.
Salvaged from PR #3514 by aydnOktay.
Co-authored-by: aydnOktay <xaydinoktay@gmail.com>
* feat: GPT tool-use steering + strip budget warnings from history
Two changes to improve tool reliability, especially for OpenAI GPT models:
1. GPT tool-use enforcement prompt: Adds GPT_TOOL_USE_GUIDANCE to the
system prompt when the model name contains 'gpt' and tools are loaded.
This addresses a known behavioral pattern where GPT models describe
intended actions ('I will run the tests') instead of actually making
tool calls. Inspired by similar steering in OpenCode (beast.txt) and
Cline (GPT-5.1 variant).
2. Budget warning history stripping: Budget pressure warnings injected by
_get_budget_warning() into tool results are now stripped when
conversation history is replayed via run_conversation(). Previously,
these turn-scoped signals persisted across turns, causing models to
avoid tool calls in all subsequent messages after any turn that hit
the 70-90% iteration threshold.
* fix: replace hardcoded ~/.hermes paths with get_hermes_home() for profile support
Prep for the upcoming profiles feature — each profile is a separate
HERMES_HOME directory, so all paths must respect the env var.
Fixes:
- gateway/platforms/matrix.py: Matrix E2EE store was hardcoded to
~/.hermes/matrix/store, ignoring HERMES_HOME. Now uses
get_hermes_home() so each profile gets its own Matrix state.
- gateway/platforms/telegram.py: Two locations reading config.yaml via
Path.home()/.hermes instead of get_hermes_home(). DM topic thread_id
persistence and hot-reload would read the wrong config in a profile.
- tools/file_tools.py: Security path for hub index blocking was
hardcoded to ~/.hermes, would miss the actual profile's hub cache.
- hermes_cli/gateway.py: Service naming now uses the profile name
(hermes-gateway-coder) instead of a cryptic hash suffix. Extracted
_profile_suffix() helper shared by systemd and launchd.
- hermes_cli/gateway.py: Launchd plist path and Label now scoped per
profile (ai.hermes.gateway-coder.plist). Previously all profiles
would collide on the same plist file on macOS.
- hermes_cli/gateway.py: Launchd plist now includes HERMES_HOME in
EnvironmentVariables — was missing entirely, making custom
HERMES_HOME broken on macOS launchd (pre-existing bug).
- All launchctl commands in gateway.py, main.py, status.py updated
to use get_launchd_label() instead of hardcoded string.
Test fixes: DM topic tests now set HERMES_HOME env var alongside
Path.home() mock. Launchd test uses get_launchd_label() for expected
commands.
PR #3566 intentionally routes suppressed content to stream_delta_callback
when tool calls are present, so reasoning tag extraction can fire during
streaming. The test was still asserting the old behavior where content
after tool calls was fully suppressed from the callback.
Updated the assertion to match: content IS delivered to the callback
(for tag extraction), with display-level suppression handled by the
CLI's _stream_delta.
StreamResponse headers are flushed on prepare() before the CORS
middleware can inject them. Resolve CORS headers up front using
_cors_headers_for_origin() so the full set (including
Access-Control-Allow-Origin) is present on SSE streams.
Co-authored-by: ygd58 <ygd58@users.noreply.github.com>
Add GET /v1/health as an alias to the existing /health endpoint so
OpenAI-compatible health checks work out of the box.
Co-authored-by: Oktay Aydin <aydnOktay@users.noreply.github.com>
The base orchestrator passes metadata=_thread_metadata to
send_image_file, send_video, and send_document. WhatsApp was the
only platform adapter missing the parameter, causing TypeError
crashes when sending media.
Extended to all three methods (original PR only fixed send_image_file).
Salvaged from PR #3144.
Co-authored-by: afifai <afifai@users.noreply.github.com>
* fix(provider): remove MiniMax /v1→/anthropic auto-correction to allow user override
The minimax-specific auto-correction in runtime_provider.py was
preventing users from overriding to the OpenAI-compatible endpoint
via MINIMAX_BASE_URL. Users in certain regions get nginx 404 on
api.minimax.io/anthropic and need to switch to api.minimax.chat/v1.
The generic URL-suffix detection already handles /anthropic →
anthropic_messages, so the minimax-specific code was redundant for
the default path and harmful for the override path.
Now: default /anthropic URL works via generic detection, user
override to /v1 gets chat_completions mode naturally.
Closes#3546 (different approach — respects user overrides instead
of changing the default endpoint).
* fix(display): show reasoning during streaming even when tool calls suppress content
When a model generates content (containing <REASONING_SCRATCHPAD> tags)
alongside tool calls in the same API response, content deltas were
suppressed from streaming once any tool call chunk arrived. This
prevented the CLI's tag extraction from running, so reasoning was
never shown during streaming. The post-response fallback then
displayed reasoning AFTER the already-visible streamed response,
creating a confusing reversed order.
Fix: route suppressed content to stream_delta_callback even when tool
calls are present. The CLI's _stream_delta handles tag extraction —
reasoning tags are routed to the reasoning display box, while
non-reasoning text is handled by the existing stream display logic.
This ensures reasoning appears before tool execution and the final
response, matching the expected visual order.
The TOOL_USE_ENFORCEMENT_GUIDANCE injection (added in #3528) was
hardcoded to only match gpt/codex model names. This makes it a
config option so users can turn it on for any model family.
New config key: agent.tool_use_enforcement
- "auto" (default): matches gpt/codex (existing behavior)
- true: inject for all models
- false: never inject
- list of strings: custom model-name substrings to match
e.g. ["gpt", "codex", "deepseek", "qwen"]
No version bump needed — deep merge provides the default
automatically for existing installs.
12 new tests covering all config modes.
The documentation incorrectly instructed users to set Public Bot to OFF,
but this prevents using the Discord-provided invite link (recommended method),
causing the error: 'Private application cannot have a default authorization link'.
Changes:
- Changed Step 2: Public Bot now set to ON (required for Installation tab method)
- Added info callout explaining the Private Bot alternative (use Manual URL)
- Added note in Step 5 Option A clarifying the Public Bot requirement
Fixes Discord bot setup flow for new users following the recommended path.
Co-authored-by: Docs Fix <docs-fix@example.com>