文件最后提交记录最后更新时间
test: reorganize test structure and add missing unit tests Reorganize flat tests/ directory to mirror source code structure (tools/, gateway/, hermes_cli/, integration/). Add 11 new test files covering previously untested modules: registry, patch_parser, fuzzy_match, todo_tool, approval, file_tools, gateway session/config/ delivery, and hermes_cli config/models. Total: 147 unit tests passing, 9 integration tests gated behind pytest marker. 3 个月前
fix(update): quarantine hermes.exe vs concurrent Windows instance (#26670) (#26677) * fix(update): detect concurrent hermes.exe on Windows; retry + restart-defer quarantine Closes #26670. When 'hermes update' runs on Windows with another hermes.exe alive (most commonly the Hermes Desktop Electron app's spawned backend) _quarantine_running_hermes_exe() fails to rename the venv shim with [WinError 32]. uv pip install -e . then exits 2, the git-pull fast path is silently abandoned, and the ZIP fallback runs (and fails the same way) before eventually succeeding. This change implements three of the five proposed fixes from the issue: 1. Concurrent-instance detection (preferred fix). _detect_concurrent_hermes_instances() uses psutil to enumerate processes whose .exe is one of our venv shims (hermes.exe / hermes-gateway.exe), excluding the caller's PID. When any match exists, cmd_update prints an actionable message naming the blocking PIDs and exits 2 BEFORE any destructive work. New --force flag bypasses the gate. 2. Retry + restart-deferred fallback. _quarantine_running_hermes_exe() now retries the rename up to 4 times with 100/250/500/1000 ms backoff (covers the transient AV-scanner-handle case). If all retries fail, it schedules the replacement via MoveFileExW with the OS deferred-rename flag so the new shim can land at the original path and the update completes; the old image is fully unloaded after the user's next system restart. 3. Actionable warning text. The old 'Could not quarantine: [WinError 32]' warning is replaced with one that names the likely culprits (Hermes Desktop, REPLs, gateway, AV) and points to the new --force flag. Tests: - 13 new tests in tests/hermes_cli/test_update_concurrent_quarantine.py covering: psutil-based enumeration, self-pid exclusion, case-insensitive matching of .EXE, no-psutil graceful degradation, off-Windows no-op, helpful warning formatting, retry-then-succeed, restart-deferred fallback, cmd_update abort + exit code 2, and --force bypass. - New autouse fixture in tests/hermes_cli/conftest.py defaults _detect_concurrent_hermes_instances to [] so the rest of the suite isn't tripped by the developer's own running hermes.exe. Opt-out marker 'real_concurrent_gate' registered in pyproject.toml. - Updating docs page (website/docs/getting-started/updating.md) gains a short section explaining the new Windows error and remediation. * chore: refresh uv.lock to match pyproject.toml exact pins aiohttp 3.13.4 -> 3.13.3 (matches pyproject pin: aiohttp==3.13.3) anthropic 0.87.0 -> 0.86.0 (matches pyproject pin: anthropic==0.86.0) hermes-agent 0.13.0 -> 0.14.0 (matches pyproject version) CI's uv lock --check was failing on the merged state because main drifted: pyproject.toml uses exact == pins for those two deps and the hermes-agent version was bumped to 0.14.0 but the lockfile still had 0.13.0.16 天前
refactor(ai-gateway): single source of truth for model catalog (#13304) Delete the stale literal _PROVIDER_MODELS["ai-gateway"] (gpt-5, gemini-2.5-pro, claude-4.5 — outdated the moment PR #13223 landed with its curated AI_GATEWAY_MODELS snapshot) and derive it from AI_GATEWAY_MODELS instead, so the picker tuples and the bare-id fallback catalog stay in sync automatically. Also fixes get_default_model_for_provider('ai-gateway') to return kimi-k2.6 (the curated recommendation) instead of claude-opus-4.6.1 个月前
fix: re-auth on stale OAuth token; read Claude Code credentials from macOS Keychain Bug 3 — Stale OAuth token not detected in 'hermes model': - _model_flow_anthropic used 'has_creds = bool(existing_key)' which treats any non-empty token (including expired OAuth tokens) as valid. - Added existing_is_stale_oauth check: if the only credential is an OAuth token (sk-ant- prefix) with no valid cc_creds fallback, mark it stale and force the re-auth menu instead of silently accepting a broken token. Bug 4 — macOS Keychain credentials never read: - Claude Code >=2.1.114 migrated from ~/.claude/.credentials.json to the macOS Keychain under service 'Claude Code-credentials'. - Added _read_claude_code_credentials_from_keychain() using the 'security' CLI tool; read_claude_code_credentials() now tries Keychain first then falls back to JSON file. - Non-Darwin platforms return None from Keychain read immediately. Tests: - tests/agent/test_anthropic_keychain.py: 11 cases covering Darwin-only guard, security command failures, JSON parsing, fallback priority. - tests/hermes_cli/test_anthropic_model_flow_stale_oauth.py: 8 cases covering stale OAuth detection, API key passthrough, cc_creds fallback. Refs: #12905 1 个月前
refactor(tests): re-architect tests + fix CI failures (#5946) * refactor: re-architect tests to mirror the codebase * Update tests.yml * fix: add missing tool_error imports after registry refactor * fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers, causing test_code_execution tests to hit the Modal remote path. * fix(tests): fix update_check and telegram xdist failures - test_update_check: replace patch("hermes_cli.banner.os.getenv") with monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os directly, it uses get_hermes_home() from hermes_constants. - test_telegram_conflict/approval_buttons: provide real exception classes for telegram.error mock (NetworkError, TimedOut, BadRequest) so the except clause in connect() doesn't fail with "catching classes that do not inherit from BaseException" when xdist pollutes sys.modules. * fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock1 个月前
refactor(tests): re-architect tests + fix CI failures (#5946) * refactor: re-architect tests to mirror the codebase * Update tests.yml * fix: add missing tool_error imports after registry refactor * fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers, causing test_code_execution tests to hit the Modal remote path. * fix(tests): fix update_check and telegram xdist failures - test_update_check: replace patch("hermes_cli.banner.os.getenv") with monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os directly, it uses get_hermes_home() from hermes_constants. - test_telegram_conflict/approval_buttons: provide real exception classes for telegram.error mock (NetworkError, TimedOut, BadRequest) so the except clause in connect() doesn't fail with "catching classes that do not inherit from BaseException" when xdist pollutes sys.modules. * fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock1 个月前
fix(tests): catch up 25 stale tests after recent merges (#28626) Sweep of all CI failures on origin/main, grouped by drift source: Telegram allowlist gate (db50af910 added user-authz to _should_process_message): - Hardcoded "[Telegram]" prefix in the logger.warning so the call no longer dereferences self.name → self.platform, which test fixtures built via object.__new__ never set. - test_telegram_format / test_allowed_channels_widening fixtures stub _is_callback_user_authorized → True so the new gate doesn't reject guest-mode / allowed-channels test messages. - test_telegram_approval_buttons::test_update_prompt_callback_not_affected sets TELEGRAM_ALLOWED_USERS="*" so the fail-closed default doesn't reject the callback before it writes .update_response. Approval surface (6d495d9e7 renamed status, 214b95392 detached stdin): - test_no_callback_returns_approval_required: status is now "pending_approval" (was "approval_required"). - test_close_stdin_allows_eof_driven_process_to_finish: switch to use_pty=True; non-PTY now uses stdin=DEVNULL. Mattermost (send() now resolves root_id via _api_get first): - test_send_with_thread_reply mocks _session.get with a thread-root response so the new resolver doesn't TypeError on a bare AsyncMock. Kanban (d8ad431de rename, f55d94a1e review column, _kanban_worker_skill_available): - _safe_int → _to_epoch in the two test_kanban_db tests. - Spawn-skills tests (×3) monkey-patch _kanban_worker_skill_available to True since the isolated kanban_home fixture has no devops/kanban-worker tree. - test_gateway_dispatcher_disables_corrupt_board: connect count 3 → 5 (review-column probe now also runs per tick). Aux-config severity at_or_above (a94ddd807): - test_diagnostics_endpoint_severity_filter expects warning filter to include error+critical now (was exact-match). Anthropic error handling (conversation loop extracted from run_agent): - _no_backoff_wait fixture patches BOTH run_agent.jittered_backoff AND agent.conversation_loop.jittered_backoff. The latter is the actual call site; without the second patch tests burn ~2s per retry and hit the 30s SIGALRM timeout on CI. Other test pollution / drift: - test_auto_does_not_select_copilot_from_github_token: patch agent.bedrock_adapter.has_aws_credentials → False so boto3's credential chain can't auto-pick Bedrock from developer ~/.aws. - test_setup_openclaw_migration: patch hermes_cli.gateway.get_env_value in addition to setup_mod.get_env_value — _platform_status reads through the gateway module's binding. - test_gateway_prefix: COMPONENT_PREFIXES["gateway"] now includes "hermes_plugins" too. - test_recommended_update_command_defaults_to_hermes_update: also short-circuit get_managed_update_command in case a stray ~/.hermes/.managed marker is present. - test_user_id_is_not_explicit: _parse_target_ref now returns is_explicit=False for Slack U.../W... IDs (chat.postMessage rejects them — a DM must be opened first via conversations.open).16 天前
fix(cli): /model picker honors provider-specific context caps (#16030) _apply_model_switch_result (the interactive /model picker's confirmation path) printed ModelInfo.context_window straight from models.dev, which reports the vendor-wide value (1.05M for gpt-5.5 on openai). ChatGPT Codex OAuth caps the same slug at 272K, so the picker showed 1M while the runtime (compressor, gateway /model, typed /model <name>) correctly used 272K — the classic 'sometimes 1M, sometimes 272K' mismatch on a single model. Both display paths now go through resolve_display_context_length(), matching the fix that _handle_model_switch received earlier. Also bump the stale last-resort fallback in DEFAULT_CONTEXT_LENGTHS (gpt-5.5: 400000 -> 1050000) to match the real OpenAI API value; the 272K Codex cap is already enforced via the Codex-OAuth branch, so the fallback now reflects what every non-Codex probe-miss should see. Tests: adds test_apply_model_switch_result_context.py with three scenarios (Codex cap wins, OpenRouter shows 1.05M, resolver-empty falls back to ModelInfo). Updates the existing non-Codex fallback test to assert 1.05M (the correct value). ## Validation | path | before | after | |-------------------------------|-----------|-----------| | picker -> gpt-5.5 on Codex | 1,050,000 | 272,000 | | picker -> gpt-5.5 on OpenAI | 1,050,000 | 1,050,000 | | picker -> gpt-5.5 on OpenRouter | 1,050,000 | 1,050,000 | | typed /model gpt-5.5 on Codex | 272,000 | 272,000 |1 个月前
fix(profiles): honour active_profile when HERMES_HOME points to hermes root Problem: After hermes profile use NAME, the gateway (started via systemd with HERMES_HOME=/root/.hermes hardcoded) ignores the active profile and always runs as the Default profile. WebUI, Telegram, and all non-CLI platforms are affected. Root cause: _apply_profile_override() contained an early-return guard: if profile_name is None and os.environ.get("HERMES_HOME"): return # trust the inherited value The intent was to let child processes inherit their parent's profile via HERMES_HOME without redundantly re-reading active_profile. But systemd also sets HERMES_HOME — to the hermes root (/root/.hermes), not a profile directory — so the guard fired and silently skipped the active_profile check. The user's hermes profile use NAME write to ~/.hermes/active_profile was never seen by the gateway process. Fix: Only skip the active_profile check when HERMES_HOME is already a profile directory, identified by its immediate parent directory being named "profiles" (e.g. ~/.hermes/profiles/coder or /opt/data/profiles/coder). When HERMES_HOME points to a root directory (parent name != "profiles"), continue to read active_profile. Tests: - test_hermes_home_at_root_with_active_profile_is_redirected: the bug scenario — HERMES_HOME=/root/.hermes + active_profile=coder → HERMES_HOME must be redirected to .../profiles/coder. Stash-verified: FAILS without fix, PASSES with fix. - test_hermes_home_already_profile_dir_is_trusted: child-process inheritance contract unchanged — .../profiles/coder is trusted as-is. - test_hermes_home_unset_reads_active_profile: classic path unchanged. - test_hermes_home_unset_default_profile_no_redirect: "default" still produces no redirect. 4/4 tests green. Closes #22502. 26 天前
feat(providers): add tencent-tokenhub provider support Registers tencent-tokenhub (https://tokenhub.tencentmaas.com/v1) as a new API-key provider with model tencent/hy3-preview (256K context). - PROVIDER_REGISTRY entry + TOKENHUB_API_KEY / TOKENHUB_BASE_URL env vars - Aliases: tencent, tokenhub, tencent-cloud, tencentmaas - openai_chat transport with is_tokenhub branch for top-level reasoning_effort (Hy3 is a reasoning model) - tencent/hy3-preview:free added to OpenRouter curated list - 60+ tests (provider registry, aliases, runtime resolution, credentials, model catalog, URL mapping, context length) - Docs: integrations/providers.md, environment-variables.md, model-catalog.json Author: simonweng <simonweng@tencent.com> Salvaged from PR #16860 onto current main (resolved conflicts with #16935 Azure Anthropic env-var hint tests and the --provider choices= list removal in chat_parser). 1 个月前
feat: shell hooks — wire shell scripts as Hermes hook callbacks Users can declare shell scripts in config.yaml under a hooks: block that fire on plugin-hook events (pre_tool_call, post_tool_call, pre_llm_call, subagent_stop, etc). Scripts receive JSON on stdin, can return JSON on stdout to block tool calls or inject context pre-LLM. Key design: - Registers closures on existing PluginManager._hooks dict — zero changes to invoke_hook() call sites - subprocess.run(shell=False) via shlex.split — no shell injection - First-use consent per (event, command) pair, persisted to allowlist JSON - Bypass via --accept-hooks, HERMES_ACCEPT_HOOKS=1, or hooks_auto_accept - hermes hooks list/test/revoke/doctor CLI subcommands - Adds subagent_stop hook event fired after delegate_task children exit - Claude Code compatible response shapes accepted Cherry-picked from PR #13143 by @pefontana. 1 个月前
fix(tui): @folder: only yields directories, @file: only yields files Reported during TUI v2 blitz testing: typing @folder: in the composer pulled up .dockerignore, .env, .gitignore, and every other file in the cwd alongside the actual directories. The completion loop yielded every entry regardless of the explicit prefix and auto-rewrote each completion to @file: vs @folder: based on is_dir — defeating the user's choice. Also fixed a pre-existing adjacent bug: a bare @file: or @folder: (no path) used expanded=="." as both search_dir AND match_prefix, filtering the list to dotfiles only. When expanded is empty or ".", search in cwd with no prefix filter. - want_dir = prefix == "@folder:" drives an explicit is_dir filter - preserve the typed prefix in completion text instead of rewriting - three regression tests cover: folder-only, file-only, and the bare- prefix case where completions keep the @folder: prefix 1 个月前
refactor(tests): re-architect tests + fix CI failures (#5946) * refactor: re-architect tests to mirror the codebase * Update tests.yml * fix: add missing tool_error imports after registry refactor * fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers, causing test_code_execution tests to hit the Modal remote path. * fix(tests): fix update_check and telegram xdist failures - test_update_check: replace patch("hermes_cli.banner.os.getenv") with monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os directly, it uses get_hermes_home() from hermes_constants. - test_telegram_conflict/approval_buttons: provide real exception classes for telegram.error mock (NetworkError, TimedOut, BadRequest) so the except clause in connect() doesn't fail with "catching classes that do not inherit from BaseException" when xdist pollutes sys.modules. * fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock1 个月前
refactor(tests): re-architect tests + fix CI failures (#5946) * refactor: re-architect tests to mirror the codebase * Update tests.yml * fix: add missing tool_error imports after registry refactor * fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers, causing test_code_execution tests to hit the Modal remote path. * fix(tests): fix update_check and telegram xdist failures - test_update_check: replace patch("hermes_cli.banner.os.getenv") with monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os directly, it uses get_hermes_home() from hermes_constants. - test_telegram_conflict/approval_buttons: provide real exception classes for telegram.error mock (NetworkError, TimedOut, BadRequest) so the except clause in connect() doesn't fail with "catching classes that do not inherit from BaseException" when xdist pollutes sys.modules. * fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock1 个月前
fix(model): let Codex setup reuse or reauthenticate 1 个月前
Switch to JWT token for inference against Nous, falling back to old opaque token on failure. 18 天前
fix(auth): improve xAI OAuth SSH hint with visual header and auto-detected host 17 天前
test+docs(oauth): pin manual-paste semantics and document browser-only path (#26923) Tests (tests/hermes_cli/test_auth_manual_paste.py): * 9 parametrised + scalar cases for _is_remote_session covering the new Cloud Shell / Codespaces / Gitpod / Replit / StackBlitz env vars (plus the existing SSH ones). * 9 cases for _parse_pasted_callback covering every paste form (full URL, https URL with extra params, bare ?code=..., bare code=... fragment, bare opaque value, error+description, empty, whitespace-only, malformed URL). * 3 cases for _prompt_manual_callback_paste (happy path, EOF, Ctrl-C). * 3 end-to-end _xai_oauth_loopback_login(manual_paste=True) cases: the HTTP server MUST NOT be started (asserted via a callable that raises if invoked), wrong state still rejected with xai_state_mismatch (no CSRF bypass), and empty paste surfaces xai_code_missing. * SSH-hint mention test ensures the --manual-paste instruction is printed in the remote-session hint. Docs: * oauth-over-ssh.md — new "Browser-only remote (Cloud Shell / Codespaces / EC2 Instance Connect)" section with the --manual-paste recipe, plus a TL;DR note for the new flag. * xai-grok-oauth.md — short subsection pointing at the same recipe and the OAuth-over-SSH guide anchor. 17 天前
fix(auth) fix a few cases where refresh tokens were not rotated. 18 天前
fix(auth): fall back to global-root auth.json for providers missing in profile Profile processes (kanban workers, cron subprocesses, delegated subagents) read the profile's auth.json only. If a provider was authenticated at the global root but not inside the profile, the profile's credential_pool comes back empty and the process fails with 'No LLM provider configured' — even though the credentials are sitting in ~/.hermes/auth.json. #18594 propagated HERMES_HOME correctly, which is what surfaced this: workers now land in the right profile, and the profile turns out to shadow global with no fallback. Semantics (read-only, per-provider shadowing): * Profile has any entries for provider X → use profile only (global ignored). * Profile has zero entries for provider X → fall back to global. * Writes (write_credential_pool, _save_auth_store) still target the profile. * Classic mode (HERMES_HOME == global root) skips the fallback entirely — _global_auth_file_path() returns None. Also mirrors the fallback in get_provider_auth_state so OAuth singletons (nous, minimax-oauth, openai-codex, spotify) inherit cleanly — the Nous shared-token store (PR #19712) remains the authoritative path for Nous OAuth rotation, this just makes the read side consistent with it. Seat belt: _load_global_auth_store() refuses to read the real user's ~/.hermes/auth.json under PYTEST_CURRENT_TEST even when HERMES_HOME points to a profile-shaped path. Guard uses $HOME (stable across fixtures) rather than Path.home() (which fixtures often monkeypatch to a tmp root). Reported by @SeedsForbidden on Twitter as the credential_pool shadowing follow-up to the #18594 fix. 29 天前
fix: resolve CI test failures — add missing functions, fix stale tests (#9483) Production fixes: - Add clear_session_context() to hermes_logging.py (fixes 48 teardown errors) - Add clear_session() to tools/approval.py (fixes 9 setup errors) - Add SyncError M_UNKNOWN_TOKEN check to Matrix _sync_loop (bug fix) - Fall back to inline api_key in named custom providers when key_env is absent (runtime_provider.py) Test fixes: - test_memory_user_id: use builtin+external provider pair, fix honcho peer_name override test to match production behavior - test_display_config: remove TestHelpers for non-existent functions - test_auxiliary_client: fix OAuth tokens to match _is_oauth_token patterns, replace get_vision_auxiliary_client with resolve_vision_provider_client - test_cli_interrupt_subagent: add missing _execution_thread_id attr - test_compress_focus: add model/provider/api_key/base_url/api_mode to mock compressor - test_auth_provider_gate: add autouse fixture to clean Anthropic env vars that leak from CI secrets - test_opencode_go_in_model_list: accept both 'built-in' and 'hermes' source (models.dev API unavailable in CI) - test_email: verify email Platform enum membership instead of source inspection (build_channel_directory now uses dynamic enum loop) - test_feishu: add bot_added/bot_deleted handler mocks to _Builder - test_ws_auth_retry: add AsyncMock for sync_store.get_next_batch, add _pending_megolm and _joined_rooms to Matrix adapter mocks - test_restart_drain: monkeypatch-delete INVOCATION_ID (systemd sets this in CI, changing the restart call signature) - test_session_hygiene: add user_id to SessionSource - test_session_env: use relative baseline for contextvar clear check (pytest-xdist workers share context)1 个月前
feat(qwen): add Qwen OAuth provider with portal request support Based on #6079 by @tunamitom with critical fixes and comprehensive tests. Changes from #6079: - Fix: sanitization overwrite bug — Qwen message prep now runs AFTER codex field sanitization, not before (was silently discarding Qwen transforms) - Fix: missing try/except AuthError in runtime_provider.py — stale Qwen credentials now fall through to next provider on auto-detect - Fix: 'qwen' alias conflict — bare 'qwen' stays mapped to 'alibaba' (DashScope); use 'qwen-portal' or 'qwen-cli' for the OAuth provider - Fix: hardcoded ['coder-model'] replaced with live API fetch + curated fallback list (qwen3-coder-plus, qwen3-coder) - Fix: extract _is_qwen_portal() helper + _qwen_portal_headers() to replace 5 inline 'portal.qwen.ai' string checks and share headers between init and credential swap - Fix: add Qwen branch to _apply_client_headers_for_base_url for mid-session credential swaps - Fix: remove suspicious TypeError catch blocks around _prompt_provider_choice - Fix: handle bare string items in content lists (were silently dropped) - Fix: remove redundant dict() copies after deepcopy in message prep - Revert: unrelated ai-gateway test mock removal and model_switch.py comment deletion New tests (30 test functions): - _qwen_cli_auth_path, _read_qwen_cli_tokens (success + 3 error paths) - _save_qwen_cli_tokens (roundtrip, parent creation, permissions) - _qwen_access_token_is_expiring (5 edge cases: fresh, expired, within skew, None, non-numeric) - _refresh_qwen_cli_tokens (success, preserve old refresh, 4 error paths, default expires_in, disk persistence) - resolve_qwen_runtime_credentials (fresh, auto-refresh, force-refresh, missing token, env override) - get_qwen_auth_status (logged in, not logged in) - Runtime provider resolution (direct, pool entry, alias) - _build_api_kwargs (metadata, vl_high_resolution_images, message formatting, max_tokens suppression) 1 个月前
fix(auth): honor SSL CA env vars across httpx + requests callsites - hermes_cli/auth.py: add _default_verify() with macOS Homebrew certifi fallback (mirrors weixin 3a0ec1d93). Extend env var chain to include REQUESTS_CA_BUNDLE so one env var works across httpx + requests paths. - agent/model_metadata.py: add _resolve_requests_verify() reading HERMES_CA_BUNDLE / REQUESTS_CA_BUNDLE / SSL_CERT_FILE in priority order. Apply explicit verify= to all 6 requests.get callsites. - Tests: 18 new unit tests + autouse platform pin on existing TestResolveVerifyFallback to keep its "returns True" assertions platform-independent. Empirically verified against self-signed HTTPS server: requests honors REQUESTS_CA_BUNDLE only; httpx honors SSL_CERT_FILE only. Hermes now honors all three everywhere. Triggered by Discord reports — Nous OAuth SSL failure on macOS Homebrew Python; custom provider self-signed cert ignored despite REQUESTS_CA_BUNDLE set in env. 1 个月前
test: migrate stale os.kill monkeypatches to gateway.status._pid_exists PR #21561 migrated liveness probes across 14 call sites from os.kill(pid, 0) to gateway.status._pid_exists (psutil-first) so the gateway doesn't Ctrl+C-itself on Windows via bpo-14484. A handful of tests still patched the old os.kill seam and either happened to pass on POSIX (when PID 12345 incidentally wasn't alive on the CI worker) or failed outright — on CI runs they surfaced as 7 flaky/stable failures. Migrate each affected test to patch the correct seam: - tests/tools/test_browser_orphan_reaper.py (5 tests) Patch gateway.status._pid_exists instead of os.kill. Rename test_permission_error_on_kill_check_skips to test_alive_legacy_daemon_is_reaped — the old assertion was "PermissionError on sig 0 → skip dir"; post-migration the untracked-alive-daemon path always reaps the dir after SIGTERM (best-effort semantics were preserved). - tests/tools/test_windows_native_support.py (4 tests) Replace tests that asserted os.kill seam behavior with tests that exercise ProcessRegistry._is_host_pid_alive as a delegator and split out a new TestPidExistsOSErrorWidening class that hits gateway.status._pid_exists directly via the POSIX fallback branch (so Windows-style OSError(WinError 87) + PermissionError widening is still covered on Linux CI). - tests/tools/test_process_registry.py (1 test) Mock psutil.Process + _pid_exists instead of os.kill for the detached-session kill path. - tests/tools/test_mcp_stability.py::test_kill_orphaned_uses_sigkill_when_available SIGTERM → alive-check → SIGKILL flow now uses _pid_exists for the middle step; assertion count drops from 3 to 2. - tests/gateway/test_status.py::TestScopedLocks (2 tests) acquire_scoped_lock consults _pid_exists; patch that seam directly instead of trying to control the nested psutil call via os.kill monkeypatch. - tests/hermes_cli/test_gateway.py::test_stop_profile_gateway_keeps_pid_file_when_process_still_running The stop loop sends one SIGTERM via os.kill then polls 20x via _pid_exists; instrument both separately. Old assertion calls["kill"] == 21 split into kill == 1 + alive_probes == 20. - tests/hermes_cli/test_auth_toctou_file_modes.py::test_shared_nous_store_writes_0o600_with_0o700_parent Commit c34884ea2 switched the pytest seat-belt guard in _nous_shared_store_path() from Path.home() / ".hermes" to get_default_hermes_root(), which honors HERMES_HOME. The test sets both HERMES_HOME and HERMES_SHARED_AUTH_DIR to subpaths of the same tmp_path, and the override now collapses onto the same path the guard is refusing. Renamed the override subdirectory so the two paths diverge — guard passes, test runs. All 21 original CI failures and their local-flaky siblings now pass (278 tests across the touched files, 0 failures). 27 天前
fix(xai-oauth): pin inference base_url to x.ai origin (#28952) XAI_BASE_URL / HERMES_XAI_BASE_URL let users repoint the OAuth-authenticated inference endpoint, but the env override was an unguarded credential-leak vector: a tampered .env or hostile shell init setting XAI_BASE_URL=https://attacker.example/v1 would silently ship the SuperGrok OAuth bearer to a third party on every request. Add _xai_validate_inference_base_url() that pins the host to x.ai or a *.x.ai subdomain and rejects non-HTTPS. On rejection, fall back to the default with a warning rather than raise — a bad env var should not deadlock auth, but should never leak the bearer either. Apply at all three sites that read the env override for xai-oauth: - hermes_cli/auth.py resolve_xai_oauth_runtime_credentials (main path) - hermes_cli/auth.py _xai_oauth_loopback_login (initial login) - agent/auxiliary_client.py _resolve_xai_oauth_for_aux (aux client) E2E validated against four scenarios: attacker.example, lookalike api.x.ai.evil.com, http:// downgrade on api.x.ai, and legit custom.x.ai subdomain (which still resolves correctly). Discovered while comparing against the opencode-grok-auth plugin (github.com/ysnock404/opencode-grok-auth), which highlighted the same guard on the OpenCode side.16 天前
fix(aux): remove stale session_search model menu entry 17 天前
feat(azure-foundry): add Microsoft Entra ID auth Use azure-identity DefaultAzureCredential for keyless Foundry auth. Preserve refreshable callable credentials through OpenAI and Anthropic client paths. Add setup, doctor, auth status, docs, and tests for Entra auth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 17 天前
feat(azure-foundry): add Microsoft Entra ID auth Use azure-identity DefaultAzureCredential for keyless Foundry auth. Preserve refreshable callable credentials through OpenAI and Anthropic client paths. Add setup, doctor, auth status, docs, and tests for Entra auth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 17 天前
fix(backup): floor pre-update backup_keep to 1 so the new backup survives updates.backup_keep: 0 (or any negative value) wiped the freshly- created pre-update zip: _prune_pre_update_backups(backup_dir, keep=0): backups = sorted(..., reverse=True) # newest first, includes # the zip we just wrote for p in backups[0:]: # = all of them p.unlink() The wrapper in main.py then printed Saved: <path> for a file that no longer existed (the size lookup is wrapped in try/except OSError which silently degrades to "0 B"), leaving operators believing they had a recovery point when they had none. This is a real footgun because some config systems treat 0 as "keep unlimited"; here it does the opposite — every backup is destroyed right after creation. Fix: clamp keep to a minimum of 1 inside _prune_pre_update_backups since that helper is only invoked immediately after a fresh backup is written. Operators who genuinely want no backups should set updates.pre_update_backup: false (which gates creation entirely) rather than relying on backup_keep: 0. Also extends the backup_keep config docstring to spell out the floor and point at pre_update_backup: false as the off-switch. ## Tests Three regression tests added in TestPreUpdateBackup: - test_keep_zero_does_not_delete_freshly_created_backup — asserts the file persists after keep=0 - test_keep_negative_does_not_delete_freshly_created_backup — same for negative values - test_keep_zero_still_prunes_older_backups — proves the floor only protects the new backup; older ones are still rotated out Verified the new tests fail on origin/main (without the floor) and pass with it; full tests/hermes_cli/test_backup.py suite green (84 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 1 个月前
feat(banner): hyperlink startup banner title to latest GitHub release (#14945) Wrap the existing version label in the welcome-banner panel title ('Hermes Agent v… · upstream … · local …') with an OSC-8 terminal hyperlink pointing at the latest git tag's GitHub release page (https://github.com/NousResearch/hermes-agent/releases/tag/<tag>). Clickable in modern terminals (iTerm2, WezTerm, Windows Terminal, GNOME Terminal, Kitty, etc.); degrades to plain text on terminals without OSC-8 support. No new line added to the banner. New get_latest_release_tag() helper runs 'git describe --tags --abbrev=0' in the Hermes checkout (3s timeout, per-process cache, silent fallback for non-git/pip installs and forks without tags).1 个月前
fix: CLI/UX batch — ChatConsole errors, curses scroll, skin-aware banner, git state banner (#5974) * fix(cli): route error messages through ChatConsole inside patch_stdout Cherry-pick of PR #5798 by @icn5381. Replace self.console.print() with ChatConsole().print() for 11 error/status messages reachable during the interactive session. Inside patch_stdout, self.console (plain Rich Console) writes raw ANSI escapes that StdoutProxy mangles into garbled text. ChatConsole uses prompt_toolkit's native print_formatted_text which renders correctly. Same class of bug as #2262 — that fix covered agent output but missed these error paths in _ensure_runtime_credentials, _init_agent, quick commands, skill loading, and plan mode. * fix(model-picker): add scrolling viewport to curses provider menu Cherry-pick of PR #5790 by @Lempkey. Fixes #5755. _curses_prompt_choice rendered items starting unconditionally from index 0 with no scroll offset. The 'More providers' submenu has 13 entries. On terminals shorter than ~16 rows, items past the fold were never drawn. When UP-arrow wrapped cursor from 0 to the last item (Cancel, index 12), the highlight rendered off-screen — appearing as if only Cancel existed. Adds scroll_offset tracking that adjusts each frame to keep the cursor inside the visible window. * feat(cli): skin-aware compact banner + git state in startup banner Combined salvage of PR #5922 by @ASRagab and PR #5877 by @xinbenlv. Compact banner changes (from #5922): - Read active skin colors and branding instead of hardcoding gold/NOUS HERMES - Default skin preserves backward-compatible legacy branding - Non-default skins use their own agent_name and colors Git state in banner (from #5877): - New format_banner_version_label() shows upstream/local git hashes - Full banner title now includes git state (upstream hash, carried commits) - Compact banner line2 shows the version label with git state - Widen compact banner max width from 64 to 88 to fit version info Both the full Rich banner and compact fallback are now skin-aware and show git state.1 个月前
refactor: DRY cleanup from code review - dep_ensure.py: use get_hermes_home() instead of hand-rolled env var - dep_ensure.py: add "chrome" to browser name list (was inconsistent with browser_tool.py) - main.py _cmd_update_check: use detect_install_method() directly instead of redundant .git check - main.py _cmd_update_pip: build command list directly instead of fragile split() on display string - banner.py: rename _check_via_pypi → check_via_pypi (cross-module public API) 20 天前
fix: disabled skills respected across banner, system prompt, slash commands, and skill_view (#1897) * fix: banner skill count now respects disabled skills and platform filtering The banner's get_available_skills() was doing a raw rglob scan of ~/.hermes/skills/ without checking: - Whether skills are disabled (skills.disabled config) - Whether skills match the current platform (platforms: frontmatter) This caused the banner to show inflated skill counts (e.g. '100 skills' when many are disabled) and list macOS-only skills on Linux. Fix: delegate to _find_all_skills() from tools/skills_tool which already handles both platform gating and disabled-skill filtering. * fix: system prompt and slash commands now respect disabled skills Two more places where disabled skills were still surfaced: 1. build_skills_system_prompt() in prompt_builder.py — disabled skills appeared in the <available_skills> system prompt section, causing the agent to suggest/load them despite being disabled. 2. scan_skill_commands() in skill_commands.py — disabled skills still registered as /skill-name slash commands in CLI help and could be invoked. Both now load _get_disabled_skill_names() and filter accordingly. * fix: skill_view blocks disabled skills skill_view() checked platform compatibility but not disabled state, so the agent could still load and read disabled skills directly. Now returns a clear error when a disabled skill is requested, telling the user to enable it via hermes skills or inspect the files manually. --------- Co-authored-by: Test <test@test.com>2 个月前
test(ci): stabilize shared optional dependency baselines 22 天前
feat(skills): add skill bundles — alias /<name> loads multiple skills (#28373) Skill bundles are tiny YAML files in ~/.hermes/skill-bundles/ that group several skills under one slash command. Invoking /<bundle-name> from any surface (CLI, TUI, dashboard, any gateway platform) loads every referenced skill into a single combined user message. Use cases: - /backend-dev → loads github-code-review + test-driven-development + github-pr-workflow as one bundle. - /research → loads several research skills together. - Team task profiles shared via dotfiles. Behavior: - Bundles take precedence over individual skills when slugs collide. - Missing skills are skipped with a note, not fatal. - No system-prompt mutation — bundles generate a fresh user message at invocation time, the same way /<skill> does. Prompt cache stays intact. - Works in CLI dispatch, gateway dispatch, autocomplete (CLI + TUI), /help display. Schema (~/.hermes/skill-bundles/<slug>.yaml): name: backend-dev description: Backend feature work. skills: - github-code-review - test-driven-development instruction: | Optional extra guidance prepended to the loaded skills. New module: agent/skill_bundles.py — load, scan, resolve, build invocation message, save, delete. yaml.safe_load only; broken bundles log a warning and are skipped, never raise. New CLI subcommand: hermes bundles {list,show,create,delete,reload}. Implementation in hermes_cli/bundles.py; wired in hermes_cli/main.py. 'bundles' added to _BUILTIN_SUBCOMMANDS so plugin discovery skips it. New in-session slash command: /bundles lists installed bundles in both CLI and gateway. /<bundle-name> dispatch added to CLI (cli.py) and gateway (gateway/run.py) before the existing /<skill-name> path. Autocomplete: SlashCommandCompleter gained an optional skill_bundles_provider parameter that defaults to None — the prompt shows '▣ <description> (N skills)' for bundles vs '⚡' for skills. Tests: - tests/agent/test_skill_bundles.py — 33 tests covering slugify, scan/cache freshness, resolve (including underscore→hyphen Telegram alias), build_bundle_invocation_message (loading, missing skills, user/bundle instruction injection, dedup), save/delete, reload diff, list sort. - tests/hermes_cli/test_bundles.py — 8 tests for the CLI subcommand (create/list/show/delete/reload, --force, missing bundle errors). - tests/gateway/test_bundles_command.py — 4 tests for the gateway handler and bundle resolution priority. Live E2E: verified subprocess invocations of hermes bundles {list,create,show,reload,delete} round-trip correctly against an isolated HERMES_HOME. Docs: - website/docs/user-guide/features/skills.md — new 'Skill Bundles' section with quick example, YAML schema, management commands, behavior notes. - website/docs/reference/cli-commands.md — 'hermes bundles' added to the top-level command table and given its own subcommand section.17 天前
fix(termux): add local image chat route 1 个月前
fix(ci): stabilize main test suite regressions (#17660) * fix: stabilize main test suite regressions * test(agent): update MiniMax normalization expectation * test: stabilize remaining CI assertions * test: harden config helper monkeypatching * test: harden CI-only assertions * fix(agent): propagate fast streaming interrupts1 个月前
fix: warn and clear stale OPENAI_BASE_URL on provider switch (#5161) 1 个月前
chore: ruff auto-fix PLR6201 resweep — tuple → set in membership tests (#27355) Six days after #23937 (608 fixes) the codebase had accumulated 241 new PLR6201 violations. Same mechanical x in (...)x in {...} fix, same zero-risk profile: set lookup is O(1) vs O(n) for tuple and the two are semantically equivalent for hashable scalar membership tests. All 241 instances fixed via `ruff check --select PLR6201 --fix --unsafe-fixes`, zero remaining. Every changed value is a hashable scalar (str/int/None/enum/signal); no risk of unhashable runtime errors. No behavior change. Test plan: - 119 files changed, +244/-244 (net zero) — exactly one-line edits - ruff check clean afterward - Compile checks pass on the largest touched files (cli.py, run_agent.py, gateway/run.py, gateway/platforms/discord.py, model_tools.py) - Subset broad test run on tests/gateway/ tests/hermes_cli/ tests/agent/ tests/tools/: 18187 passed, 59 pre-existing failures (verified against origin/main with the same shape — identical failure count, identical category — all xdist test-order flakes unrelated to this change) Follows the same template as PR #23937 ([tracker: #23972](https://github.com/NousResearch/hermes-agent/issues/23972)).18 天前
fix(cli): handle unquoted multi-word session names in -c/--continue and -r/--resume When a user runs hermes -w -c Pokemon Agent Dev without quoting the session name, argparse would fail with: error: argument command: invalid choice: 'Agent' This is because argparse parses -c Pokemon (consuming one token via nargs='?'), then sees 'Agent' and tries to match it as a subcommand. Fix: add _coalesce_session_name_args() that pre-processes sys.argv before argparse, joining consecutive non-flag, non-subcommand tokens after -c or -r into a single argument. This makes both quoted and unquoted multi-word session names work transparently. Includes 17 tests covering all edge cases: multi-word names, single-word, bare flags, flag ordering, subcommand boundaries, and passthrough. 2 个月前
test(codex-spark): add live-API regression and make picker test deterministic Two follow-ups from self-review: 1. Add unit test for _fetch_models_from_api covering the live HTTP path. The salvaged PR #19530 dropped the supported_in_api:false filter in both _fetch_models_from_api and _read_cache_models, but only the cache path had a regression test. This adds the symmetric live-fetch test (mocked httpx) so a future drive-by change to the HTTP path can't silently re-introduce the filter. 2. Pin test_codex_picker_uses_live_codex_catalog to the cache fallback. The test wrote a fake JWT and a CODEX_HOME cache, but provider_model_ids ('openai-codex') still issued a real 10s HTTP probe to chatgpt.com/backend-api/codex/models before falling back to the cache. That made the test slow and non-deterministic in restricted/CI networks. Patch _fetch_models_from_api to return [] so we go straight to the cache path the test actually means to exercise. 25 天前
test(codex-spark): add live-API regression and make picker test deterministic Two follow-ups from self-review: 1. Add unit test for _fetch_models_from_api covering the live HTTP path. The salvaged PR #19530 dropped the supported_in_api:false filter in both _fetch_models_from_api and _read_cache_models, but only the cache path had a regression test. This adds the symmetric live-fetch test (mocked httpx) so a future drive-by change to the HTTP path can't silently re-introduce the filter. 2. Pin test_codex_picker_uses_live_codex_catalog to the cache fallback. The test wrote a fake JWT and a CODEX_HOME cache, but provider_model_ids ('openai-codex') still issued a real 10s HTTP probe to chatgpt.com/backend-api/codex/models before falling back to the cache. That made the test slow and non-deterministic in restricted/CI networks. Patch _fetch_models_from_api to return [] so we go straight to the cache path the test actually means to exercise. 25 天前
fix(codex-runtime): de-dup [plugins.X] tables and stop leaking HERMES_HOME into config.toml Builds on @steezkelly's Bug A fix (#25857, top-level default_permissions via _insert_managed_block_at_top_level) by addressing the other two config-corruption bugs described in #26250: Bug B (duplicate [plugins.X] tables) - Codex itself writes [plugins."<name>@<marketplace>"] tables to config.toml when the user runs codex plugins enable directly, before hermes-agent's managed block exists. On the next migrate run, _query_codex_plugins() re-discovers the same plugins via plugin/list and render_codex_toml_section() re-emits them inside the managed block. Codex's strict TOML parser then rejects the duplicate table header on startup. - Add _strip_unmanaged_plugin_tables() that drops [plugins.*] tables from the user-content portion of the file. Only run it when plugin/list succeeded — if the RPC failed we can't re-emit and must preserve the user's tables. plugin/list is the source of truth when it answers. Bug C (HERMES_HOME pytest-tempdir leak into ~/.codex/config.toml) - _build_hermes_tools_mcp_entry() read HERMES_HOME directly from os.environ, so a sibling pytest's monkeypatch.setenv("HERMES_HOME", tmp_path) silently burned a transient pytest tempdir into the user's real ~/.codex/config.toml. After pytest reaped the tempdir, every codex-routed hermes-tools tool call failed silently. - Derive HERMES_HOME from get_hermes_home() (the canonical resolver that goes through the profile-aware path) and refuse to emit obvious test-tempdir paths via _looks_like_test_tempdir() as belt-and-suspenders for any other callsite that forgets to patch migrate(). - test_enable_succeeds_when_codex_present in test_codex_runtime_switch.py invoked the real migrate() (no mock), writing to Path.home() / .codex using whatever HERMES_HOME the running pytest session had set. Add the same migrate patch the other apply() tests already use, so the suite stops touching the user's real ~/.codex/config.toml. E2E verification (replicating the issue's repro): - Pre-state config.toml with user [mcp_servers.omx_team_run] + codex-installed [plugins."tasks@openai-curated"], HERMES_HOME="/private/var/folders/.../pytest-of-.../..." - On origin/main: tomllib refuses to load the result with "Cannot declare ('plugins', 'tasks@openai-curated') twice" AND the pytest-tempdir HERMES_HOME is burned in. - On this branch: file parses cleanly, default_permissions is top-level, exactly one [plugins."tasks@openai-curated"] table inside the managed block, no HERMES_HOME in the MCP env. 7 new regression tests covering all three bugs + the test-leak guard. bash scripts/run_tests.sh tests/hermes_cli/test_codex_runtime_*.py — 95 passed, 0 failed. Closes #26250 20 天前
chore: ruff auto-fix PLR6201 resweep — tuple → set in membership tests (#27355) Six days after #23937 (608 fixes) the codebase had accumulated 241 new PLR6201 violations. Same mechanical x in (...)x in {...} fix, same zero-risk profile: set lookup is O(1) vs O(n) for tuple and the two are semantically equivalent for hashable scalar membership tests. All 241 instances fixed via `ruff check --select PLR6201 --fix --unsafe-fixes`, zero remaining. Every changed value is a hashable scalar (str/int/None/enum/signal); no risk of unhashable runtime errors. No behavior change. Test plan: - 119 files changed, +244/-244 (net zero) — exactly one-line edits - ruff check clean afterward - Compile checks pass on the largest touched files (cli.py, run_agent.py, gateway/run.py, gateway/platforms/discord.py, model_tools.py) - Subset broad test run on tests/gateway/ tests/hermes_cli/ tests/agent/ tests/tools/: 18187 passed, 59 pre-existing failures (verified against origin/main with the same shape — identical failure count, identical category — all xdist test-order flakes unrelated to this change) Follows the same template as PR #23937 ([tracker: #23972](https://github.com/NousResearch/hermes-agent/issues/23972)).18 天前
fix(gateway): prioritize Telegram command menu 15 天前
test(cli): strengthen zsh completion regression coverage 22 天前
fix(config): warn loudly on YAML parse failure instead of silent default fallback (#23585) A YAML parse error in ~/.hermes/config.yaml caused load_config() to print one line to stdout (Warning: Failed to load config: ...) and silently fall back to DEFAULT_CONFIG, dropping every user override (auxiliary providers, fallback chain, model settings). Users only noticed when downstream behavior misbehaved — see issue #23570 where a tab-indent error in the auxiliary section caused aux fallback to use OpenRouter (depleted) instead of the configured Codex/MiniMax chain. Now: log at WARNING (so 'hermes logs' surfaces it), write a prominent line to stderr, dedup on (path, mtime_ns, size) so concurrent loads don't spam, and re-warn after the user edits the file. Both call sites (raw read + merged load) route through the same helper. Refs #2357025 天前
feat(delegate): orchestrator role and configurable spawn depth (default flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR #11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com> 1 个月前
fix(ci): stabilize main test suite regressions (#17660) * fix: stabilize main test suite regressions * test(agent): update MiniMax normalization expectation * test: stabilize remaining CI assertions * test: harden config helper monkeypatching * test: harden CI-only assertions * fix(agent): propagate fast streaming interrupts1 个月前
fix(config): preserve env refs when save_config rewrites config (#11892) Co-authored-by: binhnt92 <84617813+binhnt92@users.noreply.github.com>1 个月前
fix(config): accept fallback_model list (chain) in validator + save Runtime already supports list-form fallback_model (run_agent.py:1459 iterates fallback_chain; fallback_cmd.py migrates legacy single-dict configs to list format). The config validator and save_config comment gate still assumed single-dict form and flagged list-form configs as errors. Fix both: - validate_config_structure: when fallback_model is a list, validate each entry has provider+model; keep the existing single-dict path. - save_config: suppress the "add fallback_model" comment when any list entry is well-formed. Adds 4 list-form validator tests. 1 个月前
fix(ci): stabilize main test suite regressions (#17660) * fix: stabilize main test suite regressions * test(agent): update MiniMax normalization expectation * test: stabilize remaining CI assertions * test: harden config helper monkeypatching * test: harden CI-only assertions * fix(agent): propagate fast streaming interrupts1 个月前
fix: remove 115 verified dead code symbols across 46 production files Automated dead code audit using vulture + coverage.py + ast-grep intersection, confirmed by Opus deep verification pass. Every symbol verified to have zero production callers (test imports excluded from reachability analysis). Removes ~1,534 lines of dead production code across 46 files and ~1,382 lines of stale test code. 3 entire files deleted (agent/builtin_memory_provider.py, hermes_cli/checklist.py, tests/hermes_cli/test_setup_model_selection.py). Co-authored-by: alt-glitch <balyan.sid@gmail.com> 1 个月前
fix(copilot): require successful exchange when walking credential_pool catalog tokens Address Copilot review on #16868: 1. Tighten pool iteration. validate_copilot_token only rejects empty strings and classic PATs (ghp_*); a malformed/unsupported gho_* token at credential_pool.copilot[0] would pass the gate and short- circuit the loop, hiding a later valid entry. Switch to calling exchange_copilot_token directly: only entries that actually exchange into a live Copilot API token are returned. Bad/expired entries fall through to the next, and an exhausted pool returns "" so the picker falls back to the curated list (existing behaviour). 2. Reword the docstring + test module docstring to describe the pool seed path accurately — hermes auth add copilot adds an api-key-typed credential whose access_token field stores the pasted token, and _seed_from_env mirrors COPILOT_GITHUB_TOKEN from ~/.hermes/.env into the pool. The previous wording implied auth add copilot itself ran the device-code flow, which it does not (the device-code flow lives in hermes model). Two new tests cover the iteration change: - test_skips_pool_entry_that_fails_to_exchange — pool[0] raises, pool[1] succeeds, picker uses pool[1]. - test_all_pool_entries_fail_exchange_returns_empty — every entry raises, return "". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 1 个月前
fix(copilot): wire live /models max_prompt_tokens into context-window resolver The Copilot provider resolved context windows via models.dev static data, which does not include account-specific models (e.g. claude-opus-4.6-1m with 1M context). This adds the live Copilot /models API as a higher- priority source for copilot/copilot-acp/github-copilot providers. New helper get_copilot_model_context() in hermes_cli/models.py extracts capabilities.limits.max_prompt_tokens from the cached catalog. Results are cached in-process for 1 hour. In agent/model_metadata.py, step 5a queries the live API before falling through to models.dev (step 5b). This ensures account-specific models get correct context windows while standard models still have a fallback. Part 1 of #7731. Refs: #7272 1 个月前
fix(model): repair Discord Copilot /model flow Keep Discord Copilot model switching responsive and current by refreshing picker data from the live catalog when possible, correcting the curated fallback list, and clearing stale controls before the switch completes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> 1 个月前
fix(copilot): exchange raw GitHub token for Copilot API JWT Raw GitHub tokens (gho_/github_pat_/ghu_) are now exchanged for short-lived Copilot API tokens via /copilot_internal/v2/token before being used as Bearer credentials. This is required to access internal-only models (e.g. claude-opus-4.6-1m with 1M context). Implementation: - exchange_copilot_token(): calls the token exchange endpoint with in-process caching (dict keyed by SHA-256 fingerprint), refreshed 2 minutes before expiry. No disk persistence — gateway is long-running so in-memory cache is sufficient. - get_copilot_api_token(): convenience wrapper with graceful fallback — returns exchanged token on success, raw token on failure. - Both callers (hermes_cli/auth.py and agent/credential_pool.py) now pipe the raw token through get_copilot_api_token() before use. 12 new tests covering exchange, caching, expiry, error handling, fingerprinting, and caller integration. All 185 existing copilot/auth tests pass. Part 2 of #7731. 1 个月前
feat: add cron job profile support 17 天前
feat(curator): add archive and prune subcommands (#20200) * fix(curator): protect hub skills by frontmatter name * test(skill_usage): add mark_agent_created to regression test The cherry-picked test predates #19618/#19621 which rewrote list_agent_created_skill_names() to require an explicit created_by: 'agent' provenance marker. Without mark_agent_created(), my-skill is excluded from the list and the positive assertion fails. * feat(curator): add archive and prune subcommands Adds 'hermes curator archive <skill>' and 'hermes curator prune [--days N] [--yes] [--dry-run]' alongside the existing status, run, pause, resume, pin, unpin, restore, backup, rollback verbs. These are the two genuinely new user-facing verbs requested in #19384. The other verbs proposed there ('stats' and 'restore') already exist as 'curator status' and 'curator restore', so no duplicate surface is added — all skill lifecycle commands live under the single 'hermes curator' namespace. - archive: manual archive of an agent-created skill. Refuses pinned skills with a hint pointing at 'hermes curator unpin'. - prune: bulk-archive unpinned skills idle for >= N days (default 90). Falls back to created_at when last_activity_at is null so never-used skills can still be pruned. --dry-run previews, --yes skips prompt. Adapted from @elmatadorgh's PR #19454 which placed the same verbs under 'hermes skills' with a separate hermes_cli/skills_config.py handler and rich table for stats. The 'stats' and 'restore' parts of that PR duplicated existing surface, so only archive and prune are kept, rewritten to match hermes_cli/curator.py's existing plain-text handler style. Tests rewritten from scratch against the new handlers. Closes #19384 Co-authored-by: elmatadorgh <coktinbaran5@gmail.com> --------- Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com> Co-authored-by: elmatadorgh <coktinbaran5@gmail.com>30 天前
feat(curator): show rename map in user-visible summary (#22910) * feat(curator): show rename map (where skills went) in user-visible summary The full data has always been on disk in REPORT.md, but the user-visible curator summary (gateway 💾 line, CLI session-start panel, hermes curator status) was counts-only — "consolidated 4 into 2 umbrellas" with no names. Users only discovered renames when something they expected was gone. New _build_rename_summary() formats the rename map and appends it to final_summary: auto: 1 marked stale; llm: consolidated 2 into 1, pruned 1 archived 3 skill(s): • docx-extraction → document-tools • pdf-extraction → document-tools • old-stale-thing — pruned (stale) full report: hermes curator status Empty on no-op ticks (no archives), so most ticks add zero log noise. Cap of 10 entries keeps agent.log readable when a 50-skill consolidation lands; the full list is always in REPORT.md. hermes curator status indents continuation lines so the multi-line summary reads as one logical field. 5 new tests in tests/agent/test_curator_classification.py covering empty / consolidation / pruning / cap / mixed cases. * feat(curator): show recent run summary once on hermes update The rename map is now visible from where users actually look — the update flow they explicitly run, instead of just the live gateway log or transient CLI session-start panel. Behavior: - After hermes update, if the most recent curator run produced a rename map (multi-line summary) that the user hasn't seen yet, print it once with a 'last run Xh ago' header and a one-time-message footer. - Stamp last_run_summary_shown_at = last_run_at after printing so subsequent hermes update invocations are silent until a newer curator run lands. - Silent on no-op runs (single-line summary like 'auto: no changes; llm: no change'). Still stamps shown so we don't reconsider on every update. - Silent when the curator has never run (the existing first-run notice handles that case). Output: ℹ Skill curator — last run 4h ago auto: 1 marked stale; llm: consolidated 2 into 1, pruned 1 archived 3 skill(s): • docx-extraction → document-tools • pdf-extraction → document-tools • old-stale-thing — pruned (stale) full report: hermes curator status (This message shows once per curator run. View anytime: hermes curator status) State migration: - _default_state() gains last_run_summary_shown_at: None. Existing state files lack the field; .get() returns None; the comparison treats any prior run as 'not yet shown' and prints once on next update. Self-healing. Wiring: - Both hermes update paths in main.py call the new _print_curator_recent_run_notice() right after the existing first-run notice. Best-effort try/except so a state-load bug never breaks the update flow. 6 tests in tests/hermes_cli/test_curator_recent_run_notice.py: no-run / single-line / multi-line / show-once / new-run-resets / time-formatter buckets.26 天前
fix(curator): make manual runs synchronous 28 天前
fix(curator): make manual runs synchronous 28 天前
fix(context): honor custom_providers context_length on /model switch + bump probe tier to 256K (#15844) Fixes #15779. Custom-provider per-model context_length (custom_providers[].models.<id>.context_length) is now honored across every resolution path, not just agent startup. Also adds 256K as the top probe tier and default fallback. ## What changed New helper hermes_cli.config.get_custom_provider_context_length() — single source of truth for the per-model override lookup, with trailing-slash-insensitive base-url matching. agent.model_metadata.get_model_context_length() gains an optional custom_providers= kwarg (step 0b — runs after explicit config_context_length but before every other probe). Wired through five call sites that previously either duplicated the lookup or ignored it entirely: - run_agent.py startup — refactored to use the new helper (dedups legacy inline loop, keeps invalid-value warning) - AIAgent.switch_model() — re-reads custom_providers from live config on every /model switch - hermes_cli.model_switch.resolve_display_context_length() — new custom_providers= kwarg - gateway/run.py /model confirmation (picker callback + text path) - gateway/run.py _format_session_info (/info) ## Context probe tiers CONTEXT_PROBE_TIERS = [256_000, 128_000, 64_000, 32_000, 16_000, 8_000] — was [128_000, ...]. DEFAULT_FALLBACK_CONTEXT follows tier[0], so unknown models now default to 256K. The stale 128000 literal in the OpenRouter metadata-miss path is replaced with DEFAULT_FALLBACK_CONTEXT for consistency. ## Repro (from #15779) ```yaml custom_providers: - name: my-custom-endpoint base_url: https://example.invalid/v1 model: gpt-5.5 models: gpt-5.5: context_length: 1050000 ``` /model gpt-5.5 --provider custom:my-custom-endpoint → previously "Context: 128,000", now "Context: 1,050,000". ## Tests - tests/hermes_cli/test_custom_provider_context_length.py — new file, 19 tests covering the helper, step-0b integration, and the 256K tier invariants - tests/hermes_cli/test_model_switch_context_display.py — added regression tests for #15779 through the display resolver - tests/gateway/test_session_info.py — updated default-fallback assertion (128K → 256K) - tests/agent/test_model_metadata.py — updated tier assertions for the new top tier1 个月前
fix(model): match custom provider by active base url 16 天前
Merge upstream/main and address Copilot review feedback Merge resolved conflicts in web/src/{i18n/{en,zh,types}.ts,lib/api.ts} by keeping both this branch's profiles additions and upstream's new models page additions. Copilot review feedback: - Implement POST /api/profiles/{name}/open-terminal endpoint (already present); align Windows branch to cmd.exe /c start "" <cmd> so it matches the new test and spawns a fresh window instead of /k reusing the parent console. - Move backslash escaping out of the macOS AppleScript f-string expression (Python <3.12 disallows backslashes inside f-string expression parts). - Patch _get_wrapper_dir via monkeypatch in test_profiles_create_creates_wrapper_alias_when_safe so the test no longer writes to the real ~/.local/bin. - Extend test_dashboard_browser_safe_imports to scan .ts files in addition to .tsx. - Switch upstream's new ModelsPage.tsx away from the @nous-research/ui root barrel onto per-component subpaths to satisfy the stricter scan. - Fix NouiTypography leading-1.4 -> leading-[1.4] so Tailwind actually emits the line-height for the sm variant. - Guard ProfilesPage.openSoulEditor against out-of-order responses by tracking the latest requested profile via a ref. - Replace ProfilesPage's hand-rolled setup command with a fetch to /api/profiles/{name}/setup-command so the copied command always matches what the backend would actually run (handles wrapper-alias collisions and reserved names correctly). - Wire SOUL.md textarea label htmlFor -> textarea id so screen readers and clicking the label work as expected. 1 个月前
feat(dashboard): add --stop and --status flags (#17840) hermes dashboard is a long-lived foreground server that users often start and forget about, sometimes in a shell they've since closed. We didn't have a way to stop it — users had to find the PID manually. Adds two lifecycle flags that reuse the same detection + termination path the post-hermes update cleanup (PR #17832) uses: hermes dashboard --status List running hermes dashboard processes with PID + cmdline. Exit 0, informational. hermes dashboard --stop Terminate all running dashboards (3s grace then force-kill survivors). Exit 0 if none remain, 1 if any couldn't be stopped. Windows uses taskkill /F as before. Both flags short-circuit before any fastapi/uvicorn import so they work even on installations where the dashboard extras aren't installed — useful when you're cleaning up after uninstalling. The kill helper gained an optional reason=... param so the output reads "(requested via --stop)" instead of the post-update-specific "running backend no longer matches the updated frontend" wording. E2E: hermes dashboard --status with nothing running prints the empty message; with a fake hermes dashboard ... cmdline spawned via exec -a, --status lists it, --stop terminates it (exit -15), and a follow-up --status returns empty.1 个月前
fix(dashboard): UI polish — modals, layout, consistency, test fixes Dashboard UX polish pass — consolidates create forms into modals triggered from the page header, fixes layout inconsistencies, adds scroll-to navigation for the Keys page, and aligns the TokenBar with the design system. Changes: - App.tsx: add padding to sidebar header - resolve-page-title.ts: add missing routes, better fallback title - en.ts: fix nav labels (Profiles was 'profiles : multi agents') - ModelsPage: two-col layout, auxiliary tasks modal, TokenBar redesign - ProfilesPage: create button in header, form in modal, Checkbox component - CronPage: create button in header, form in modal - EnvPage: scroll-to sub-nav in header, fix text overflow Modal and dialog standardization: - Replace all native confirm()/window.confirm() with ConfirmDialog (OAuthProvidersCard, PluginsPage, ModelsPage, ConfigPage) - Add useModalBehavior hook (Escape-to-close, scroll lock, focus restore) - Apply hook to ProfilesPage, CronPage, AuxiliaryTasksModal Component fixes (from PR review): - Checkbox: fix controlled/uncontrolled mismatch, add focus-visible ring - TokenBar: add rounded-full to legend dots, remove dead code CI/test fixes: - Fix TS unused imports (noUnusedLocals), type-narrow PickerTarget union - Add windows-footgun suppression on platform-guarded os.killpg - Fix 19 stale unit tests + 9 e2e tests broken by recent main changes - Restore minimal example-dashboard plugin for plugin auth test 23 天前
feat(security): enable secret redaction by default (#17691, #20785) (#21193) Flip the default for HERMES_REDACT_SECRETS from off to on so the redactor already wired into send_message_tool, logs, and tool output actually runs on a fresh install. - agent/redact.py: env-var default "" → "true" - hermes_cli/config.py: DEFAULT_CONFIG security.redact_secrets True; two config-template comments rewritten - gateway/run.py + cli.py: startup log / banner warning when the user has explicitly opted out, so the downgrade is visible in agent.log and at CLI banner time - docs/reference/environment-variables.md: description reconciled - tests: flipped the default-pin, restructured the force=True regression test to explicit-false instead of unset Users who need raw credential values (redactor development) can still opt out via security.redact_secrets: false in config.yaml or HERMES_REDACT_SECRETS=false in .env. Closes #17691. Addresses #20785 (short-term output-pipeline recommendation).28 天前
feat(dep_ensure): complete Windows bootstrap — dep_ensure + install.ps1 + detection (#27845) * feat(dep_ensure): complete Windows bootstrap — dep_ensure + install.ps1 + detection dep_ensure.py gains Windows awareness: PowerShell invocation, platform- specific browser detection, (path, shell) tuple returns. install.ps1 gains -Ensure/-PostInstall modes using npm -g --prefix (aligned with install.sh) and agent-browser install for Chromium. browser_tool.py gains node/ in candidate dirs for Windows .cmd shims. Both install scripts bundled in pip wheel. Tracking: #27826 * fix(install.ps1): add --ignore-scripts to npm install for camofox @askjo/camofox-browser has a dependency (impit) whose postinstall script runs npx only-allow pnpm, which fails under npm. Adding --ignore-scripts avoids the spurious failure without affecting functionality. Tracking: #27826 * fix: remove duplicate install scripts from git CI already copies scripts/install.{sh,ps1} into hermes_cli/scripts/ during wheel build. No need to commit copies — .gitignore keeps them out, _find_install_script() falls back to scripts/ for git-clone users. Tracking: #27826 * fix: address review — remove env_extra, fix ps1 error handling - Remove unused env_extra parameter from ensure_dependency() - Invoke-EnsureMode node case now uses Test-Node consistently - Install-AgentBrowser uses throw instead of exit 117 天前
fix: enforce config.yaml as sole CWD source + deprecate .env CWD vars + add hermes memory reset (#11029) config.yaml terminal.cwd is now the single source of truth for working directory. MESSAGING_CWD and TERMINAL_CWD in .env are deprecated with a migration warning. Changes: 1. config.py: Remove MESSAGING_CWD from OPTIONAL_ENV_VARS (setup wizard no longer prompts for it). Add warn_deprecated_cwd_env_vars() that prints a migration hint when deprecated env vars are detected. 2. gateway/run.py: Replace all MESSAGING_CWD reads with TERMINAL_CWD (which is bridged from config.yaml terminal.cwd). MESSAGING_CWD is still accepted as a backward-compat fallback with deprecation warning. Config bridge skips cwd placeholder values so they don't clobber the resolved TERMINAL_CWD. 3. cli.py: Guard against lazy-import clobbering — when cli.py is imported lazily during gateway runtime (via delegate_tool), don't let load_cli_config() overwrite an already-resolved TERMINAL_CWD with os.getcwd() of the service's working directory. (#10817) 4. hermes_cli/main.py: Add 'hermes memory reset' command with --target all/memory/user and --yes flags. Profile-scoped via HERMES_HOME. Migration path for users with .env settings: Remove MESSAGING_CWD / TERMINAL_CWD from .env Add to config.yaml: terminal: cwd: /your/project/path Addresses: #10225, #4672, #10817, #76631 个月前
feat: confirm prompt for destructive slash commands (#4069) (#22687) /clear, /new, /reset, and /undo now ask the user to confirm before discarding conversation state — three-option prompt routed through the existing tools.slash_confirm primitive. Native yes/no buttons render on Telegram, Discord, and Slack (their adapters already implement send_slash_confirm); other platforms get a text-fallback prompt and reply with /approve, /always, or /cancel. The classic prompt_toolkit CLI uses the same three-option flow via the established _prompt_text_input pattern (see _confirm_and_reload_mcp). TUI keeps its existing modal overlay (#12312). Gated by new config key approvals.destructive_slash_confirm (default true). Picking 'Always Approve' flips the gate to false so subsequent destructive commands run silently — matches the established mcp_reload_confirm UX. Out of scope: /cron remove (separate domain — scheduled jobs, not session history). Existing TUI overlay env-var (HERMES_TUI_NO_CONFIRM) left unchanged; cosmetic unification can come later. Closes #4069.26 天前
fix: restrict provider URL detection to exact hostname matches 1 个月前
fix: extend hostname-match provider detection across remaining call sites Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the two openai/xai sites in run_agent.py. This finishes the sweep: the same substring-match false-positive class (e.g. https://api.openai.com.evil/v1, https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1) existed in eight more call sites, and the hostname helper was duplicated in two modules. - utils: add shared base_url_hostname() (single source of truth). - hermes_cli/runtime_provider, run_agent: drop local duplicates, import from utils. Reuse the cached AIAgent._base_url_hostname attribute everywhere it's already populated. - agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg selection to hostname equality. - run_agent: native-anthropic check in the Claude-style model branch and in the AIAgent init provider-auto-detect branch. - agent/model_metadata: Anthropic /v1/models context-length lookup. - hermes_cli/providers.determine_api_mode: anthropic / openai URL heuristics for custom/unknown providers (the /anthropic path-suffix convention for third-party gateways is preserved). - tools/delegate_tool: anthropic detection for delegated subagent runtimes. - hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint native-OpenAI detection (paired with deduping the repeated check into a single is_native_openai boolean per branch). Tests: - tests/test_base_url_hostname.py covers the helper directly (path-containing-host, host-suffix, trailing dot, port, case). - tests/hermes_cli/test_determine_api_mode_hostname.py adds the same regression class for determine_api_mode, plus a test that the /anthropic third-party gateway convention still wins. Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP. 1 个月前
test(dingtalk): cover QR device-flow auth + OpenClaw branding disclosure Adds 15 regression tests for hermes_cli/dingtalk_auth.py covering: * _api_post — network error mapping, errcode-nonzero mapping, success path * begin_registration — 2-step chain, missing-nonce/device_code/uri error cases * wait_for_registration_success — success path, missing-creds guard, on_waiting callback invocation * render_qr_to_terminal — returns False when qrcode missing, prints when available * Configuration — BASE_URL default + override, SOURCE default Also adds a one-line disclosure in dingtalk_qr_auth() telling users the scan page will be OpenClaw-branded. Interim measure: DingTalk's registration portal is hardcoded to route all sources to /openapp/ registration/openClaw, so users see OpenClaw branding regardless of what 'source' value we send. We keep 'openClaw' as the source token until DingTalk-Real-AI registers a Hermes-specific template. Also adds meng93 to scripts/release.py AUTHOR_MAP. 1 个月前
test: add tests for cmd_key preservation through name clamping - TestClampCommandNamesTriples: unit tests for 3-tuple support in _clamp_command_names (short names, long names, collisions, multiple entries, backward compat with 2-tuples) - TestDiscordSkillCmdKeyDispatch: integration test through the full discord_skill_commands pipeline verifying long skill names retain their original cmd_key after clamping - Add contributor CharlieKerfoot to AUTHOR_MAP 1 个月前
fix(doctor): attach codex CLI hint to OpenAI Codex auth warning for #27975 hermes doctor printed 'codex CLI not installed (optional — ...)' as a generic info line at the bottom of the auth section, several rows below 'OpenAI Codex auth (not logged in)' and after MiniMax/Gemini auth checks. Users reading sequentially mistook it for MiniMax-related advice. Move the hint up under the Codex auth warning so it's adjacent to the row it actually pertains to. Behavior unchanged when the codex CLI is installed (success path keeps its 'codex CLI ✓' row at the bottom). Tests cover both placement and suppression cases. Salvage of @xxxigm's 3-commit stack (#27986). Closes #27975. 16 天前
feat(doctor): add Command Installation check for hermes bin symlink hermes doctor now checks whether the ~/.local/bin/hermes symlink exists and points to the correct venv entry point. With --fix, it creates or repairs the symlink automatically. Covers: - Missing symlink at ~/.local/bin/hermes (or $PREFIX/bin on Termux) - Symlink pointing to wrong target - Missing venv entry point (venv/bin/hermes or .venv/bin/hermes) - PATH warning when ~/.local/bin is not on PATH - Skipped on Windows (different mechanism) Addresses user report: 'python -m hermes_cli.main doesn't have an option to fix the local bin/install' 10 new tests covering all scenarios. 1 个月前
fix(doctor): skip pluggable provider profiles when a dedicated check exists (#22346) Problem ------- hermes doctor ran two health checks for Anthropic: a dedicated one with the correct x-api-key + anthropic-version headers, and a generic Bearer-auth one driven by the pluggable ProviderProfile for "anthropic". The generic check called https://api.anthropic.com/v1/models with Authorization: Bearer ..., which Anthropic answers with HTTP 404, producing a noisy duplicate warning even when the dedicated check passed. Root cause ---------- hermes_cli/doctor.py:_build_apikey_providers_list deduplicated profiles against a _known_canonical set built from the static list (Z.AI/GLM, Kimi, DeepSeek, …). Providers with their own dedicated check above the generic loop (Anthropic, OpenRouter, Bedrock) were not in that set, so their profiles were appended and ran a second, broken check. Fix --- Add {"anthropic", "openrouter", "bedrock"} to the skip set, and also skip profiles whose aliases match any of those names (e.g. claude, claude-oauth → anthropic). Tests ----- tests/hermes_cli/test_doctor_dedicated_provider_skip.py: - test_build_apikey_providers_list_skips_dedicated_check_providers: asserts the assembled list does not contain anthropic, openrouter, or bedrock entries. - test_build_apikey_providers_list_includes_non_dedicated_providers: sanity guard that legitimate providers (DeepSeek, Z.AI/GLM) survive. Both confirmed via stash-verify (fail pre-fix with anthropic/openrouter leaking, pass post-fix). Fixes #22346 26 天前
perf(tools): cache get_nous_auth_status() and load_env() to fix slow hermes tools menus (#25341) hermes tools -> "All Platforms" took ~14s to render the checklist because building the toolset labels called get_nous_auth_status() ~31x transitively (_toolset_has_keys -> _visible_providers -> get_nous_subscription_features -> managed_nous_tools_enabled). Each call did a synchronous OAuth refresh POST to portal.nousresearch.com (~350ms even on the failure path), so one menu paint burned >13s of HTTP and 31 single-use Nous refresh tokens. Secondary hot spot: every get_env_value() re-read and re-sanitised the entire .env file. 116 reads with O(lines x known-keys) scanning added ~300ms of CPU per render. Fix is two process-level caches, both mtime-keyed so login/logout/edit invalidate naturally: * hermes_cli/auth.py: memoise get_nous_auth_status() for 15s keyed on auth.json mtime. Splits _compute_nous_auth_status() as the uncached impl. Adds invalidate_nous_auth_status_cache(). * hermes_cli/config.py: memoise load_env() keyed on .env (path, mtime, size). Adds invalidate_env_cache(), wired into save_env_value, remove_env_value, and the sanitize-on-load writer so writers don't return stale dicts on same-second writes. Before/after on Teknium's box (real HERMES_HOME, no Nous login): * "All Platforms" cold path: ~13,874ms -> ~691ms label-build * Warm re-open within the same process: ~122ms -> ~17ms Side benefit: stops burning a Nous refresh token on every menu paint, which was risking the portal's reuse-detection revocation logic.22 天前
clarify placeholder telegram credential in tests 1 个月前
clarify placeholder telegram credential in tests 1 个月前
feat(cli): add 'hermes fallback' command to manage fallback providers (#16052) Manage the fallback_providers chain from the CLI instead of hand-editing config.yaml. The picker reuses select_provider_and_model() from 'hermes model' — same provider list, same credential prompts, same model picker. hermes fallback [list] Show the current chain (primary + fallbacks) hermes fallback add Run the model picker, append selection to chain hermes fallback remove Pick an entry to delete (arrow-key menu) hermes fallback clear Remove all entries (with confirmation) 'add' snapshots config['model'] before calling the picker, extracts the user's selection from the post-picker state, then restores the primary and appends {provider, model, base_url?, api_mode?} to fallback_providers. Auth store's active_provider is snapshot/restored too so OAuth-provider fallbacks don't silently deactivate the user's primary. Duplicates and self-as-fallback are rejected. Legacy single-dict 'fallback_model' entries are auto-migrated to the list format on first write.1 个月前
fix(gateway): harden Windows gateway install lifecycle Preserve Windows profile install decisions across UAC handoff, avoid visible console windows by launching via pythonw, make repeated install/start idempotent, recreate stale Scheduled Tasks, and separate start-now from login auto-start behavior. Add Windows gateway regression coverage and systemd setup tests for the shared install flow. 16 天前
fix(termux): disable gateway service flows on android 1 个月前
fix(install): use --extra all not --all-extras; drop lazy-covered extras from [all] (#24515) * fix(install): use --extra all not --all-extras; drop lazy-covered extras from [all] Two coupled fixes for the Windows install hang where uv sync built python-olm from sdist and failed on missing make. # Root cause: --all-extras vs --extra all (credit: ethernet) uv sync --all-extras installs every key in [project.optional- dependencies], bypassing the curated [all] extra entirely. So even when [all] excluded [matrix], [rl], [yc-bench], etc., the installer pulled them anyway because they were still defined as extras. On Windows that meant python-olm (no wheel, needs make to build from sdist) and the install died there. The right flag is --extra all — install just the [all] extra's contents, respecting curation. Empirically verified via dry-run: --all-extras: pulls python-olm, mautrix, ctranslate2, onnxruntime, atroposlib, tinker, wandb, modal, daytona, vercel, python-telegram-bot, discord.py, slack-bolt, dingtalk-stream, lark-oapi, anthropic, boto3, edge-tts, elevenlabs, exa-py, fal-client, faster- whisper, firecrawl-py, honcho-ai, parallel-web --extra all: pulls none of those — just [all]'s curated set Dockerfile already uses --extra all (with comment explaining the gotcha) — knowledge existed; the gap was install.sh / install.ps1 / setup-hermes.sh. Sites fixed: scripts/install.sh L1118, scripts/install.ps1 L809, setup-hermes.sh L245. # Companion fix: drop lazy-covered extras from [all] tools/lazy_deps.py already covers anthropic, bedrock, exa, firecrawl, parallel-web, fal, edge-tts, elevenlabs, modal, daytona, vercel, all messaging platforms (telegram/discord/slack/matrix/ dingtalk/feishu), honcho, and faster-whisper. They were ALSO in [all], which defeats the whole point of lazy-install — fresh installs eager-pulled them and inherited whatever was broken upstream (the matrix → python-olm → no Windows wheel chain being the proximate symptom). [all] now contains only what genuinely can't be lazy-installed: cron, cli, dev, pty, mcp, homeassistant, sms, acp, google, web, youtube. Same trim applied to [termux-all]. New regression test asserts the contract: every extra in LAZY_DEPS must NOT also appear in [all]. # Companion fix: surface uv progress + errors setup-hermes.sh's hash-verified path swallowed uv's stderr to a tempfile, identical to the install.sh bug fixed in PR #24504. Same fix applied: stream stderr through directly so users see live progress instead of staring at a frozen prompt. # Files - pyproject.toml: trim [all] and [termux-all] to non-lazy extras only. - scripts/install.sh: --all-extras → --extra all; trim _ALL_EXTRAS / _PYPI_EXTRAS to match. - scripts/install.ps1: --all-extras → --extra all; trim $allExtras / $pypiExtras to match. - setup-hermes.sh: --all-extras → --extra all; stream stderr. - tests/test_project_metadata.py: invert matrix-in-[all] assertion; add lazy-coverage contract test. - uv.lock: regenerated. # Validation 5/5 metadata tests pass. 37/37 in update_autostash + tool_token_ estimation. uv lock --check passes. Empirical dry-run confirms --extra all excludes python-olm + RL chain on the new lockfile. * fix(install): parse [all] from pyproject.toml instead of mirroring it ethernet's review point: the previous patch left two hand-mirrored copies of [all]'s contents (in install.sh's $_ALL_EXTRAS and install.ps1's $allExtras). That guarantees future drift the next time pyproject.toml's [all] changes. Now both scripts parse pyproject.toml at install time using stdlib tomllib (Python 3.11+, which the bootstrap step already requires). Single source of truth. The only purpose of the parsed list is to build the 'Tier 2: [all] minus broken extras' fallback spec — so we parse, filter against $brokenExtras, and rebuild the .[a,b,c] spec. Also: removed redundant fallback tiers. Before: Tier 1 [all] Tier 2 [all] minus broken Tier 3 PyPI-only extras (no git deps) Tier 4 [web,mcp,cron,cli,messaging,dev] Tier 5 . After: Tier 1 [all] Tier 2 [all] minus broken Tier 3 . Tier 3 (PyPI-only) and Tier 4 (dashboard+core) used to dodge the [rl] git+sdist deps and the [matrix] python-olm build. Both are no longer in [all] post-2026-05-12 lazy-install migration, so the carve-out tiers had no remaining content. Tier 4 also referenced [messaging], which is now lazy-installed — the hardcoded fallback was actually inconsistent with the new policy. Defensive fallback: if tomllib parse fails (corrupted pyproject, unexpected schema), Tier 2 collapses to '.[all]' (same as Tier 1) so the broken-extras path becomes a no-op rather than crashing. * fix(gateway): hide Matrix from setup picker on Windows Matrix is the one messaging platform that has no working install path on Windows: [matrix] -> mautrix[encryption] -> python-olm, which has Linux-only wheels and needs make + libolm to build from sdist. The [all] cleanup in this PR keeps mautrix out of fresh installs, but a user who picked Matrix in 'hermes setup gateway' would still walk into the same sdist build failure when the wizard tried to install the extra. Hide the option at the picker so users never get the chance to try. The gate lives in _all_platforms() — single source of truth for the setup wizard, the curses gateway-config menu, and any future picker. Adapter loading at runtime is intentionally NOT gated: users who already have MATRIX_* env vars set (e.g. config copied from a Linux install) keep working if they somehow have python-olm available. This is the lowest-friction fix — picker visibility only. Tests cover linux/darwin/win32 and verify other platforms aren't collateral damage.23 天前
fix(gateway): detect gateway process via /proc in Docker without procps Salvage of NousResearch/hermes-agent#7622. Docker images often lack procps so ps is unavailable. Try reading /proc/*/cmdline first (works in any Linux container) and fall back to ps -A eww only when /proc is not present. PermissionError on individual PIDs is silently skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> 26 天前
fix(gateway): harden Telegram polling conflict handling - detect Telegram getUpdates conflicts and stop polling cleanly instead of retry-spamming forever - add a machine-local token-scoped lock so different HERMES_HOME profiles on the same host can't poll the same bot token at once - persist gateway runtime health/fatal adapter state and surface it in ● hermes-gateway.service - Hermes Agent Gateway - Messaging Platform Integration Loaded: loaded (/home/teknium/.config/systemd/user/hermes-gateway.service; enabled; preset: enabled) Active: active (running) since Sat 2026-03-14 09:25:35 PDT; 2h 45min ago Invocation: 8879379b25994201b98381f4bd80c2af Main PID: 1147926 (python) Tasks: 16 (limit: 76757) Memory: 151.4M (peak: 168.1M) CPU: 47.883s CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/hermes-gateway.service ├─1147926 /home/teknium/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main gateway run --replace └─1147966 node /home/teknium/.hermes/hermes-agent/scripts/whatsapp-bridge/bridge.js --port 3000 --session /home/teknium/.hermes/whatsapp/session --mode self-chat Mar 14 09:27:03 teknium-dev python[1147926]: 🔄 Retrying API call (2/3)... Mar 14 09:27:04 teknium-dev python[1147926]: [409B blob data] Mar 14 09:27:04 teknium-dev python[1147926]: Content: '' Mar 14 09:27:04 teknium-dev python[1147926]: ❌ Max retries (3) for empty content exceeded. Mar 14 09:27:07 teknium-dev python[1147926]: [1K blob data] Mar 14 09:27:07 teknium-dev python[1147926]: Content: '' Mar 14 09:27:07 teknium-dev python[1147926]: 🔄 Retrying API call (1/3)... Mar 14 09:27:12 teknium-dev python[1147926]: [1.7K blob data] Mar 14 09:27:12 teknium-dev python[1147926]: Content: '' Mar 14 09:27:12 teknium-dev python[1147926]: 🔄 Retrying API call (2/3)... ⚠ Installed gateway service definition is outdated Run: hermes gateway restart # auto-refreshes the unit ✓ Gateway service is running ✓ Systemd linger is enabled (service survives logout) - cleanly exit non-retryable startup conflicts without triggering service restart loops Tests: - gateway status runtime-state helpers - Telegram token-lock and polling-conflict behavior - GatewayRunner clean exit on non-retryable startup conflict - CLI runtime health summary 2 个月前
ci(tests): add pytest-timeout 60s hard cap to break suite-teardown deadlock (#28861) * ci(tests): add pytest-timeout 60s hard cap to break suite-teardown deadlock The full pytest suite reliably hangs at ~96% on origin/main, blowing through the 20-minute GHA job timeout on every CI push since yesterday. Individual tests complete in <30s — the deadlock builds up at session teardown after all tests run, when leaked threads and atexit handlers from thousands of tests interact and one of them lands in a futex-wait that never resolves. This PR is a stopgap that unblocks CI immediately + speeds up several slow tests we found while diagnosing. Changes - pyproject.toml: add pytest-timeout==2.4.0 to dev deps; bake --timeout=60 --timeout-method=thread into the default addopts. - scripts/run_tests.sh: re-add --timeout flags directly because the script wipes pyproject addopts with -o 'addopts='. - .github/workflows/tests.yml: explicit --timeout/--timeout-method on the CI pytest invocation for clarity. - gateway/run.py: in _run_agent, if the stream consumer was never created (e.g. non-streaming agent or test stub), cancel the stream_task immediately instead of waiting out the 5s wait_for timeout. ~5s saved per non-streaming gateway test run. - tests/run_agent/conftest.py: extend _fast_retry_backoff to patch agent.conversation_loop.jittered_backoff alongside run_agent.jittered_backoff. The retry loop was extracted into agent.conversation_loop which holds its own import — patching the run_agent reference alone left tests burning real wall-clock backoff seconds. - tests/run_agent/test_anthropic_error_handling.py tests/run_agent/test_run_agent.py (TestRetryExhaustion) tests/run_agent/test_fallback_model.py: same conversation_loop fix for per-test fixtures (defensive — the conftest covers them too). - tests/gateway/test_gateway_inactivity_timeout.py: trim run_duration 10.0 → 2.0 / 5.0 → 2.0 on three tests that wait the full SlowFakeAgent duration. Adjusted thresholds proportionally. - tests/gateway/test_api_server_runs.py: test_stop_interrupt_exception_does_not_crash trips the interrupted event in addition to raising, so the slow_run thread unblocks at teardown instead of waiting 10s. - tests/hermes_cli/test_update_gateway_restart.py: also patch time.monotonic in the autouse fixture. _wait_for_service_active loops on a wall-clock deadline; with sleep no-op'd the loop spun on real monotonic until 10s real-time per restart attempt (20s+ per test). - tests/tools/test_zombie_process_cleanup.py: cut runner._restart_drain_timeout 5.0 → 0.1 in test_gateway_stop_calls_close. Suite still hangs at 96% on full no-timeout runs; with these changes CI runs through to a real pass/fail signal. * chore(lock): regenerate uv.lock after adding pytest-timeout * ci: drop pytest-timeout 60 → 30s + bump GHA job 20 → 30 min Prior commit's timeout=60 was too generous — CI test job still hit the 20-min wall-clock cap with the suite hung at 96% (orphan agent-browser subprocesses blocking pytest session teardown). The local timeout=20 run completed in 6:17, so 30s is conservative enough to let real tests finish but aggressive enough to short-circuit deadlocks. Also bump GHA job timeout to 30 min as a safety margin. * test: delete 11 pre-existing failing tests + revert monotonic patch The previous PR commit landed pytest-timeout=30s and the suite now completes in 18:14 instead of hanging at 96%, but 11 pre-existing tests fail with real assertions. Per Teknium: nuke them. Deleted (no replacements): - tests/gateway/test_restart_resume_pending.py::test_clean_drain_does_not_mark_resume_pending - tests/gateway/test_restart_resume_pending.py::test_drain_timeout_only_marks_still_running_sessions - tests/hermes_cli/test_gateway_service.py::TestGatewaySystemServiceRouting::test_gateway_install_passes_system_flags - tests/hermes_cli/test_gateway_wsl.py::TestGatewayCommandWSLMessages::test_install_wsl_with_systemd_warns - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_detects_launchd_and_skips_manual_restart_message - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_restarts_profile_manual_gateways - tests/tools/test_file_operations.py::TestGitBaselineCheck::* (6 tests, entire class — _check_git_baseline helper doesn't exist) Also reverted my time.monotonic autouse-fixture hack in test_update_gateway_restart.py — it was causing worker crashes in CI by poisoning later tests in the same xdist worker. The two slow tests in that file (~24s and ~20s) will go back to taking real time but should still finish under the 30s pytest-timeout. * test: delete more pre-existing CI failures After previous push 3 more tests failed on CI; cull them all. Removed: - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_without_launchd_shows_manual_restart - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_profile_manual_gateway_falls_back_to_sigterm - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateResetFailedBeforeRestart::test_reset_failed_also_runs_before_retry_restart - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateResetFailedBeforeRestart::test_final_failure_message_tells_user_to_reset_failed - tests/run_agent/test_tool_call_args_sanitizer.py::test_marker_message_inserted_when_missing The 4 update_gateway_restart tests trigger _wait_for_service_active polling on a real wall-clock deadline that occasionally exceeds the 30s pytest-timeout cap and crashes xdist workers. The marker test has a pre-existing assertion mismatch. * test: nuke entire TestCmdUpdateLaunchdRestart class After surgical deletes of 4 tests this class keeps producing new worker-crashing tests. The pattern is consistent: any test in this class that triggers cmd_update's _wait_for_service_active polling spins on real wall-clock time and trips pytest-timeout's thread method, crashing the xdist worker. Just delete the whole class (285 lines, ~10 tests). These exercise macOS-only launchd behavior that's better tested on a real macOS runner than in linux xdist. * test: stub the 2 fallback_model tests that crash xdist workers on CI * test: delete test_anthropic_error_handling.py + test_fallback_model.py entirely These two files exercise the agent retry/fallback code paths and consistently crash xdist workers under pytest-timeout's thread method. Whack-a-mole-stubbing individual tests just surfaces the next ones. Nuke both files. * test: delete tests/hermes_cli/test_update_gateway_restart.py entirely This file's cmd_update integration tests consistently crash xdist workers under pytest-timeout's thread method. Surgical deletes just surface the next set. Removing the whole file. * ci(tests): switch pytest-timeout method thread → signal Thread-method has been crashing xdist workers when it interrupts code that's not interruption-safe (retry loops, threading.Event waits, etc). Signal method uses SIGALRM which is interpreter-level and cleanly raises a Failed: Timeout exception in test code. Should stop the worker crash cascade — failures will surface as proper Timeout markers we can diagnose individually.16 天前
fix(gateway): build service PATH from existing dirs only, include ~/.hermes/node_modules Extract PATH building into _build_service_path_dirs() that skips directories which don't exist on disk (e.g. node_modules/.bin for pip installs) and also includes ~/.hermes/node/bin and ~/.hermes/node_modules/.bin for agent-browser. 20 天前
test(gateway-windows): make ctypes.windll monkeypatch tolerant on non-Windows Linux/macOS CI runners don't have ctypes.windll, so the elevated-gateway test fails at module load. Adding raising=False lets monkeypatch install the mock attribute without first requiring it to exist. 16 天前
ci(tests): add pytest-timeout 60s hard cap to break suite-teardown deadlock (#28861) * ci(tests): add pytest-timeout 60s hard cap to break suite-teardown deadlock The full pytest suite reliably hangs at ~96% on origin/main, blowing through the 20-minute GHA job timeout on every CI push since yesterday. Individual tests complete in <30s — the deadlock builds up at session teardown after all tests run, when leaked threads and atexit handlers from thousands of tests interact and one of them lands in a futex-wait that never resolves. This PR is a stopgap that unblocks CI immediately + speeds up several slow tests we found while diagnosing. Changes - pyproject.toml: add pytest-timeout==2.4.0 to dev deps; bake --timeout=60 --timeout-method=thread into the default addopts. - scripts/run_tests.sh: re-add --timeout flags directly because the script wipes pyproject addopts with -o 'addopts='. - .github/workflows/tests.yml: explicit --timeout/--timeout-method on the CI pytest invocation for clarity. - gateway/run.py: in _run_agent, if the stream consumer was never created (e.g. non-streaming agent or test stub), cancel the stream_task immediately instead of waiting out the 5s wait_for timeout. ~5s saved per non-streaming gateway test run. - tests/run_agent/conftest.py: extend _fast_retry_backoff to patch agent.conversation_loop.jittered_backoff alongside run_agent.jittered_backoff. The retry loop was extracted into agent.conversation_loop which holds its own import — patching the run_agent reference alone left tests burning real wall-clock backoff seconds. - tests/run_agent/test_anthropic_error_handling.py tests/run_agent/test_run_agent.py (TestRetryExhaustion) tests/run_agent/test_fallback_model.py: same conversation_loop fix for per-test fixtures (defensive — the conftest covers them too). - tests/gateway/test_gateway_inactivity_timeout.py: trim run_duration 10.0 → 2.0 / 5.0 → 2.0 on three tests that wait the full SlowFakeAgent duration. Adjusted thresholds proportionally. - tests/gateway/test_api_server_runs.py: test_stop_interrupt_exception_does_not_crash trips the interrupted event in addition to raising, so the slow_run thread unblocks at teardown instead of waiting 10s. - tests/hermes_cli/test_update_gateway_restart.py: also patch time.monotonic in the autouse fixture. _wait_for_service_active loops on a wall-clock deadline; with sleep no-op'd the loop spun on real monotonic until 10s real-time per restart attempt (20s+ per test). - tests/tools/test_zombie_process_cleanup.py: cut runner._restart_drain_timeout 5.0 → 0.1 in test_gateway_stop_calls_close. Suite still hangs at 96% on full no-timeout runs; with these changes CI runs through to a real pass/fail signal. * chore(lock): regenerate uv.lock after adding pytest-timeout * ci: drop pytest-timeout 60 → 30s + bump GHA job 20 → 30 min Prior commit's timeout=60 was too generous — CI test job still hit the 20-min wall-clock cap with the suite hung at 96% (orphan agent-browser subprocesses blocking pytest session teardown). The local timeout=20 run completed in 6:17, so 30s is conservative enough to let real tests finish but aggressive enough to short-circuit deadlocks. Also bump GHA job timeout to 30 min as a safety margin. * test: delete 11 pre-existing failing tests + revert monotonic patch The previous PR commit landed pytest-timeout=30s and the suite now completes in 18:14 instead of hanging at 96%, but 11 pre-existing tests fail with real assertions. Per Teknium: nuke them. Deleted (no replacements): - tests/gateway/test_restart_resume_pending.py::test_clean_drain_does_not_mark_resume_pending - tests/gateway/test_restart_resume_pending.py::test_drain_timeout_only_marks_still_running_sessions - tests/hermes_cli/test_gateway_service.py::TestGatewaySystemServiceRouting::test_gateway_install_passes_system_flags - tests/hermes_cli/test_gateway_wsl.py::TestGatewayCommandWSLMessages::test_install_wsl_with_systemd_warns - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_detects_launchd_and_skips_manual_restart_message - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_restarts_profile_manual_gateways - tests/tools/test_file_operations.py::TestGitBaselineCheck::* (6 tests, entire class — _check_git_baseline helper doesn't exist) Also reverted my time.monotonic autouse-fixture hack in test_update_gateway_restart.py — it was causing worker crashes in CI by poisoning later tests in the same xdist worker. The two slow tests in that file (~24s and ~20s) will go back to taking real time but should still finish under the 30s pytest-timeout. * test: delete more pre-existing CI failures After previous push 3 more tests failed on CI; cull them all. Removed: - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_without_launchd_shows_manual_restart - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_profile_manual_gateway_falls_back_to_sigterm - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateResetFailedBeforeRestart::test_reset_failed_also_runs_before_retry_restart - tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateResetFailedBeforeRestart::test_final_failure_message_tells_user_to_reset_failed - tests/run_agent/test_tool_call_args_sanitizer.py::test_marker_message_inserted_when_missing The 4 update_gateway_restart tests trigger _wait_for_service_active polling on a real wall-clock deadline that occasionally exceeds the 30s pytest-timeout cap and crashes xdist workers. The marker test has a pre-existing assertion mismatch. * test: nuke entire TestCmdUpdateLaunchdRestart class After surgical deletes of 4 tests this class keeps producing new worker-crashing tests. The pattern is consistent: any test in this class that triggers cmd_update's _wait_for_service_active polling spins on real wall-clock time and trips pytest-timeout's thread method, crashing the xdist worker. Just delete the whole class (285 lines, ~10 tests). These exercise macOS-only launchd behavior that's better tested on a real macOS runner than in linux xdist. * test: stub the 2 fallback_model tests that crash xdist workers on CI * test: delete test_anthropic_error_handling.py + test_fallback_model.py entirely These two files exercise the agent retry/fallback code paths and consistently crash xdist workers under pytest-timeout's thread method. Whack-a-mole-stubbing individual tests just surfaces the next ones. Nuke both files. * test: delete tests/hermes_cli/test_update_gateway_restart.py entirely This file's cmd_update integration tests consistently crash xdist workers under pytest-timeout's thread method. Surgical deletes just surface the next set. Removing the whole file. * ci(tests): switch pytest-timeout method thread → signal Thread-method has been crashing xdist workers when it interrupts code that's not interruption-safe (retry loops, threading.Event waits, etc). Signal method uses SIGALRM which is interpreter-level and cleanly raises a Failed: Timeout exception in test code. Should stop the worker crash cascade — failures will surface as proper Timeout markers we can diagnose individually.16 天前
feat(gemini): block free-tier keys at setup + surface guidance on 429 (#15100) Google AI Studio's free tier (<= 250 req/day for gemini-2.5-flash) is exhausted in a handful of agent turns, so the setup wizard now refuses to wire up Gemini when the supplied key is on the free tier, and the runtime 429 handler appends actionable billing guidance. Setup-time probe (hermes_cli/main.py): - _model_flow_api_key_provider fires one minimal generateContent call when provider_id == 'gemini' and classifies the response as free/paid/unknown via x-ratelimit-limit-requests-per-day header or 429 body containing 'free_tier'. - Free -> print block message, refuse to save the provider, return. - Paid -> 'Tier check: paid' and proceed. - Unknown (network/auth error) -> 'could not verify', proceed anyway. Runtime 429 handler (agent/gemini_native_adapter.py): - gemini_http_error appends billing guidance when the 429 error body mentions 'free_tier', catching users who bypass setup by putting GOOGLE_API_KEY directly in .env. Tests: 21 unit tests for the probe + error path, 4 tests for the setup-flow block. All 67 existing gemini tests still pass.1 个月前
test: stop testing mutable data — convert change-detectors to invariants (#13363) Catalog snapshots, config version literals, and enumeration counts are data that changes as designed. Tests that assert on those values add no behavioral coverage — they just break CI on every routine update and cost engineering time to 'fix.' Replace with invariants where one exists, delete where none does. Deleted (pure snapshots): - TestMinimaxModelCatalog (3 tests): 'MiniMax-M2.7 in models' et al - TestGeminiModelCatalog: 'gemini-2.5-pro in models', 'gemini-3.x in models' - test_browser_camofox_state::test_config_version_matches_current_schema (docstring literally said it would break on unrelated bumps) Relaxed (keep plumbing check, drop snapshot): - Xiaomi / Arcee / Kimi moonshot / Kimi coding / HuggingFace static lists: now assert 'provider exists and has >= 1 entry' instead of specific names - HuggingFace main/models.py consistency test: drop 'len >= 6' floor Dynamicized (follow source, not a literal): - 3x test_config.py migration tests: raw['_config_version'] == DEFAULT_CONFIG['_config_version'] instead of hardcoded 21 Fixed stale tests against intentional behavior changes: - test_insights::test_gateway_format_hides_cost: name matches new behavior (no dollar figures); remove contradicting '$' in text assertion - test_config::prefers_api_then_url_then_base_url: flipped per PR #9332; rename + update to base_url > url > api - test_anthropic_adapter: relax assert_called_once() (xdist-flaky) to assert called — contract is 'credential flowed through' - test_interrupt_propagation: add provider/model/_base_url to bare-agent fixture so the stale-timeout code path resolves Fixed stale integration tests against opt-in plugin gate: - transform_tool_result + transform_terminal_output: write plugins.enabled allow-list to config.yaml and reset the plugin manager singleton Source fix (real consistency invariant): - agent/model_metadata.py: add moonshotai/Kimi-K2.6 context length (262144, same as K2.5). test_model_metadata_has_context_lengths was correctly catching the gap. Policy: - AGENTS.md Testing section: new subsection 'Don't write change-detector tests' with do/don't examples. Reviewers should reject catalog-snapshot assertions in new tests. Covers every test that failed on the last completed main CI run (24703345583) except test_modal_sandbox_fixes::test_terminal_tool_present + test_terminal_and_file_toolsets_resolve_all_tools, which now pass both alone and with the full tests/tools/ directory (xdist ordering flake that resolved itself).1 个月前
refactor(gmi): move User-Agent to profile.default_headers The previous revision of this PR added six GMI-specific branches (elif base_url_host_matches(..., 'api.gmi-serving.com')) across run_agent.py and agent/auxiliary_client.py, plus a _HERMES_UA_HEADERS constant in auxiliary_client.py. ProviderProfile already has a default_headers: dict[str, str] field commented as 'Client-level quirks (set once at client construction)'. Other plugins (ai-gateway, kimi-coding) already use it. Two of the four auxiliary_client sites we previously patched already had a generic else: profile.default_headers fallback that picked it up (so did both run_agent sites). This revision: * Sets default_headers={'User-Agent': 'HermesAgent/<ver>'} on the GMI profile in plugins/model-providers/gmi/__init__.py. * Reverts all six GMI-specific branches in run_agent.py and auxiliary_client.py. * Adds the generic profile-fallback else block to the two auxiliary_client sites (_to_async_client, resolve_provider_client) that didn't have it yet. This benefits every provider whose profile declares default_headers, not just GMI — e.g. Vercel AI Gateway's HTTP-Referer/X-Title now flow through the async client path too. * Replaces the GMI-specific URL-branch tests with a profile-level assertion and keeps the run_agent integration test (with provider='gmi' so the fallback picks up the profile). Net diff vs main: +82/-0 across 5 files, touching only the GMI plugin, two generic fallback blocks in auxiliary_client.py, AUTHOR_MAP, and tests. No core files change. Based on #20907 by @isaachuangGMICLOUD. 27 天前
feat(goals): /subgoal — user-added criteria appended to active /goal (#25449) * feat(goals): /subgoal — user-added criteria appended to active /goal Layers a /subgoal command on top of the existing freeform Ralph judge loop. The user can append extra criteria mid-loop; the judge factors them into its done/continue verdict and the continuation prompt surfaces them to the agent. No new tool, no agent self-judging — the existing judge model just sees a richer prompt. Forms: /subgoal show current subgoals /subgoal <text> append a criterion /subgoal remove <n> drop subgoal n (1-based) /subgoal clear wipe all subgoals How it integrates: - GoalState gains subgoals: List[str] (default []), backwards-compat for existing state_meta rows. - judge_goal accepts an optional subgoals kwarg; non-empty switches to JUDGE_USER_PROMPT_WITH_SUBGOALS_TEMPLATE which lists them as numbered criteria and asks 'is the goal AND every additional criterion satisfied?' - next_continuation_prompt picks CONTINUATION_PROMPT_WITH_SUBGOALS_TEMPLATE when non-empty so the agent sees what to target. - /subgoal is allowed mid-run on the gateway since it only touches the state the judge reads at turn boundary — no race with the running turn. - Status line shows '... , N subgoals' when present. Surface: - hermes_cli/goals.py — field, prompt blocks, manager methods, judge weave - hermes_cli/commands.py — /subgoal CommandDef - cli.py — _handle_subgoal_command - gateway/run.py — _handle_subgoal_command + mid-run dispatch - tests/hermes_cli/test_goals.py — 15 new tests (backcompat, mutation, persistence, prompt template selection, judge-prompt content via mock, status-line rendering) 77 goal-related tests passing across goals + cli + gateway + tui. * fix(goals): slash commands don't preempt the goal-continuation hook Two findings from live-testing /subgoal: 1. Slash commands queued while the agent is running landed in _pending_input (same queue as real user messages). The goal hook's 'is a real user message pending?' check returned True and silently skipped — but the slash command consumes its queue slot via process_command() which never re-fires the goal hook, so the loop stalls indefinitely. Now the hook peeks the queue and only defers when a non-slash payload is present. 2. The with-subgoals judge prompt was too soft — opus 4.7 said 'done, implying all requirements met' without verifying. Tightened to demand specific per-criterion evidence (file contents, output line, command result) and explicitly reject phrases like 'implying it was done.' Live verified: /subgoal injected mid-loop now correctly forces the judge to refuse done until the new criterion is met. Agent gets the continuation prompt with subgoals listed, updates the script, judge confirms done with specific evidence cited.21 天前
feat: shell hooks — wire shell scripts as Hermes hook callbacks Users can declare shell scripts in config.yaml under a hooks: block that fire on plugin-hook events (pre_tool_call, post_tool_call, pre_llm_call, subagent_stop, etc). Scripts receive JSON on stdin, can return JSON on stdout to block tool calls or inject context pre-LLM. Key design: - Registers closures on existing PluginManager._hooks dict — zero changes to invoke_hook() call sites - subprocess.run(shell=False) via shlex.split — no shell injection - First-use consent per (event, command) pair, persisted to allowlist JSON - Bypass via --accept-hooks, HERMES_ACCEPT_HOOKS=1, or hooks_auto_accept - hermes hooks list/test/revoke/doctor CLI subcommands - Adds subagent_stop hook event fired after delegate_task children exit - Claude Code compatible response shapes accepted Cherry-picked from PR #13143 by @pefontana. 1 个月前
refactor(cli): derive relaunch flag table from argparse introspection Pull the top-level + chat parser construction out of main() into hermes_cli/_parser.py so relaunch.py can introspect parser._actions to discover which flags exist and whether they take values, instead of maintaining a parallel hand-rolled (flag, takes_value) tuple list. - _parser.py: build_top_level_parser() returns (parser, subparsers, chat_parser); side-effect-free import. - main.py: ~290 lines of inline parser construction collapsed to a helper call. Other subparsers stay inline (dispatch is bound to module-level cmd_* functions). - _parser._inherited_flag(parser, ...): wraps parser.add_argument and sets action.inherit_on_relaunch = True. Used in place of parser.add_argument for the 25 flags (top-level + chat) that need to carry over. - _parser.PRE_ARGPARSE_INHERITED_FLAGS: holds --profile/-p, which isn't on argparse (consumed earlier by main._apply_profile_override). - relaunch.py: drops _CRITICAL_DESTS and _PRE_ARGPARSE_FLAGS; the table builder now filters by getattr(action, 'inherit_on_relaunch', False). - test_ignore_user_config_flags.py: brittle inspect.getsource grep replaced with proper parser introspection. - test_relaunch.py: introspection sanity tests added. Salvaged from PR #17549; added top-level -t/--toolsets flag to _parser.py so #17623 (fix(tui): honor launch toolsets) behavior is preserved on current main. Co-authored-by: ethernet <arilotter@gmail.com> 1 个月前
fix(tools): video_gen picker reflects active xAI selection and runs xai_grok post_setup Two bugs in the hermes tools reconfigure flow caused picking xAI Grok Imagine for video_gen (or image_gen) to feel like a no-op: 1. _is_provider_active() had a branch for image_gen_plugin_name but none for video_gen_plugin_name, so a row marked as the active xAI video provider was never recognized as active. The picker fell through to the env-var fallback in _detect_active_provider_index(), which matched the FAL row (because FAL_KEY is set), so the picker visually defaulted to FAL even though the user had selected xAI. 2. _plugin_video_gen_providers() and _plugin_image_gen_providers() built picker rows from the plugin's get_setup_schema() but only copied name, badge, tag, env_vars. The xAI plugins declare post_setup: "xai_grok" so the picker should run the OAuth / API-key prompt hook after selection — that key was silently dropped, so the hook never fired from the picker rows. Adds the missing video_gen_plugin_name branch (placed before the managed_nous_feature block, mirroring the existing image_gen branch) and propagates post_setup from the plugin schema into both picker-row builders. Adds focused tests in test_video_gen_picker.py and test_image_gen_picker.py. 20 天前
chore: ruff auto-fix PLR6201 resweep — tuple → set in membership tests (#27355) Six days after #23937 (608 fixes) the codebase had accumulated 241 new PLR6201 violations. Same mechanical x in (...)x in {...} fix, same zero-risk profile: set lookup is O(1) vs O(n) for tuple and the two are semantically equivalent for hashable scalar membership tests. All 241 instances fixed via `ruff check --select PLR6201 --fix --unsafe-fixes`, zero remaining. Every changed value is a hashable scalar (str/int/None/enum/signal); no risk of unhashable runtime errors. No behavior change. Test plan: - 119 files changed, +244/-244 (net zero) — exactly one-line edits - ruff check clean afterward - Compile checks pass on the largest touched files (cli.py, run_agent.py, gateway/run.py, gateway/platforms/discord.py, model_tools.py) - Subset broad test run on tests/gateway/ tests/hermes_cli/ tests/agent/ tests/tools/: 18187 passed, 59 pre-existing failures (verified against origin/main with the same shape — identical failure count, identical category — all xdist test-order flakes unrelated to this change) Follows the same template as PR #23937 ([tracker: #23972](https://github.com/NousResearch/hermes-agent/issues/23972)).18 天前
refactor(inventory): extract shared ConfigContext + build_models_payload Three call-sites in the codebase each duplicated the same config-slice + list_authenticated_providers + post-processing pattern: - hermes_cli/web_server.py /api/model/options - tui_gateway/server.py model.options JSON-RPC - tui_gateway/server.py model.save_key JSON-RPC This consolidates them onto hermes_cli/inventory.py: load_picker_context() -> ConfigContext Replaces the 17-LOC config-slice (model.{default,name,provider, base_url}, providers:, custom_providers:) every consumer did inline. ConfigContext.with_overrides(*, current_provider=, current_model=, current_base_url=) -> ConfigContext Truthy-only overlay for TUI agent-session state on top of disk config. Empty getattr(agent, ...) attrs MUST NOT clobber disk. build_models_payload(ctx, *, include_unconfigured, picker_hints, canonical_order, max_models) -> dict Single payload builder. Delegates curation to list_authenticated_providers (does not call provider_model_ids per row \u2014 that pulls non-agentic models). picker_hints + canonical_order produce the TUI ModelPickerDialog shape; defaults match the dashboard's existing /api/model/options contract. Two latent bugs fixed by consolidation: 1. The dashboard read cfg.get('custom_providers') directly, missing the v12+ keyed providers: form. Now both surfaces go through get_compatible_custom_providers(). 2. The TUI's canonical-merge keyed on is_user_defined to decide order. Section 3 of list_authenticated_providers sets is_user_defined=True on rows from the providers: config dict even when the slug is canonical \u2014 that silently demoted them to the picker tail. _reorder_canonical now keys on slug membership instead. Stats: +666 / -145 (net +521). Module 240 LOC; 18 behavior tests. This PR replaces the rejected #23369 (which bundled the consolidation with new scriptable CLI surfaces \u2014 hermes models list/status, hermes providers list \u2014 and a JSON contract that have no external user demand). Just the refactor; the CLI surface is deferred to a separate PR gated on actual demand. Refs #23359. 22 天前