chore: sync with upstream main (2026-05-23) - #42
Closed
bot-ted wants to merge 2333 commits into
Closed
Conversation
Extends the previous commit to cover the remaining additive-column index that sits on the same migration trap: - ``task_events.run_id`` -> ``idx_events_run`` was still in SCHEMA_SQL. A legacy ``task_events`` table predating NousResearch#17805 (no ``run_id``) would still abort ``executescript`` before ``_migrate_add_optional_columns`` could add the column. Hoisted out of SCHEMA_SQL and made unconditional in the migration alongside the other three indexes. - Removed the now-redundant ``CREATE INDEX idx_tasks_idempotency`` that was nested inside the ``if "idempotency_key" not in cols`` branch. The unconditional create lower in the function makes it idempotent on both fresh and legacy DBs. - Strengthened the regression test to cover all four indexes (``idx_tasks_session_id``, ``idx_tasks_tenant``, ``idx_tasks_idempotency``, ``idx_events_run``) and to seed a pre-NousResearch#17805 ``task_events`` shape that exercises the ``run_id`` migration path. The result: every ``CREATE INDEX`` that depends on an additive column now runs after the migration ensures the column exists. Verified against a realistic pre-NousResearch#16081 board fixture (tasks + task_events both legacy shape) — origin/main reproduces ``no such column: session_id``; this branch migrates cleanly and creates all four indexes.
When discord.py is not installed at import time, DISCORD_AVAILABLE=False and the view class definitions at module bottom are skipped. check_discord_requirements() performs a lazy install and sets DISCORD_AVAILABLE=True but never re-ran the class definitions, causing NameError on the first button interaction (exec approval, slash confirm, etc.). Extract the five ui.View subclasses into _define_discord_view_classes() and call it both at module load (when discord.py is pre-installed) and inside check_discord_requirements() after a successful lazy install.
…tory-truncation fix(tui): render full assistant text in scrollback (no history truncation)
For PR NousResearch#28774 (firecrawl integration tag). Co-authored-by: alt-glitch <balyan.sid@gmail.com>
…ser and web providers" (NousResearch#28862) This reverts commit 273ff5c.
…usResearch#26670) (NousResearch#26677) * fix(update): detect concurrent hermes.exe on Windows; retry + restart-defer quarantine Closes NousResearch#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.
Apply CREATE_NO_WINDOW flags when the cron scheduler launches job scripts on Windows so gateway-managed no-agent cron jobs do not flash cmd or python console windows every tick.
Apply Windows CREATE_NO_WINDOW flags to foreground local terminal subprocesses and tracked background processes so Hermes operations do not flash or steal focus with extra console windows.
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.
…-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.
Introduces make_tool_result_message() in tool_dispatch_helpers.py as the single place where tool-result message dicts are built. All six construction sites in tool_executor.py, agent_runtime_helpers.py, and mini_swe_runner.py now use it, so tool_name is set in memory from the moment a message is created rather than relying on fallback logic in the flush paths. Fixes blank tool_name in both state.db and JSON session logs. Adds tests.
Adds a Termux runtime detection helper and gates three TUI defaults on it: - Skip the startup scrollback clear on Termux so users can review/copy earlier output after reopening the app. Desktop keeps the existing \x1b[2J\x1b[H\x1b[3J slate (AlternateScreen takes over there anyway). - Default INLINE_MODE on under Termux: primary-buffer rendering makes long-thread review and copy/paste much less fragile when users background/foreground the app. Override with HERMES_TUI_INLINE=0/1. - Default mouse tracking off under Termux so touch selection isn't intercepted by terminal mouse protocols. Explicit override via HERMES_TUI_MOUSE_TRACKING=0/1; legacy HERMES_TUI_DISABLE_MOUSE still works on desktop. Detection is purely env-based (TERMUX_VERSION or PREFIX path) with an explicit opt-out HERMES_TUI_TERMUX_MODE=0 for debugging. Non-Termux platforms keep every existing default. Co-authored-by: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com>
…l-names-at-msg-construction fix blank tool_name entries in state.db and JSON session logs
Adds BrowseShSource — a new skill source adapter that integrates Browserbase's browse.sh catalog (169+ site-specific SKILL.md files) into the Hermes Skills Hub. - BrowseShSource class in tools/skills_hub.py implementing SkillSource ABC - Fetches browse.sh catalog API with 1h TTL cache - Full-text search across name, title, description, hostname, category, tags - fetch() downloads SKILL.md via sourceUrl (GitHub HTML -> raw URL conversion) - Registered in create_source_router() after LobeHubSource - Tests in tests/tools/test_skills_hub_browse_sh.py (7 tests, all passing)
- Add 'browse-sh' to _PER_SOURCE_LIMIT in both do_browse() and browse_skills() with limit=500 (covers full 171-skill catalog) - Add 'browse-sh' to --source argparse choices for both 'hermes skills browse' and 'hermes skills search' Without these, browse-sh fell back to the default cap of 50 results and was not filterable via --source.
The catalog's sourceUrl points at github.com/browserbase/browse.sh,
whose underlying repository is not always public — most raw URLs derived
from it 404. Use the per-skill detail endpoint instead, which returns a
skillMdUrl CDN blob that reliably resolves to the SKILL.md text. Fall
back to a raw.githubusercontent.com sourceUrl if the detail call fails.
- tools/skills_hub.py: rewrite BrowseShSource.fetch() to resolve via
/api/skills/{slug} -> skillMdUrl; drop the unreachable _to_raw_url
helper; expose the resolved URL in bundle.metadata.skill_md_url.
- tests/tools/test_skills_hub_browse_sh.py: match the real catalog
shape (name = task name, slug = host/task-id), exercise the
detail-endpoint -> blob two-call flow, and add a fallback test.
- scripts/release.py: map kylejeong21@gmail.com -> Kylejeong2.
Add browse.sh (browse-sh) to the supported-sources table and integrated-hubs section in user-guide/features/skills.md, and to the --source notes in reference/cli-commands.md. Companion to the BrowseShSource adapter merged in NousResearch#28936.
Resync the setup wizard's in-memory config after the shared model picker writes to disk so the wizard's final save does not overwrite auxiliary choices or other provider updates.\n\nAdds a regression test for auxiliary task choices saved by the picker.
…for base_url trust (NousResearch#27132) When config.yaml has provider: ollama (or vllm/llamacpp/llama-cpp) with a non-loopback base_url, auth.py's resolve_provider() correctly normalises the alias to 'custom' at the top level, but two sites in runtime_provider.py were still comparing the *original* string against the literal 'custom': - _config_base_url_trustworthy_for_bare_custom() rejected non-loopback URLs because cfg_provider_norm was 'ollama', not 'custom'. - _resolve_openrouter_runtime() only entered the trust branch when requested_norm == 'custom'. Both sites now consult resolve_provider() and treat any alias that resolves to 'custom' identically. Result: provider: ollama + LAN IP no longer silently falls through to OpenRouter (HTTP 401), matching the behaviour of provider: custom with the same base_url. E2E verified across 6 cases (ollama/vllm/llamacpp/custom + LAN; ollama + loopback; openrouter + cloud) — all route to the configured endpoint; 'frobnicate' + LAN still rejects with AuthError as before. Also adds scripts/release.py AUTHOR_MAP entry for @stepanov1975 (PR NousResearch#22074 — wizard config picker preservation, cherry-picked into the preceding commit).
…ousResearch#28864) `cli.py` was eager-importing `openai._base_client` at module-load time purely to monkeypatch `AsyncHttpxClientWrapper.__del__` (defense against "Press ENTER to continue..." errors when AsyncOpenAI clients are GC'd against dead event loops). That import cost ~166ms / ~30MB on every cold CLI start because openai's type tree (responses/*, graders/*) is huge. Replace with a `sys.meta_path` finder that intercepts the first import of `openai._base_client` from anywhere in the codebase, lets the normal load run, then applies the `__del__ = lambda self: None` patch before control returns to the caller. Same correctness guarantee (patch applies before any AsyncOpenAI instance can be constructed), zero cost until the SDK is actually needed. Hot path: every hermes chat / gateway boot / cron tick / subagent spawn. A/B benchmark, 10 runs each, fresh subprocess: BEFORE AFTER delta import cli wall 0.86s 0.62s -28% (median) import cli wall 0.85s 0.59s -31% (min) import cli RSS 91.2MB 74.0MB -19% (median) The `neuter_async_httpx_del` function in agent/auxiliary_client.py is unchanged; its tests still pass and any future callers can still invoke it directly. Verified: - import cli no longer pulls openai into sys.modules - first 'from openai._base_client import AsyncHttpxClientWrapper' triggers the patch; __del__.__name__ == '<lambda>' - tests/run_agent/test_async_httpx_del_neuter.py: 9/9 pass - tests/agent/test_auxiliary_client.py: 159/159 pass - tests/cli/: 715/715 pass
…rgeted hot-path optimizations (NousResearch#28866) * perf(config): add load_config_readonly() fast path for hot agent loop `load_config()` is called from the agent loop's per-API-call hot path via `get_provider_request_timeout()` and `get_provider_stale_timeout()` — both invoked once per turn from `_resolved_api_call_timeout()` in run_agent.py. Profiling a synthetic 20-tool-call agent run revealed: - 21 invocations of `load_config()` cumulating 56ms (~17% of agent loop) - 34,398 deepcopy calls totaling 37ms (config defensive deepcopy + chain) - 8,652 `_expand_env_vars` invocations (~412 per turn) Microbench (cache-hit, real config.yaml present): load_config() 265us/call (125us deepcopy + 140us infra) load_config_readonly() 138us/call (~48% faster) `load_config_readonly()` returns the cached dict directly without the defensive deepcopy. Documented contract: caller must not mutate. Returns plain dict (not MappingProxyType) so downstream `isinstance(x, dict)` guards keep working — caught during initial implementation when MappingProxyType broke get_provider_request_timeout's guard logic. Wired into hermes_cli/timeouts.py (the two functions called per agent turn). load_config() is unchanged for the 263 other call sites that mutate the result before save_config(), are not in the hot path, or where the safety guarantee matters more than the perf. Profile A/B (cached config, 21-turn agent loop): BEFORE AFTER delta get_provider_request_timeout 55ms 16ms -71% total function calls 399k 160k -60% deepcopy calls (in hotspots) 34,398 ~0 ~elim Verified: - isinstance(load_config_readonly(), dict) is True - timeout/stale resolutions correct - load_config() still returns isolated mutable deepcopies - tests/hermes_cli/test_config*.py / test_timeouts.py: 102/102 pass - tests/cli/ + tests/agent/test_auxiliary_client.py: 883/883 pass * perf(redact): substring pre-screens skip non-matching regex chains Every log record passes through `RedactingFormatter.format` which calls `redact_sensitive_text`, which historically ran ALL 13 secret-pattern regexes against every line — including DB connection strings, JWTs, Discord mentions, Signal phone numbers, etc. — even for typical clean log records like 'INFO run_agent: API call completed'. Add cheap substring pre-checks before each regex pass. False positives still run the regex (which then matches nothing); false negatives are impossible because every pattern requires the gated substring to match its leading anchor: - `_PREFIX_RE` gated on any of 33 known credential prefix substrings - `_ENV_ASSIGN_RE` gated on `=` in text - `_JSON_FIELD_RE` gated on `:` and `"` in text - `_AUTH_HEADER_RE` gated on `uthorization`/`UTHORIZATION` in text - `_TELEGRAM_RE` gated on `:` in text - `_PRIVATE_KEY_RE` gated on `BEGIN` and `-----` - `_DB_CONNSTR_RE` gated on `://` in text - `_JWT_RE` gated on `eyJ` in text - URL userinfo/query gated on `://` - `_redact_form_body` gated on `&` and `=` - `_DISCORD_MENTION_RE` gated on `<@` - `_SIGNAL_PHONE_RE` gated on `+` Microbench (5 typical log records, 20k iterations each): BEFORE AFTER delta redact_sensitive_text per call 5.63us 1.79us -68% Real-world impact: ~244 log records emitted in a 30-turn agent loop, so the chain saves ~1ms of CPU per conversation. Bigger win is the reduction in regex execution and GC pressure during heavy logging sessions (verbose logging, gateway message processing). Security regression test: 30 secret-containing inputs (sk-/ghp_/JWT/DB connstr/Auth-Bearer/private key/URL userinfo/Discord/Signal/etc.) verified to produce identical redacted output before/after. All 75 existing tests/agent/test_redact.py cases pass. The `?access_token=foo&code=bar` (bare query string, no scheme) case that 'leaks' is pre-existing behavior — the URL query redaction requires a well-formed URL with scheme+host. Not a regression. * perf(run_agent): cache _needs_thinking_reasoning_pad result per (provider, model, base_url) Profile of a 31-turn synthetic agent run shows `_needs_thinking_reasoning_pad` fires 495 times (~16 per turn) and each call ran 3 helper methods, each hitting `base_url_host_matches` 1-4 times via `urlparse`. Total cost: 3,342 base_url_host_matches calls + 3,373 urlparse calls accounting for ~36ms of agent-loop overhead (~7% of the entire post-network work). Provider / model / base_url don't change during a conversation except via `switch_model` and fallback activation — both of which already overwrite those attributes atomically. Cache the result on a tuple key; since the key is derived from the very fields that would change, the cache auto-invalidates on the next read after a switch. No manual invalidation needed in switch_model / _try_activate_fallback. Profile A/B (31-turn cached-config agent run): BEFORE AFTER delta _needs_thinking_reasoning_pad cum 18ms 1ms -94% _copy_reasoning_content_for_api cum 17ms 1ms -94% base_url_host_matches calls 3,342 372 -89% urlparse calls 3,373 403 -88% total function calls 296k 223k -25% Verified: - tests/run_agent/test_deepseek_reasoning_content_echo.py: 36/36 pass - tests/run_agent/ (full): 1383/1383 pass + 3 skipped
Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.20.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](websockets/ws@8.20.0...8.20.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.20.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…Hub API (NousResearch#28943) The Windows installer fetched the latest git-for-windows release via api.github.com/repos/git-for-windows/git/releases/latest, which is rate-limited to 60 requests/hour/IP for unauthenticated callers. Users behind CGNAT, corporate NAT, dorm WiFi, or shared ISP routinely hit the limit, and the installer aborts asking them to install Git manually. Switch to a pinned release tag (v2.54.0.windows.1) and a static github.com/.../releases/download/<tag>/<asset> URL. Static download URLs are served by GitHub's blob storage and are not subject to the API rate limit. Trade-offs: - We have to bump the pin when we want a newer Git for Windows. The installer doesn't depend on Git features beyond 'works', so this is a once-a-year maintenance cost at most. - Loses the (cosmetic) MB size display, since we no longer have asset metadata. Replaced with the version string in the 'Downloading ...' line instead.
…8952) 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.
Add original_name parameter to _download_and_cache, preferring the attachment metadata filename over the CDN URL path basename. Previously files were cached with meaningless QQ CDN hash names (e.g. qqdownload_...oadftnv5), causing ugly filenames when sent back to users. Aligns with qqbot-agent-sdk's AttachmentDownloader.download_document.
_guess_ext_from_data: data[:5] == b"#!SILK" -> data[:6] (6-byte string) _looks_like_silk: data[:4] == b"#!SILK" -> data[:6] The previous slices were too short to ever match the 6-byte "#!SILK" literal, relying entirely on the "#!SILK_V3" (9-byte) and 0x02! (2-byte) fallback paths for SILK format detection.
…esearch#29507) The helper used to call ``socket.shutdown(SHUT_RDWR)`` followed by ``socket.close()`` to drop CLOSE-WAIT entries immediately. On its own ``shutdown()`` is safe from any thread — it only sends FIN and breaks pending ``recv``/``send`` — but ``close()`` releases the FD integer to the kernel. When the helper runs on a stranger thread (the interrupt loop, the stale-call detector) the FD release races the owning httpx worker thread that still has the same integer cached inside the SSL BIO. The kernel then recycles that integer to the next ``open()`` call — in production, kanban dispatcher's ``kanban.db`` — and the worker's delayed TLS flush writes a 24-byte TLS application-data record on top of the SQLite header. Restrict the helper to ``shutdown(SHUT_RDWR)`` only. The owning httpx worker's own unwind will close the underlying socket via the same Python ``socket.socket`` object, which atomically swaps ``_fd`` to -1 before issuing ``close(2)`` — no FD-aliasing window. The log field ``tcp_force_closed=N`` is kept (now counts shutdowns) so existing dashboards / log parsers keep working.
…upt (NousResearch#29507) Layer-2 defense for the FD-recycling race: even with ``force_close_tcp_sockets`` reduced to shutdown-only, the followup ``client.close()`` in ``_close_openai_client`` still walks the httpx pool and closes sockets — and if called from a stranger thread (the interrupt-check loop, the stale-call detector) it has the same FD-recycling exposure that wrote a TLS record on top of ``kanban.db``. Stamp the request_client_holder with the owning thread's ident at ``_set_request_client`` time. In ``_close_request_client_once``: * Owning thread (the worker's ``finally``) → pop + ``client.close()`` via ``_close_request_openai_client``, exactly as before. * Stranger thread → ``_abort_request_openai_client`` (new): only ``shutdown(SHUT_RDWR)`` the pool sockets and log a deferred-close marker. The holder stays populated so the worker's eventual ``finally`` performs the real close from its own thread context, where the FD release races nothing. Applied symmetrically to both the non-streaming ``interruptible_api_call`` and the streaming variant — both routinely get hit by stranger-thread interrupts. The log field ``tcp_force_closed=N`` keeps its existing shape; the new abort path adds ``deferred_close=stranger_thread`` so production triage can distinguish the two close kinds.
…NousResearch#29507) Ten regressions across both prongs of the NousResearch#29507 fix, organised so each test names exactly which way the bug could come back: Prong 1 — ``force_close_tcp_sockets``: * ``shutdown_only_no_close`` is the smoking-gun assertion. If a future refactor adds back ``sock.close()`` to this helper, the FD-recycling race that wrote TLS bytes on top of ``kanban.db`` is back, and this trips. * ``uses_shut_rdwr`` pins that both halves are shut down (a half-close wouldn't unblock a worker stuck in ``recv``). * ``swallows_oserror_on_shutdown`` covers the already-shutdown case. * ``handles_multiple_pool_entries`` walks all pool connections. Prong 2 — thread-aware ``_close_request_client_once``: * ``stranger_thread_aborts_only_no_close`` simulates the asyncio_0 → Thread-1616 interrupt path: stranger drives abort, holder stays populated for the worker's eventual finally. * ``owner_thread_pops_and_full_close`` is the worker-thread path: pops + full close. * ``stranger_then_owner_close_sequence_runs_full_close_exactly_once`` replays the reporter's exact timeline at object level: abort runs once, full close runs once, holder ends empty. Agent surface: * ``_abort_request_openai_client_does_not_call_client_close`` pins that the new entrypoint shuts sockets and emits the ``deferred_close=stranger_thread`` marker but never calls ``client.close()``. * ``_abort_request_openai_client_null_client_is_noop`` defensive. End-to-end: * ``fd_recycle_window_closed_by_shutdown_only`` reproduces the race at object level — runs the abort path from a stranger thread and asserts that no ``close()`` ever fires, so the kernel can never recycle the FD under the owner's still-active reference.
…ousResearch#30852) `_deliver_kanban_artifacts` routes candidates through `BasePlatformAdapter.filter_local_delivery_paths` (added in 41d2c75), which rejects paths outside `MEDIA_DELIVERY_SAFE_ROOTS`. The two artifact-delivery tests create fixtures under `tmp_path`, which lives outside the cache roots — so under CI's hermetic HOME the filter silently dropped both fake files and the assertions on `images_uploaded` / `documents_uploaded` failed. Fix: monkeypatch `HERMES_MEDIA_ALLOW_DIRS=str(tmp_path)` in both tests so the safety filter accepts the fixtures. Production behaviour unchanged; test-side fix only. CI fail repro on origin/main: test (6) shard, both test_notifier_uploads_artifacts_on_completion and test_notifier_artifact_delivery_skips_missing_files.
…n skills_guard - _determine_verdict() returned 'caution' for medium/low-only findings, causing community skills with harmless patterns (e.g. path traversal notation, unpinned pip install) to be incorrectly blocked. Now returns 'safe' when only medium/low severity findings are present. - should_allow_install() allowed --force to override 'dangerous' verdict, contradicting documented behavior that --force does NOT override dangerous scan results. Added explicit check to prevent force-installing skills with dangerous verdict.
Follow-up to @sprmn24's verdict-logic fix. The previous block-message ended in 'Use --force to override' regardless of verdict — but as of the --force fix above, dangerous community/trusted skills can't be overridden by --force at all. The misleading hint sends users in a loop. Replace it with a specific message that tells them what the documented behavior actually is. Adds two regression tests covering the dangerous-verdict message shape and one that pins the existing --force hint for non-dangerous blocks.
…ousResearch#30860) * feat(portal): one-shot setup, status CLI, and Nous-included markers Four small Portal-aware surfaces that drive subscription value without adding friction for non-Portal users. - hermes setup --portal: one-shot Nous OAuth + provider switch + Tool Gateway opt-in. Shareable as a single command from docs/social. - hermes portal {status,open,tools}: small surface over Portal auth + Tool Gateway routing. Defaults to 'status' when no subcommand. - Tool picker (hermes tools): when the user is logged into Nous, mark Nous-managed provider rows with a star and 'Included with your Nous subscription'. Suppressed when not authed — non-subscribers see the picker unchanged. - BYOK setup hint: a single dim line 'Available through Nous Portal subscription.' appears when the user is being prompted for a paid API key (Firecrawl, FAL, ElevenLabs, Browserbase, etc.) AND the category has a Nous-managed sibling AND the user is not already authed to Nous. Suppressed in all other cases. Tested live end-to-end in an isolated HERMES_HOME with a simulated authed and unauthed user. Targeted suite (tests/hermes_cli/ test_tools_config.py + test_setup.py) passes 97/97. * fix: add portal to _BUILTIN_SUBCOMMANDS so plugin discovery fast-path skips it
PR 2362cc4 ("fix(gateway): enforce env variable template expansion on runtime config loaders") refactored `_load_service_tier` to read config via the new `_load_gateway_runtime_config` wrapper instead of opening `_hermes_home/config.yaml` directly. The `test_run_agent_passes_priority_processing_to_gateway_agent` test still only stubbed `_load_gateway_config` (the inner loader), so the runtime wrapper saw an empty config and `_load_service_tier` returned None, breaking the test: FAILED tests/gateway/test_fast_command.py::test_run_agent_passes_priority_processing_to_gateway_agent - AssertionError: assert None == 'priority' Fix: also stub `_load_gateway_runtime_config` to return the expected `agent.service_tier=fast` config, so the test once again drives the priority routing path it was written to verify. Confirmed reproducing on current main before the patch and passing after.
…ousResearch#30864) Closes NousResearch#30045. Based on @qike-ms's PR NousResearch#30141. Telegram status callbacks (lifecycle, compression, context-pressure) used to append a fresh bubble on every emit. Now adapter tracks {(chat_id, status_key) -> message_id}; first call sends, subsequent calls edit. Failed edits drop the cache entry and fall through to a fresh send. - gateway/platforms/telegram.py: send_or_update_status() (+34 LOC) - gateway/run.py: route _status_callback_sync through it when the adapter supports it; plain adapter.send() otherwise (+15 LOC) - 5 tests covering first send / edit-in-place / edit-failure fallback / distinct key & chat isolation
…-facing pages (NousResearch#30869) PR NousResearch#30860 added a one-shot Portal setup command and a small portal CLI surface. Update the docs so the new commands are discoverable without upgrading the tone of existing Portal mentions. - getting-started/quickstart.md: small tip near Choose a Provider pointing at 'hermes setup --portal' as the easiest fresh-install path. - user-guide/features/tool-gateway.md: lead the Get-Started section with 'hermes setup --portal' for fresh installs, keep 'hermes model' for already-configured users, and add 'hermes portal status / tools' to the activity-check commands. - user-guide/features/{web-search,image-generation,tts,browser}.md: the existing 'Nous Subscribers' tip blocks now name the one-shot command for new installs, keeping the existing 'hermes tools' path for users who only want to swap a single backend. - reference/cli-commands.md: register 'hermes portal' in the top-level command table, add a 'hermes portal' section with subcommands, and add '--portal' to the 'hermes setup' options table. Tone: each page already had a Portal mention. This PR keeps the per-page count to one and uses concrete CLI commands rather than promotional copy. Tool Gateway page is the one exception (the whole doc is about Portal).
… page describes (NousResearch#30874) Follow-up to NousResearch#30869. Adds Portal mentions on user-facing pages that naturally call for an LLM + tool credentials but didn't previously acknowledge Portal as a one-stop option. - getting-started/installation.md: tip after the 'after install' block pointing at 'hermes setup --portal' for users who want everything wired at once instead of piecewise via 'hermes model' + 'hermes tools'. - user-guide/configuring-models.md: small tip near the top — the page is literally about provider/model choice and previously had zero Portal mention. - user-guide/features/voice-mode.md: Prerequisites need both an LLM and TTS — a Portal subscription is the single setup that covers both. - user-guide/features/batch-processing.md: highlights Portal as a predictable-cost option for parallel agent runs that hit many APIs. - user-guide/features/api-server.md: backend needs models + tools; one Portal sub gives a fully-equipped OpenAI-compatible endpoint. - user-guide/windows-native.md: early-beta users on Windows benefit most from skipping per-tool Windows-key-juggling. - integrations/providers.md: updates the existing Tool Gateway tip and the Nous Portal section to mention the new commands. - user-guide/features/fallback-providers.md: Nous row in the provider table now lists 'hermes setup --portal' as the fresh-install path. Tone discipline: one Portal mention per page, concrete CLI commands (no marketing copy), always solving a problem the page itself sets up.
…tlement classifier (NousResearch#29344) ``_is_entitlement_failure`` over-matched on xAI 403s. xAI returns the same permission-denied ``code`` text for two distinct conditions: 1. Unsubscribed account ("active Grok subscription. Manage at https://grok.com" in the ``error`` field). 2. Stale OAuth access token ("OAuth2 access token could not be validated. [WKE=unauthenticated:bad-credentials]" in the ``error`` field). The classifier's "does not have permission + grok" substring heuristic treated both identically, so the credential-pool refresh path was short-circuited for case (2) — long-running TUI sessions stuck on a stale OAuth token surfaced a non-retryable client error and the user had to exit + reopen the TUI to recover (the startup-resolve path bypasses the classifier entirely, which is why bridge adapters with proactive refresh cadences didn't see this in practice). This patch adopts the reporter's recommended fix (option 1, tightest): honor xAI's explicit ``[WKE=unauthenticated:...]`` suffix and the ``OAuth2 access token could not be validated`` phrasing as authoritative "this is auth, not entitlement" signals. When either appears anywhere in the body's text fields, the classifier returns False eagerly — *before* the entitlement keyword checks run — so the refresh-on-401 path takes over and the existing loop-protection still guards against runaway refresh storms if the refresh itself fails. Two small adjustments fall out of this: * The haystack now also covers ``code`` and ``error`` keys directly, not just the ``message``/``reason`` shape ``_extract_api_error_context`` produces. Real runtime paths use the normalised shape, but the test suite and any future call sites that pass raw bodies get the same treatment. Backwards compatible: missing keys default to empty strings, the haystack still skips when everything is blank. * Both disambiguator checks fire BEFORE the entitlement keyword checks. If a future xAI body somehow lands with both an entitlement message AND the WKE suffix, the WKE suffix wins (correct — auth is recoverable; entitlement is not, and a refreshed token will surface the entitlement message on the next request anyway). Existing tests (``test_is_entitlement_failure_matches_real_xai_bodies``, ``test_is_entitlement_failure_false_for_unrelated_auth_errors``, ``test_recover_with_credential_pool_skips_refresh_on_entitlement_403``, ``test_recover_with_credential_pool_still_refreshes_genuine_auth_failure``) continue to pass unchanged — the unsubscribed-account path, the generic auth-error path, and the refresh-on-401 path are all left intact.
…uator (NousResearch#29344) Eleven new tests pinning the NousResearch#29344 fix. Layout mirrors the existing "Fix D" entitlement section so the bad-credentials disambiguator sits alongside the entitlement-block tests it complements. Classifier-level coverage: * ``test_is_entitlement_failure_false_for_bad_credentials_wke_suffix`` — verbatim shape from the reporter's wire capture (``{code: 'caller does not have permission', error: 'OAuth2 access token could not be validated. [WKE=unauthenticated:bad-credentials]'}``) ↦ classifier must return False so the refresh path runs. * ``test_is_entitlement_failure_false_for_wke_suffix_in_normalized_shape`` — same body after ``_extract_api_error_context`` has rewritten it to ``{reason, message}``. The disambiguator must fire in BOTH shapes; without this guard the production call site at ``_recover_with_credential_pool`` (which goes through the normalised extractor) would still misclassify. * ``test_is_entitlement_failure_false_for_any_wke_unauthenticated_variant`` — parametrised forward-compat: ``bad-credentials``, ``expired-token``, ``revoked``, ``some-future-reason``. xAI documents the prefix as stable, the suffix after the colon as a reason code that can grow; every variant under ``unauthenticated:`` must route to refresh. * ``test_is_entitlement_failure_false_via_oauth2_validation_phrase_alone`` — belt-and-braces guard: if a future API revision drops the WKE suffix but keeps "OAuth2 access token could not be validated", we still classify correctly. * ``test_is_entitlement_failure_wke_signal_overrides_entitlement_keywords`` — defensive: if a body ever carries BOTH the WKE suffix and entitlement language, the WKE signal wins. Auth is recoverable; entitlement isn't, and a refreshed token will resurface the entitlement message on the next request. * ``test_is_entitlement_failure_case_insensitive_wke_match`` — pins that the classifier lowercases the haystack so a future xAI build that uppercases the prefix doesn't reintroduce the bug. Recovery-path coverage (end-to-end through ``_recover_with_credential_pool``): * ``test_recover_with_credential_pool_refreshes_on_xai_bad_credentials_403`` — the headline test the reporter requested: a bad-credentials 403 with the exact wire body must call ``try_refresh_current()`` exactly once and ``_swap_credential`` once. Pre-fix this returned ``(False, _)`` because the entitlement classifier over-matched and short-circuited the refresh path. * ``test_recover_with_credential_pool_still_blocks_real_entitlement`` — companion regression guard for NousResearch#26847: a pure unsubscribed- account body (no WKE suffix, no OAuth2-validation phrase) must still surface as entitlement and skip refresh. The new disambiguator must not weaken the original loop-protection it was added to preserve. The scaffolding reuses ``_make_codex_agent``, ``_FakePool``, and the existing ``MagicMock`` patterns from the surrounding tests so the new section reads as a natural extension of "Fix D" rather than a separate test file.
…ousResearch#29344) _recover_with_credential_pool had a second classification site that blanket- treated any 403 against xai-oauth as entitlement (defense-in-depth for NousResearch#26847). That override defeated the new _is_entitlement_failure disambiguator from the parent commit — bad-credentials 403s still short-circuited the refresh path. Apply the same WKE-unauthenticated / OAuth2-validation-phrase guard at the override site so xAI's authoritative 'this is auth, not entitlement' signal wins there too. The NousResearch#26847 catch-all still triggers for genuine entitlement bodies that don't carry the disambiguator. Closes the end-to-end gap exposed by test_recover_with_credential_pool_refreshes_on_xai_bad_credentials_403.
…esearch#26045) (NousResearch#30877) Reproduction (production, 2026-05-14): two concurrent sessions on the same agent. Session A patches MEMORY.md directly via the patch tool, appending ~8KB of structured content (Vendor Master, Standing Orders, Pin Board) — none of it through the memory tool, so no § delimiters. Session B starts later with stale in-memory state (1 entry, ~331 chars). Session B calls memory(action=replace) on its one known entry. The tool's _read_file parses A's content as a single 8KB 'entry' (no § splits), then replace truncates that entry to B's new 333-byte content. ~8KB of structured content silently destroyed. The atomic-rename write path is fine in isolation. The bug is the implicit contract: the tool assumes MEMORY.md is exclusively a §-delimited list of small entries it wrote, but the v0.13 install runbook itself uses 'cat >> MEMORY.md' for onboarding, the patch tool edits the file directly, and operators do too. Fix: a drift guard in MemoryStore._detect_external_drift that fires on either signal: 1. Re-parse + re-serialize doesn't produce identical bytes (catches oddly-encoded delimiters / partial writes). 2. Any single parsed entry exceeds the store's whole-file char limit. The tool budgets the ENTIRE store against that limit (2200 chars for memory, 1375 for user), so no tool-written entry can legitimately be larger. An entry bigger than the store limit means an external writer dropped free-form content into what the tool will treat as one entry. When drift fires, _reload_target writes a .bak.<ts> snapshot of the on-disk file, then add/replace/remove refuse to flush. The original file stays untouched. The error dict surfaces the .bak path AND a remediation string ('integrate missing entries via memory(add=...) one at a time, then rewrite the file clean') so the model can act on it without escalating to the operator. Tests: - test_replace_refuses_on_drift, test_add_refuses_on_drift, test_remove_refuses_on_drift — all three mutators refuse - test_clean_file_does_not_trigger_drift — false-positive check - test_error_message_points_at_remediation — error string shape - test_drift_guard_also_protects_user_target — USER.md too - test_drift_backup_filename_is_unique_per_invocation — bak.<ts> naming pin 144 memory tests passing (was 137; +7). Fixes NousResearch#26045
…ousResearch#24912) (NousResearch#30879) User incident (Slack, 2026-05-13): user walked away mid-conversation, agent requested approval to run `rm -rf .git`, the prompt timed out after the gateway_timeout (default 300s), and the agent removed the .git folder on its own. Corroborated by an independent report from a Telegram user. The underlying code path was correct — `check_all_command_guards` returns `approved=False` with a BLOCKED message on both timeout and explicit deny, and `terminal_tool` surfaces that as `status=blocked` to the agent. The bug is at the model-interface layer: the message "BLOCKED: Command timed out. Do NOT retry this command." reads to some models as "try a different command achieving the same outcome." This commit changes only the model-facing message + the structured return shape: - Timeout message now explicitly names the three evasion paths the agent must avoid: retry, rephrase, AND achieve the same outcome via a different command. Ends with "Silence is not consent." - Explicit deny gets the same shape minus the silence-is-not-consent line (it WAS an explicit deny, not silence). - New structured fields on the return dict: `outcome` ("timeout" or "denied") and `user_consent` (always False on this branch) so plugins, hooks, and audit pipelines don't have to string-parse the message to distinguish the two cases. The mechanism that should already have prevented the original incident — timeout treated as deny, BLOCKED result, post hook fires with `choice="timeout"` — is unchanged. This commit hardens only the agent's reading of the result. Tests: - test_timeout_returns_approved_false_with_no_consent — pins the return shape on the Slack-shaped notify_cb-registered path - test_timeout_message_is_emphatic_against_retry_and_rephrase — pins the exact phrases the message must contain - test_explicit_deny_carries_same_no_consent_shape — same contract on explicit /deny - test_timeout_emits_post_hook_with_timeout_outcome — pins the post_approval_response hook payload so audit plugins can act 329 approval tests passing (4 new + 325 existing). Fixes NousResearch#24912
…/Messaging reference (NousResearch#30941) A small, self-contained section under 'Skip the API-key collection — Nous Portal' explaining what Portal gives you (300+ models + Tool Gateway), the one-shot install command, and how to inspect routing. No buzzwords, no comparison tables, no overselling. Positioned right after 'Getting Started' so it lands where someone scanning the README has just seen the install steps and is deciding their next move. Skippable by anyone who already knows their provider. The line 'You can still bring your own keys per-tool whenever you want' is the deliberate honesty rail — Portal is an option, not a funnel. Existing per-provider language elsewhere in the README is unchanged. Mirrored to README.zh-CN.md to keep the two READMEs in sync.
…indings Path.resolve() before any I/O and confine backup writes to the resolved parent directory. Adds explicit parent-equality assertions so static analyzers see the containment guarantee, and walks WAL/SHM sidecars through the same resolved-parent path so accidental .. segments are collapsed before shutil.copy2. Functionally equivalent to the original PR; preserves the corrupt bytes to <db>.corrupt.<ts>.bak in the same directory, still raises KanbanDbCorruptError from connect(). E2E with Stefan's exact hex header + malformed pages still passes. 163/163 kanban tests still pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Daily sync with upstream. Auto-created by cron job.
Commits synced: 9278 new commits from upstream.
Commit list
cae7537 infographic: kanban.db corruption defense (NousResearch#30858 + NousResearch#30862) (NousResearch#30952)
c4b8f5e fix(kanban): harden corrupt-db backup against CodeQL path-injection findings
4f835f7 chore(release): map NickLarcombe author email for NousResearch#30707 salvage
39fe4ec fix(kanban): refuse corrupt db auto-init
e97a4c8 docs(readme): add Nous Portal section between Getting Started and CLI/Messaging reference (NousResearch#30941)
7245bc7 fix(fallback): merge fallback_providers with legacy fallback_model configurations
7f1b2b4 fix(approval): pin 'silence is not consent' contract on timeout/deny (NousResearch#24912) (NousResearch#30879)
6855d17 fix(memory): guard against external drift in MEMORY.md/USER.md (NousResearch#26045) (NousResearch#30877)
cc93053 fix(xai-oauth): apply WKE disambiguator to recovery-path catch-all (NousResearch#29344)
b5ea6a5 test(xai-oauth): regression coverage for the bad-credentials disambiguator (NousResearch#29344)
8b3cb93 fix(xai-oauth): honor [WKE=unauthenticated:...] disambiguator in entitlement classifier (NousResearch#29344)
64b3eb0 docs: surface Nous Portal on pages where it solves a real problem the page describes (NousResearch#30874)
f3fb789 docs: surface 'hermes setup --portal' and 'hermes portal' across user-facing pages (NousResearch#30869)
9acf949 feat(telegram): edit status messages in place instead of appending (NousResearch#30864)
4b6d68b test(fast-command): stub _load_gateway_runtime_config too
61ac118 fix(webhook): enforce INSECURE_NO_AUTH safety rail on dynamic route reloads
b4cf5b6 feat(portal): one-shot setup, status CLI, and Nous-included markers (NousResearch#30860)
6942b18 fix(skills_guard): explain why --force is rejected on dangerous verdicts
789043b fix(security): update tests for verdict and --force changes
0f8215f fix(security): correct verdict logic and enforce --force limitation in skills_guard
db489a3 fix(tests): allowlist tmp_path for kanban_notify artifact delivery (NousResearch#30852)
5b6f0b6 test(tls-fd-recycle): pin shutdown-only + thread-aware close contract (NousResearch#29507)
30c22f1 fix(api-call): defer client.close() to owning worker thread on interrupt (NousResearch#29507)
e2a7d73 fix(force_close_tcp_sockets): shutdown only, do not release FD (NousResearch#29507)
53cb6d3 fix(agent): use atomic_json_write for request debug dumps instead of bare write_text
b183be9 fix(gateway-windows): atomic write for .cmd and startup launcher scripts
60b0a0e fix(qqbot): fix SILK magic byte detection slice length
0e7448d fix(qqbot): use original attachment filename for cached files
a54f5af fix(qqbot): handle op 7/9 and expand fatal close code set
bbd77d1 fix(qqbot): add INTERACTION intent and expose video/file cached paths
66d81f9 fix(gateway): don't swallow expansion errors in runtime config helper
2362cc4 fix(gateway): enforce env variable template expansion on runtime config loaders
d21ac57 fix(gateway): honor key_env in auth-failure fallback resolution
99671a8 test(kanban): allow tmp_path artifacts past media-delivery validator
5772e63 chore: drop in-repo infographic/ directory; keep PR-body URLs only (NousResearch#30854)
b2e6fdd fix(agent): log warning when fallback model normalization fails instead of silently swallowing
70aaa77 fix(opencode-go): emit Kimi reasoning_effort, match KimiProfile shape
3589960 fix(provider): expose OpenCode Go reasoning controls
71291d8 test: keep tirith checks hermetic
52a368f fix(gateway): preserve WhatsApp pairing approvals across JID/LID alias flips
3127a41 test(acp): pin parse_model_input in slash-command tests
6a2df9f docs(env): clarify HERMES_ENABLE_PROJECT_PLUGINS contract (NousResearch#29156)
8bf9922 fix(plugins): block plugin-api path traversal + project RCE (NousResearch#29156)
da636e9 test(plugins): regression coverage for project-plugin RCE chain (NousResearch#29156)
09f85f2 fix(plugins): apply truthy env semantics to project-plugin gate (NousResearch#29156)
11e6dd3 chore(release): add AUTHOR_MAP entry for egilewski (PR NousResearch#30432) (NousResearch#30833)
41d2c75 Fix unsafe gateway media path delivery
4a91e36 fix(gateway): separate observed Telegram group context
729a778 infographic: PR NousResearch#17659 read-deny credentials salvage
97e975e fix(file-safety): widen read-deny to .env, mcp-tokens/, webhook secrets, root
"... and 9228 more"