Commit graph

5382 commits

Author SHA1 Message Date
Teknium
e889332c99
fix(gateway): always inject reply-to pointer, not just when quoted text is absent (#13676)
The [Replying to: "..."] prefix is disambiguation, not deduplication. When
a user explicitly replies to a prior message, the agent needs a pointer to
which specific message they're referencing — even when the quoted text
already exists somewhere in history. History can contain the same or
similar text multiple times; without an explicit pointer the agent has to
guess (or answer for both subjects), and the reply signal is silently
dropped.

Example: in a conversation comparing Japan and Italy, replying to the
"Japan is great for culture..." message and asking "What's the best time
to go?" — previously the found_in_history check suppressed the prefix
because the quoted text was already in history, leaving the agent to
guess which destination the user meant. Now the pointer is always present.

Drops the found_in_history guard added in #1594. Token overhead is
minimal (snippet capped at 500 chars on the new user turn; cached prefix
unaffected). Behavior becomes deterministic: reply sent ⇒ pointer present.

Thanks to smartyi for flagging this.
2026-04-21 13:33:02 -07:00
Teknium
7ff7155cbd fix(skills/llama-cpp): concise description, restore python bindings, fix curl
- Description truncated to 60 chars in system prompt (extract_skill_description),
  so the 500-char HF workflow description never reached the agent; shortened to
  'llama.cpp local GGUF inference + HF Hub model discovery.' (56 chars).
- Restore llama-cpp-python section (basic, chat+stream, embeddings,
  Llama.from_pretrained) and frontmatter dependencies entry.
- Fix broken 'Authorization: Bearer ***' curl line (missing closing quote;
  llama-server doesn't require auth by default).
2026-04-21 13:30:10 -07:00
burtenshaw
d6cf2cc058 improve llama.cpp skill 2026-04-21 13:30:10 -07:00
Brooklyn Nicholson
48f8244873 fix(tui): route skills.manage through the long-handler thread pool
`/skills browse` is documented to scan 6 sources and take ~15s, but the
gateway dispatched `skills.manage` on the main RPC thread.  While it
ran, every other inbound RPC — completions, new slash commands, even
`approval.respond` — blocked until the HTTP fetches finished, making
the whole TUI feel frozen.  Reported during TUI v2 retest:
"/skills browse blocks everything else".

`_LONG_HANDLERS` already exists precisely for this pattern (slash.exec,
shell.exec, session.resume, etc. run on `_pool`).  Add `skills.manage`
to that set so browse/search/install run off the dispatcher; the fast
`list` / `inspect` actions pay a negligible thread-pool hop.
2026-04-21 15:06:51 -05:00
Brooklyn Nicholson
dd5ead1007 fix(tui): preserve prior segment output on Ctrl+C interrupt
interruptTurn only flushed the in-flight streaming chunk (bufRef) to
the transcript before calling idle(), which wiped segmentMessages and
pendingSegmentTools. Every tool call and commentary line the agent had
already emitted in the current turn disappeared the moment the user
cancelled, even though that output is exactly what they want to keep
when they hit Ctrl+C (quote from the blitz feedback: "everything was
fine up until the point where you wanted to push to main").

Append each flushed segment message to the transcript first, then
render the in-flight partial with the `*[interrupted]*` marker and its
pendingSegmentTools. Sys-level "interrupted" note still fires when
there is nothing to preserve.
2026-04-21 14:48:50 -05:00
Brooklyn Nicholson
887dfc4067 fix(tui): pager supports scrolling (up/down/page/top/bottom)
The pager overlay backing /history, /toolsets, /help and any paged slash
output only advanced with Enter/Space and closed at the end. Could not
scroll back, scroll line-by-line, or jump to endpoints.

Adds Up/Down (↑↓, j/k), PgUp (b), g/G for top/bottom, keeps existing
Enter/Space/PgDn forward-and-auto-close, and clamps offset so
over-scrolling past the last page is a no-op.
2026-04-21 14:48:26 -05:00
Brooklyn Nicholson
34f24daa8d fix(tui): stabilize slash-completion dropdown height
The completion popup (e.g. typing `/model`) grew from 8 rows at
compIdx=0 up to 16 rows at compIdx≥8 — the slice end was `compIdx + 8`
so every arrow-down added another rendered row until the window filled.
Reported during TUI v2 retest: "as i scroll and more options appear,
for some reason more options appear and it expands the height".

Fixed viewport (`COMPLETION_WINDOW = 16`) centered on compIdx, clamped
so it never slides past the array bounds.  Renders exactly
`min(WINDOW, completions.length)` rows every frame.
2026-04-21 14:43:18 -05:00
Brooklyn Nicholson
4ada76b6ed fix(tui): truncate long picker rows so the height stays stable
A6 added a fixed-height grid (Array.from({length: VISIBLE})), but the
row <Text> itself had no wrap prop so Ink defaulted to wrap="wrap".
A sufficiently long model or provider name would wrap to a second
visual line and bounce the overall picker height right back — which
is exactly what reappeared during the TUI v2 blitz retest on /model.

Pin every picker row (and the empty-state / padding rows) to
wrap="truncate-end" so each slot is guaranteed one line.  Applies
across modelPicker, sessionPicker, and skillsHub.
2026-04-21 14:43:18 -05:00
Brooklyn Nicholson
9d9db1e910 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
2026-04-21 14:31:48 -05:00
Brooklyn Nicholson
f0b763c74f fix(model-switch): drop stale provider from fallback chain and env after /model
Reported during the TUI v2 blitz test: switching from openrouter to
anthropic via `/model <name> --provider anthropic` appeared to succeed,
but the next turn kept hitting openrouter — the provider the user was
deliberately moving away from.

Two gaps caused this:

1. `Agent.switch_model` reset `_fallback_activated` / `_fallback_index`
   but left `_fallback_chain` intact. The chain was seeded from
   `fallback_providers:` at agent init for the *original* primary, so
   when the new primary returned 401 (invalid/expired Anthropic key),
   `_try_activate_fallback()` picked the old provider back up without
   informing the user. Prune entries matching either the old primary
   (user is moving away) or the new primary (redundant) whenever the
   primary provider actually changes.

2. `_apply_model_switch` persisted `HERMES_MODEL` but never updated
   `HERMES_INFERENCE_PROVIDER`. Any ambient re-resolution of the runtime
   (credential pool refresh, compressor rebuild, aux clients) falls
   through to that env var in `resolve_requested_provider`, so it kept
   reporting the original provider even after an in-memory switch.

Adds three regression tests: fallback-chain prune on primary change,
no-op on same-provider model swap, and env-var sync on explicit switch.
2026-04-21 14:31:47 -05:00
Brooklyn Nicholson
fc6a27098e fix(tui): raise picker selection contrast with inverse + bold
Selected rows in the model/session/skills pickers and approval/clarify
prompts only changed from dim gray to cornsilk, which reads as low
contrast on lighter themes and LCDs (reported during TUI v2 blitz).

Switch the selected row to `inverse bold` with the brand accent color
across modelPicker, sessionPicker, skillsHub, and prompts so the
highlight is terminal-portable and unambiguous. Unselected rows stay
dim. Also extends the sessionPicker middle meta column (which was
always dim) to inherit the row's selection state.
2026-04-21 14:31:21 -05:00
Brooklyn Nicholson
c3b8c8e42c fix(tui): stabilize model picker viewport height
Warning row, "↑ N more" / "↓ N more" hints, and the items list were all
conditionally rendered, so the picker jumped in size as the selection
moved or providers without a warning slid into view.

Render every slot unconditionally: warning falls back to a blank line,
hints render an empty string when at the edge, and the items grid always
emits VISIBLE rows padded with blanks. Height is now constant across
providers, model counts, and scroll position.
2026-04-21 14:31:21 -05:00
Brooklyn Nicholson
83c1d4ec27 fix(tui): delegate unknown /tools subcommand to slash.exec
/tools' local handler silently returned for anything other than enable
or disable, so /tools list and friends looked broken even though the
Python CLI already implements them (hermes_cli/main.py registers
tools_sub for list/enable/disable).

Keep the client-owned enable/disable path (which has to run
session.setSessionStartedAt + resetVisibleHistory locally) and route
every other sub through slash.exec, matching createSlashHandler's
page/sys split for long vs short output.
2026-04-21 14:30:48 -05:00
Brooklyn Nicholson
d86c886b31 fix(tui): readline parity on Linux — Ctrl+A = home, Alt+B/F word nav
textInput treated the platform action-mod (Cmd on macOS, Ctrl on Linux)
as the sole word-boundary modifier. On Linux that meant:

- Ctrl+A selected all instead of jumping to line start (contra standard
  readline and the hotkey doc in README.md which says `Ctrl+A` = Start
  of line).
- Alt+B / Alt+F / Alt+Backspace / Alt+Delete were dropped, because
  `key.meta` was never consulted — the README already documented
  `Meta+B` / `Meta+F` as word nav.

Gate select-all to macOS Cmd+A (`isMac && mod && inp === 'a'`), route
Linux Ctrl+A through `actionHome`, and broaden every word-boundary
predicate (b/f/Backspace/Delete and the modified arrow keys) from `mod`
to `wordMod = mod || k.meta` so Alt chords work on Linux and Mac while
existing Ctrl/Cmd chords keep working.
2026-04-21 14:30:47 -05:00
Brooklyn Nicholson
4b0686f63d fix(tui): apply path/@ completion on Enter
Completion selection on Enter was gated to slash commands only
(value.startsWith('/')), so @file, ./path, and ~/path completions fell
through and submitted the incomplete input instead of inserting the
highlighted row.

Guard on completions.length && compReplace > 0 — useCompletion already
scopes population to slash and path tokens, and the next !== value check
keeps plain-text submits working when the completion is already applied.
2026-04-21 14:30:45 -05:00
Jeffrey Quesnelle
ce98e1ef11
Merge pull request #13652 from IAvecilla/fix-underscore-display
fix(cli): keep snake_case underscores intact in strip markdown mode
2026-04-21 15:09:36 -04:00
IAvecilla
54c2261214
Rename test variables 2026-04-21 16:00:34 -03:00
ethernet
943602b68a
Merge pull request #13646 from NousResearch/fix/nix
update package.locks to build in nix
2026-04-21 14:54:23 -04:00
Ari Lotter
ce0ecce6cf update package.locks 2026-04-21 14:42:49 -04:00
IAvecilla
aa61831a14
fix(cli): keep snake_case underscores intact in strip markdown mode 2026-04-21 15:32:59 -03:00
Austin Pickett
b2111a2b45
Merge pull request #13526 from NousResearch/feat/dashboard-action-buttons
feat: add buttons to update hermes and restart gateway
2026-04-21 08:40:26 -07:00
kshitijk4poor
c9e8d82ef4 fix(tui): address code review findings
Medium fixes:
- textInput.tsx: prevent silent data loss when async paste resolves
  after user types — fall back to raw text insert at current cursor
  instead of dropping the content entirely
- useComposerState.ts: tighten looksLikeDroppedPath to require a
  second '/' or '.' for bare absolute paths, avoiding unnecessary
  RPC round-trips for pasted text like /api or /help
- useComposerState.ts: add cross-reference comment linking to the
  canonical _detect_file_drop() in cli.py
- osc52.ts: add 500ms timeout via Promise.race so terminals that
  do not support OSC52 clipboard queries cannot hang paste

Low fixes:
- terminalSetup.ts: export isRemoteShellSession and reuse in
  terminalParity.ts and useComposerState.ts (was inlined 3 times)
- useComposerState.ts: extract insertAtCursor helper, replacing 3
  copies of the lead/tail spacing logic
- useComposerState.ts: remove redundant gw from handleTextPaste
  useCallback dependency array
- terminalSetup.test.ts: add EACCES (read-only keybindings.json)
  and unterminated block comment test coverage
2026-04-21 08:00:00 -07:00
kshitijk4poor
bc9927dc50 fix(tui): address PR review feedback
Fixes from OutThisLife review:
1. Restore Linux Alt+Enter newline: textInput.tsx now uses
   k.shift || (isMac ? isActionMod(k) : k.meta) so Alt+Enter
   inserts a newline on Linux (was broken by isMac guard).
2. Fix image.attach response type: useComposerState.ts now uses
   ImageAttachResponse (which already has remainder) instead of
   InputDetectDropResponse with intersection.
3. Expand looksLikeDroppedPath test coverage with edge cases for
   image extensions, file:// URIs, spaces, empty input, and
   non-file URLs.
4. Make terminalParity.test.ts hermetic: terminalParityHints() now
   accepts optional fileOps/homeDir and passes them through to
   shouldPromptForTerminalSetup(), so tests inject mock readFile
   instead of hitting the real filesystem.

Fixes from Copilot inline review:
5. Remove unused options.now parameter from configureTerminalKeybindings.
6. Replace naive stripJsonComments (full-line // only) with a proper
   JSONC stripper that handles inline // comments, block comments,
   trailing commas, and preserves comment-like sequences in strings.
7. Move backupFile() call from immediately after read to right before
   write - backups are only created when changes will actually be
   written, not on every /terminal-setup invocation.
2026-04-21 08:00:00 -07:00
kshitijk4poor
9556fef5a1 fix(tui): improve macOS paste and shortcut parity
- support Cmd-as-super and readline-style fallback shortcuts on macOS
- add layered clipboard/OSC52 paste handling and immediate image-path attach
- add IDE terminal setup helpers, terminal parity hints, and aligned docs
2026-04-21 08:00:00 -07:00
Austin Pickett
d8d4ef4e20 chore: layout 2026-04-21 10:46:12 -04:00
Teknium
432772dbdf
fix(cache): surface cache-hit telemetry for all providers, not just Anthropic-wire (#13543)
The 💾 Cache footer was gated on `self._use_prompt_caching`, which is
only True for Anthropic marker injection (native Anthropic, OpenRouter
Claude, Anthropic-wire gateways, Qwen on OpenCode/Alibaba). Providers
with automatic server-side prefix caching — OpenAI, Kimi, DeepSeek,
Qwen on OpenRouter — return `prompt_tokens_details.cached_tokens` too,
but users couldn't see their cache % because the display path never
fired for them. Result: people couldn't tell their cache was working or
broken without grepping agent.log.

`canonical_usage` from `normalize_usage()` already unifies all three
API shapes (Anthropic / Codex Responses / OpenAI chat completions) into
`cache_read_tokens` and `cache_write_tokens`. Drop the gate and read
from there — now the footer fires whenever the provider reported any
cached or written tokens, regardless of whether hermes injected markers.

Also removes duplicated branch-per-API-shape extraction code.
2026-04-21 06:42:32 -07:00
Teknium
5e0eed470f
fix(cache): enable prompt caching for Qwen on OpenCode/OpenCode-Go/Alibaba (#13528)
Qwen models on OpenCode, OpenCode Go, and direct DashScope accept
Anthropic-style cache_control markers on OpenAI-wire chat completions,
but hermes only injected markers for Claude-named models. Result: zero
cache hits on every turn, full prompt re-billed — a community user
reported burning through their OpenCode Go subscription on Qwen3.6.

Extend _anthropic_prompt_cache_policy to return (True, False) — envelope
layout, not native — for the Alibaba provider family when the model name
contains 'qwen'. Envelope layout places markers on inner content blocks
(matching pi-mono's 'alibaba' cacheControlFormat) and correctly skips
top-level markers on tool-role messages (which OpenCode rejects).

Non-Qwen models on these providers (GLM, Kimi) keep their existing
behaviour — they have automatic server-side caching and don't need
client markers.

Upstream reference: pi-mono #3392 / #3393 documented this contract for
opencode-go Qwen models.

Adds 7 regression tests covering Qwen3.5/3.6/coder on each affected
provider plus negative cases for GLM/Kimi/OpenRouter-Qwen.
2026-04-21 06:40:58 -07:00
Teknium
244ae6db15
fix(web_server,whatsapp-bridge): validate Host header against bound interface (#13530)
DNS rebinding attack: a victim browser that has the dashboard (or the
WhatsApp bridge) open could be tricked into fetching from an
attacker-controlled hostname that TTL-flips to 127.0.0.1. Same-origin
and CORS checks don't help — the browser now treats the attacker origin
as same-origin with the local service. Validating the Host header at
the app layer rejects any request whose Host isn't one we bound for.

Changes:

hermes_cli/web_server.py:
- New host_header_middleware runs before auth_middleware. Reads
  app.state.bound_host (set by start_server) and rejects requests
  whose Host header doesn't match the bound interface with HTTP 400.
- Loopback binds accept localhost / 127.0.0.1 / ::1. Non-loopback
  binds require exact match. 0.0.0.0 binds skip the check (explicit
  --insecure opt-in; no app-layer defence possible).
- IPv6 bracket notation parsed correctly: [::1] and [::1]:9119 both
  accepted.

scripts/whatsapp-bridge/bridge.js:
- Express middleware rejects non-loopback Host headers. Bridge
  already binds 127.0.0.1-only, this adds the complementary app-layer
  check for DNS rebinding defence.

Tests: 8 new in tests/hermes_cli/test_web_server_host_header.py
covering loopback/non-loopback/zero-zero binds, IPv6 brackets, case
insensitivity, and end-to-end middleware rejection via TestClient.

Reported in GHSA-ppp5-vxwm-4cf7 by @bupt-Yy-young. Hardening — not
CVE per SECURITY.md §3. The dashboard's main trust boundary is the
loopback bind + session token; DNS rebinding defeats the bind assumption
but not the token (since the rebinding browser still sees a first-party
fetch to 127.0.0.1 with the token-gated API). Host-header validation
adds the missing belt-and-braces layer.
2026-04-21 06:26:35 -07:00
Teknium
16accd44bd
fix(telegram): require TELEGRAM_WEBHOOK_SECRET in webhook mode (#13527)
When TELEGRAM_WEBHOOK_URL was set but TELEGRAM_WEBHOOK_SECRET was not,
python-telegram-bot received secret_token=None and the webhook endpoint
accepted any HTTP POST. Anyone who could reach the listener could inject
forged updates — spoofed user IDs, spoofed chat IDs, attacker-controlled
message text — and trigger handlers as if Telegram delivered them.

The fix refuses to start the adapter in webhook mode without the secret.
Polling mode (default, no webhook URL) is unaffected — polling is
authenticated by the bot token directly.

BREAKING CHANGE for webhook-mode deployments that never set
TELEGRAM_WEBHOOK_SECRET. The error message explains remediation:

  export TELEGRAM_WEBHOOK_SECRET="$(openssl rand -hex 32)"

and instructs registering it with Telegram via setWebhook's secret_token
parameter. Release notes must call this out.

Reported in GHSA-3vpc-7q5r-276h by @bupt-Yy-young. Hardening — not CVE
per SECURITY.md §3 "Public Exposure: Deploying the gateway to the
public internet without external authentication or network protection"
covers the historical default, but shipping a fail-open webhook as the
default was the wrong choice and the guard aligns us with the SECURITY.md
threat model.
2026-04-21 06:23:09 -07:00
Teknium
62348cffbe
fix(acp): wire approval callback + make it thread-local (#13525)
Two related ACP approval issues:

GHSA-96vc-wcxf-jjff — ACP's _run_agent never set HERMES_INTERACTIVE
(or any other flag recognized by tools.approval), so check_all_command_guards
took the non-interactive auto-approve path and never consulted the
ACP-supplied approval callback (conn.request_permission). Dangerous
commands executed in ACP sessions without operator approval despite
the callback being installed. Fix: set HERMES_INTERACTIVE=1 around
the agent run so check_all_command_guards routes through
prompt_dangerous_approval(approval_callback=...) — the correct shape
for ACP's per-session request_permission call. HERMES_EXEC_ASK would
have routed through the gateway-queue path instead, which requires a
notify_cb registered in _gateway_notify_cbs (not applicable to ACP).

GHSA-qg5c-hvr5-hjgr — _approval_callback and _sudo_password_callback
were module-level globals in terminal_tool. Concurrent ACP sessions
running in ThreadPoolExecutor threads each installed their own callback
into the same slot, racing. Fix: store both callbacks in threading.local()
so each thread has its own slot. CLI mode (single thread) is unaffected;
gateway mode uses a separate queue-based approval path and was never
touched.

set_approval_callback is now called INSIDE _run_agent (the executor
thread) rather than before dispatching — so the TLS write lands on the
correct thread.

Tests: 5 new in tests/acp/test_approval_isolation.py covering
thread-local isolation of both callbacks and the HERMES_INTERACTIVE
callback routing. Existing tests/acp/ (159 tests) and tests/tools/
approval-related tests continue to pass.

Fixes GHSA-96vc-wcxf-jjff
Fixes GHSA-qg5c-hvr5-hjgr
2026-04-21 06:20:40 -07:00
Teknium
ba4357d13b
fix(env_passthrough): reject Hermes provider credentials from skill passthrough (#13523)
A skill declaring `required_environment_variables: [ANTHROPIC_TOKEN]` in
its SKILL.md frontmatter silently bypassed the `execute_code` sandbox's
credential-scrubbing guarantee. `register_env_passthrough` had no
blocklist, so any name a skill chose flipped `is_env_passthrough(name) =>
True`, which shortcircuits the sandbox's secret filter.

Fix: reject registration when the name appears in
`_HERMES_PROVIDER_ENV_BLOCKLIST` (the canonical list of Hermes-managed
credentials — provider keys, gateway tokens, etc.). Log a warning naming
GHSA-rhgp-j443-p4rf so operators see the rejection in logs.

Non-Hermes third-party API keys (TENOR_API_KEY for gif-search,
NOTION_TOKEN for notion skills, etc.) remain legitimately registerable —
they were never in the sandbox scrub list in the first place.

Tests: 16 -> 17 passing. Two old tests that documented the bypass
(`test_passthrough_allows_blocklisted_var`, `test_make_run_env_passthrough`)
are rewritten to assert the new fail-closed behavior. New
`test_non_hermes_api_key_still_registerable` locks in that legitimate
third-party keys are unaffected.

Reported in GHSA-rhgp-j443-p4rf by @q1uf3ng. Hardening; not CVE-worthy
on its own per the decision matrix (attacker must already have operator
consent to install a malicious skill).
2026-04-21 06:14:25 -07:00
Teknium
7fc1e91811
security(runtime_provider): close OLLAMA_API_KEY substring-leak sweep miss (#13522)
Two call sites still used a raw substring check to identify ollama.com:

  hermes_cli/runtime_provider.py:496:
      _is_ollama_url = "ollama.com" in base_url.lower()

  run_agent.py:6127:
      if fb_base_url_hint and "ollama.com" in fb_base_url_hint.lower() ...

Same bug class as GHSA-xf8p-v2cg-h7h5 (OpenRouter substring leak), which
was fixed in commit dbb7e00e via base_url_host_matches() across the
codebase. The earlier sweep missed these two Ollama sites. Self-discovered
during April 2026 security-advisory triage; filed as GHSA-76xc-57q6-vm5m.

Impact is narrow — requires a user with OLLAMA_API_KEY configured AND a
custom base_url whose path or look-alike host contains 'ollama.com'.
Users on default provider flows are unaffected. Filed as a draft advisory
to use the private-fork flow; not CVE-worthy on its own.

Fix is mechanical: replace substring check with base_url_host_matches
at both sites. Same helper the rest of the codebase uses.

Tests: 67 -> 71 passing. 7 new host-matcher cases in
tests/test_base_url_hostname.py (path injection, lookalike host,
localtest.me subdomain, ollama.ai TLD confusion, localhost, genuine
ollama.com, api.ollama.com subdomain) + 4 call-site tests in
tests/hermes_cli/test_runtime_provider_resolution.py verifying
OLLAMA_API_KEY is selected only when base_url actually targets
ollama.com.

Fixes GHSA-76xc-57q6-vm5m
2026-04-21 06:06:16 -07:00
Austin Pickett
fc21c14206 feat: add buttons to update hermes and restart gateway 2026-04-21 09:01:23 -04:00
Teknium
4cc5065f63 fix(acp): follow-up — named-const page size, alias kwarg, tests
- Replace kwargs.get('limit', 50) with module-level _LIST_SESSIONS_PAGE_SIZE
  constant. ListSessionsRequest schema has no 'limit' field, so the kwarg
  path was dead. Constant is the single source of truth for the page cap.
- Use next_cursor= (field name) instead of nextCursor= (alias). Both work
  under the schema's populate_by_name config, but using the declared
  Python field name is the consistent style in this file.
- Add docstring explaining cwd pass-through and cursor semantics.
- Add 4 tests: first-page with next_cursor, single-page no next_cursor,
  cursor resumes after match, unknown cursor returns empty page.
2026-04-21 06:00:41 -07:00
Aniruddha Adak
c1fb7b6d27 fix: support pagination and cwd filtering in list_sessions 2026-04-21 06:00:41 -07:00
Aniruddha Adak
ea06104a3c fix(permissions): handle None response from ACP request_permission 2026-04-21 05:57:23 -07:00
Teknium
027751606a chore(release): add UNLINEARITY to AUTHOR_MAP 2026-04-21 05:52:46 -07:00
unlinearity
155b619867 fix(agent): normalize socks:// env proxies for httpx/anthropic
WSL2 / Clash-style setups often export ALL_PROXY=socks://127.0.0.1:PORT. httpx and the Anthropic SDK reject that alias and expect socks5://, so agent startup failed early with "Unknown scheme for proxy URL" before any provider request could proceed.

Add shared normalize_proxy_url()/normalize_proxy_env_vars() helpers in utils.py and route all proxy entry points through them:
  - run_agent._get_proxy_from_env
  - agent.auxiliary_client._validate_proxy_env_urls
  - agent.anthropic_adapter.build_anthropic_client
  - gateway.platforms.base.resolve_proxy_url

Regression coverage:
  - run_agent proxy env resolution
  - auxiliary proxy env normalization
  - gateway proxy URL resolution

Verified with:
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 /home/nonlinear/.hermes/hermes-agent/venv/bin/pytest -o addopts='' -p pytest_asyncio.plugin tests/run_agent/test_create_openai_client_proxy_env.py tests/agent/test_proxy_and_url_validation.py tests/gateway/test_proxy_mode.py

39 passed.
2026-04-21 05:52:46 -07:00
Teknium
bd342f30a2
chore: remove stale requirements.txt in favor of pyproject.toml (#13515)
The root requirements.txt has drifted from pyproject.toml for years
(unpinned, missing deps like slack-bolt, slack-sdk, exa-py, anthropic)
and no part of the codebase (CI, Dockerfiles, scripts, docs) consumes
it. It exists only for drive-by 'pip install -r requirements.txt'
users and will drift again within weeks of any sync.

Canonical install remains:
    pip install -e ".[all]"

Closes #13488 (thanks @hobostay — your sync was correct, we're just
deleting the drift trap instead of patching it).
2026-04-21 05:52:22 -07:00
teknium1
267b2faa15 test(cron): exercise _deliver_result and _send_media_via_adapter directly for timeout-cancel
The original tests replicated the try/except/cancel/raise pattern inline with
a mocked future, which tested Python's try/except semantics rather than the
scheduler's behavior. Rewrite them to invoke _deliver_result and
_send_media_via_adapter end-to-end with a real concurrent.futures.Future
whose .result() raises TimeoutError.

Mutation-verified: both tests fail when the try/except wrappers are removed
from cron/scheduler.py, pass with them in place.
2026-04-21 05:52:16 -07:00
VTRiot
18e7fd8364 fix(cron): cancel orphan coroutine on delivery timeout before standalone fallback
When the live adapter delivery path (_deliver_result) or media send path
(_send_media_via_adapter) times out at future.result(timeout=N), the
underlying coroutine scheduled via asyncio.run_coroutine_threadsafe can
still complete on the event loop, causing a duplicate send after the
standalone fallback runs.

Cancel the future on TimeoutError before re-raising, so the standalone
fallback is the sole delivery path.

Adds TestDeliverResultTimeoutCancelsFuture and
TestSendMediaTimeoutCancelsFuture.
2026-04-21 05:52:16 -07:00
VTRiot
3cc4d7374f chore: register VTRiot in AUTHOR_MAP 2026-04-21 05:52:16 -07:00
zhangguangtao
5c54019055 fix(skills): respect HERMES_SESSION_PLATFORM in _is_skill_disabled
Fixes #13027

Previously, `_is_skill_disabled()` only checked the explicit `platform`
argument and `os.getenv('HERMES_PLATFORM')`, missing the gateway session
context (`HERMES_SESSION_PLATFORM`). This caused `skill_view()` to expose
skills that were platform-disabled for the active gateway session.

Add `_get_session_platform()` helper that resolves the platform from
`gateway.session_context.get_session_env`, mirroring the logic in
`agent.skill_utils.get_disabled_skill_names()`.

Now the platform resolution follows the same precedence as skill_utils:
1. Explicit `platform` argument
2. `HERMES_PLATFORM` environment variable
3. `HERMES_SESSION_PLATFORM` from gateway session context
2026-04-21 05:42:32 -07:00
teknium1
793199ab0b chore(release): add mengjian-github to AUTHOR_MAP 2026-04-21 05:32:27 -07:00
Kian Meng
063bc3c1e2 fix(kimi): send max_tokens, reasoning_effort, and thinking for Kimi/Moonshot
Kimi/Moonshot endpoints require explicit parameters that Hermes was not
sending, causing 'Response truncated due to output length limit' errors
and inconsistent reasoning behavior.

Root cause analysis against Kimi CLI source (MoonshotAI/kimi-cli,
packages/kosong/src/kosong/chat_provider/kimi.py):

1. max_tokens: Kimi's API defaults to a very low value when omitted.
   Reasoning tokens share the output budget — the model exhausts it on
   thinking alone.  Send 32000, matching Kimi CLI's generate() default.

2. reasoning_effort: Kimi CLI sends this as a top-level parameter (not
   inside extra_body).  Hermes was not sending it at all because
   _supports_reasoning_extra_body() returns False for non-OpenRouter
   endpoints.

3. extra_body.thinking: Kimi CLI uses with_thinking() which sets
   extra_body.thinking={"type":"enabled"} alongside reasoning_effort.
   This is a separate control from the OpenAI-style reasoning extra_body
   that Hermes sends for OpenRouter/GitHub.  Without it, the Kimi gateway
   may not activate reasoning mode correctly.

Covers api.kimi.com (Kimi Code) and api.moonshot.ai/cn (Moonshot).

Tests: 6 new test cases for max_tokens, reasoning_effort, and
extra_body.thinking under various configs.
2026-04-21 05:32:27 -07:00
Teknium
3f72b2fe15 fix(/model): accept provider switches when /models is unreachable
Gateway /model <name> --provider opencode-go (or any provider whose /models
endpoint is down, 404s, or doesn't exist) silently failed. validate_requested_model
returned accepted=False whenever fetch_api_models returned None, switch_model
returned success=False, and the gateway never wrote _session_model_overrides —
so the switch appeared to succeed in the error message flow but the next turn
kept calling the old provider.

The validator already had static-catalog fallbacks for MiniMax and Codex
(providers without a /models endpoint). Extended the same pattern as the
terminal fallback: when the live probe fails, consult provider_model_ids()
for the curated catalog. Known models → accepted+recognized. Close typos →
auto-corrected. Unknown models → soft-accepted with a 'Not in curated
catalog' warning. Providers with no catalog at all → soft-accepted with a
generic 'Note:' warning, finally honoring the in-code comment ('Accept and
persist, but warn') that had been lying since it was written.

Tests: 7 new tests in test_opencode_go_validation_fallback.py covering the
catalog lookup, case-insensitive match, auto-correct, unknown-with-suggestion,
unknown-without-suggestion, and no-catalog paths. TestValidateApiFallback in
test_model_validation.py updated — its four 'rejected_when_api_down' tests
were encoding exactly the bug being fixed.
2026-04-21 05:19:43 -07:00
Ben
484d151e99 fix(mcp): reset circuit breaker on successful OAuth reconnect
Previously the breaker was only cleared when the post-reconnect retry
call itself succeeded (via _reset_server_error at the end of the try
block). If OAuth recovery succeeded but the retry call happened to
fail for a different reason, control fell through to the
needs_reauth path which called _bump_server_error — adding to an
already-tripped count instead of the fresh count the reconnect
justified. With fix #1 in place this would still self-heal on the
next cooldown, but we should not pay a 60s stall when we already
have positive evidence the server is viable.

Move _reset_server_error(server_name) up to immediately after the
reconnect-and-ready-wait block, before the retry_call. The
subsequent retry still goes through _bump_server_error on failure,
so a genuinely broken server re-trips the breaker as normal — but
the retry starts from a clean count (1 after a failure), not a
stale one.
2026-04-21 05:19:03 -07:00
Ben
8cc3cebca2 fix(mcp): add half-open state to circuit breaker
The MCP circuit breaker previously had no path back to the closed
state: once _server_error_counts[srv] reached _CIRCUIT_BREAKER_THRESHOLD
the gate short-circuited every subsequent call, so the only reset
path (on successful call) was unreachable. A single transient
3-failure blip (bad network, server restart, expired token) permanently
disabled every tool on that MCP server for the rest of the agent
session.

Introduce a classic closed/open/half-open state machine:

- Track a per-server breaker-open timestamp in _server_breaker_opened_at
  alongside the existing failure count.
- Add _CIRCUIT_BREAKER_COOLDOWN_SEC (60s). Once the count reaches
  threshold, calls short-circuit for the cooldown window.
- After the cooldown elapses, the *next* call falls through as a
  half-open probe that actually hits the session. Success resets the
  breaker via _reset_server_error; failure re-bumps the count via
  _bump_server_error, which re-stamps the open timestamp and re-arms
  the cooldown.

The error message now includes the live failure count and an
"Auto-retry available in ~Ns" hint so the model knows the breaker
will self-heal rather than giving up on the tool for the whole
session.

Covers tests 1 (half-opens after cooldown) and 2 (reopens on probe
failure); test 3 (cleared on reconnect) still fails pending fix #2.
2026-04-21 05:19:03 -07:00
Ben
724377c429 test(mcp): add failing tests for circuit-breaker recovery
The MCP circuit breaker in tools/mcp_tool.py has no half-open state and
no reset-on-reconnect behavior, so once it trips after 3 consecutive
failures it stays tripped for the process lifetime. These tests lock
in the intended recovery behavior:

1. test_circuit_breaker_half_opens_after_cooldown — after the cooldown
   elapses, the next call must actually probe the session; success
   closes the breaker.
2. test_circuit_breaker_reopens_on_probe_failure — a failed probe
   re-arms the cooldown instead of letting every subsequent call
   through.
3. test_circuit_breaker_cleared_on_reconnect — a successful OAuth
   recovery resets the breaker even if the post-reconnect retry
   fails (a successful reconnect is sufficient evidence the server
   is viable again).

All three currently fail, as expected.
2026-04-21 05:19:03 -07:00
Teknium
c6974043ef
refactor(acp): validate method_id against advertised provider in authenticate() (#13468)
* feat(models): hide OpenRouter models that don't advertise tool support

Port from Kilo-Org/kilocode#9068.

hermes-agent is tool-calling-first — every provider path assumes the
model can invoke tools. Models whose OpenRouter supported_parameters
doesn't include 'tools' (e.g. image-only or completion-only models)
cannot be driven by the agent loop and fail at the first tool call.

Filter them out of fetch_openrouter_models() so they never appear in
the model picker (`hermes model`, setup wizard, /model slash command).

Permissive when the field is missing — OpenRouter-compatible gateways
(Nous Portal, private mirrors, older snapshots) don't always populate
supported_parameters. Treat missing as 'unknown → allow' rather than
silently emptying the picker on those gateways. Only hide models
whose supported_parameters is an explicit list that omits tools.

Tests cover: tools present → kept, tools absent → dropped, field
missing → kept, malformed non-list → kept, non-dict item → kept,
empty list → dropped.

* refactor(acp): validate method_id against advertised provider in authenticate()

Previously authenticate() accepted any method_id whenever the server had
provider credentials configured. This was not a vulnerability under the
personal-assistant trust model (ACP is stdio-only, local-trust — anything
that can reach the transport is already code-execution-equivalent to the
user), but it was sloppy API hygiene: the advertised auth_methods list
from initialize() was effectively ignored.

Now authenticate() only returns AuthenticateResponse when method_id
matches the currently-advertised provider (case-insensitive). Mismatched
or missing method_id returns None, consistent with the no-credentials
case.

Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE
(ACP transport is stdio, local-trust model), but the correctness fix is
worth having on its own.
2026-04-21 03:39:55 -07:00