Commit graph

7117 commits

Author SHA1 Message Date
Tuna Dev
ff4c8172ce fix(gateway): attach credential_pool to session /model overrides
Per-session /model overrides supplied api_key and provider but omitted
credential_pool, so billing rotation never ran on HTTP 402. Wire the pool
on fast override, rehydrate, and apply paths; backfill from provider for
legacy persisted overrides. Regression tests in tests/gateway/.
2026-07-04 16:34:16 -07:00
luyifan
70dffb6f1f fix(codex): recover final app-server text without completion 2026-07-04 15:44:50 -07:00
Jigoooo
ddd3a2d247 fix(auxiliary): fall back to token resolver when anthropic pool has no usable entry
_try_anthropic() hard-failed (return None, None) when the anthropic
credential pool was present but had no selectable entry — e.g. the pooled
OAuth token expired and its refresh_token had gone stale, so
_select_pool_entry("anthropic") returned (True, None). This wedged every
auxiliary task routed to Anthropic (goal judge surfaced "no auxiliary
client configured") even when a perfectly valid ANTHROPIC_TOKEN /
credentials-file token was available. The main session stayed healthy
because it resolves the env token directly.

The openrouter path (_try_openrouter) and codex path already fall through
to their standalone credential on (True, None); anthropic was the only
provider that hard-failed. Make _try_anthropic fall through to
resolve_anthropic_token() on that branch so the three paths are symmetric:
a temporarily dead pool entry must not block auxiliary tasks when a valid
standalone credential exists.

Adds a regression test covering: (1) pool present + no entry + valid env
token -> client built from the env token, (2) pool present + no entry + no
resolvable token -> clean (None, None), (3) base_url defaults correctly
when falling through with pool_present=True.
2026-07-04 15:44:50 -07:00
teknium1
0d27d2ed14 fix(vision): bound the sandbox exec-read at the ingest cap
The container exec-read piped the whole file through base64 with no size
guard — the 50MB cap was only enforced host-side AFTER the full payload
had already streamed into host memory. A prompt-injected read of a huge
container file (or /dev/zero) could balloon the gateway process.

head -c (cap+1) bounds the read inside the sandbox; the +1 byte lets the
host distinguish at-cap from over-cap and reject with SourceTooLarge.
Input redirect replaces 'base64 --' (no argv exposure at all for
leading-dash paths). Docker integration tests re-verified live.
2026-07-04 15:30:50 -07:00
teknium1
4ac8c7546b fix(vision): mkdir converted-PNG output dir; wire SVG pass-through to rasterizer; AUTHOR_MAP for #52688
- _normalize_to_supported_image: ensure cache/vision exists before writing
  the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError).
- resolver: SVG passes through as image/svg+xml instead of erroring; the
  call sites rasterize to PNG via the salvaged normalize step (cairosvg /
  svglib / rsvg-convert / inkscape, best-effort with actionable error).
- normalization offloaded via asyncio.to_thread at both call sites.
- tests: resolver pass-through + rasterization + no-converter error paths.
- AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR #52688 salvage).
2026-07-04 15:30:50 -07:00
Jonathan Almanzar
ac94f2c8a1 fix(vision): convert SVG/unsupported image formats to PNG before embedding
vision_analyze embedded SVG (and BMP/TIFF) tool-results into conversation
history with media_type image/svg+xml. Anthropic only accepts jpeg/png/
gif/webp, so the request fails with a non-retryable 400. Because the image
is baked into immutable history and re-sent every turn, the session is
permanently wedged on resume — retries re-send the same bad bytes.

Add _normalize_to_supported_image(): SVG is rasterized to PNG (best-effort
via cairosvg/svglib/rsvg-convert/inkscape), other non-supported raster
formats are re-encoded to PNG via Pillow, and if conversion is impossible
the tool returns an actionable error instead of a session-wedging payload.
Wired into both the native-vision fast path and the auxiliary-API path so
the whole bug class is covered, not just the one call site.

All 99 existing vision tests pass.
2026-07-04 15:30:50 -07:00
teknium1
ab2f5a077e fix(vision): address review — restore bare relative paths, remove dead code, async I/O
- resolve_image_source: bare cwd-relative filenames ('pic.png') resolve
  again (main accepted them; the path-shape gate regressed them — review
  by egilewski). Unknown explicit schemes (ftp://, s3://) still rejected.
- Local backend: nonexistent path now raises a clean 'image file not
  found' instead of a misleading sandbox-fallback message.
- W4: remove the now-dead path-based _detect_image_mime_type (suffix-trust
  SVG acceptance) so future callers can't reintroduce it.
- W3: SVG sources rejected with a dedicated actionable message + test.
- Polish: host read_bytes / temp-file write_bytes offloaded via
  asyncio.to_thread (matches the container exec-read); unused
  ResolveContext.cfg/extra_roots fields dropped; duplicate policy check
  documented as intentional pre-flight short-circuit.
2026-07-04 15:30:50 -07:00
emozilla
316e77517e fix(vision): unified image-source resolver + terminal-backend confinement
Salvage of #35362, evolved to also close the vision sandbox-escape
(GHSA-gpxw-6wxv-w3qq). The two were the same root cause — vision read image
bytes host-side while every other tool reads through the terminal backend —
so one resolver fixes both the delivery gaps and the escape.

Delivery (from #35362, re-authored against current main since the branch was
4140 commits stale and vision_tools.py had been rewritten on both sides):
- tools/image_source.py: one resolver for data:/http(s)/file/local/container
  image sources, returning raw bytes through a single magic-byte-sniff +
  50MB-ingest chokepoint. Fixes 'no image attached' / 'Invalid image source'
  for every source type (#7571, #25118, #29643, #22328, #32709, #9077).
- tools/credential_files.py: from_agent_visible_cache_path, the container->host
  cache reverse-map (inverse of the existing forward twin).
- tools/vision_tools.py: both vision sites route through the resolver with
  task_id threaded from the handler; resolved bytes are materialized to a temp
  file so main's evolved encode/resize/embed-cap pipeline is reused verbatim
  (kept over the PR's older bytes-core resize to avoid touching browser_tool /
  conversation_compression callers).

Security (fills #35362's deliberately-stubbed _within_allowed_roots seam):
- Under a non-local terminal backend the file tools are confined to the sandbox
  (SECURITY.md 2.2), but vision read host-side — a prompt-injected
  vision_analyze('/etc/passwd') exfiltrated host secrets, and read_file even
  redirects the model to vision_analyze for image paths. The resolver now
  enforces the same boundary: local backend reads any host path (chosen
  posture); non-local backend host-reads ONLY the media caches under
  HERMES_HOME (where the gateway/download media lives) and routes every other
  path to an in-sandbox base64 exec-read — which reads the CONTAINER's file,
  the same one 'cat' would, never the host's. Paths are resolve()-d so a
  symlink can't escape a cache; fail-closed when no sandbox env exists.
  This closes the escape AND delivers container-only images (#32709) with the
  same mechanism.

Tests: unified resolver + confinement model (tests/tools/test_image_source.py,
incl. proof a non-cache host path under Docker yields container bytes not the
host secret); existing vision tests updated to the resolver boundary; Docker
integration test verified green against a real daemon (exec-read of a tmpfs
/workspace file, a root-owned mode-600 file, and the host-secret invariant).

Fixes GHSA-gpxw-6wxv-w3qq.
Co-authored-by: banditburai <promptsiren@gmail.com>
2026-07-04 15:30:50 -07:00
luyifan
c3ab1424e6 fix(telegram): redact transport error tokens 2026-07-04 15:18:41 -07:00
luyifan
2b58febe46 fix(redact): cover fireworks token prefixes 2026-07-04 15:18:41 -07:00
Teknium
e670d9cdd6
Merge pull request #58489 from NousResearch/revert-30179
Revert "feat(egress): iron-proxy credential-injection firewall" (#30179)
2026-07-04 13:41:24 -07:00
Danilo Falcão
058873805a fix(update): skip unsupported Matrix refresh on Windows 2026-07-04 13:40:59 -07:00
teknium1
c6dc7c03c3
Revert "Merge pull request #30179 from NousResearch/feat/iron-proxy"
This reverts commit 8790adc4c6, reversing
changes made to fe5054bccf.
2026-07-04 13:38:59 -07:00
Teknium
8790adc4c6
Merge pull request #30179 from NousResearch/feat/iron-proxy
feat(egress): iron-proxy credential-injection firewall for sandboxes
2026-07-04 13:29:23 -07:00
峯岸 亮
fe5054bccf fix(desktop): avoid probing custom providers on model picker open 2026-07-04 13:29:00 -07:00
infinitycrew39
bd480b4c8c test(gateway): cover per-backend cwd placeholder resolution
Add unit tests for resolve_placeholder_terminal_cwd and extend the config
bridge simulation for docker mount-on vs mount-off vs local fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 13:28:47 -07:00
kshitij
7203898ce4
Merge pull request #58350 from kshitijk4poor/salvage/dedup-tool-call-id
fix(agent): deduplicate tool_call_id across pre-API sanitizers (#58327)
2026-07-04 21:37:28 +05:30
kshitijk4poor
81f1ba8002 test(telegram): cancel leaked conflict-retry task before fatal assertion
The conflict-retry ladder schedules a background recovery task via
loop.create_task(self._handle_polling_conflict(...)) on each failed
start_polling. test_polling_conflict_becomes_fatal_after_retries never
cancelled the last one, so under a loaded scheduler a leaked task could
get a turn, re-drive the counter into the fatal branch, and fire
_notify_fatal_error a second time — breaking assert_awaited_once()
non-deterministically. The bounded updater.stop() guard added in this
salvage introduced an extra await/scheduling yield that surfaced the
latent leak in CI slice 3. Cancel the leaked task before the fatal
assertions so the test is deterministic regardless of scheduler timing.
2026-07-04 21:33:50 +05:30
kshitijk4poor
b1c7b96547 fix(telegram): bound the 3 sibling updater.stop() calls with the same CLOSE-WAIT timeout
The salvaged fix (#58272) guarded the primary network-error reconnect path.
Issue #58270's Scope section names three more unguarded await updater.stop()
sites that can hang identically on a CLOSE-WAIT socket:

- conflict handler (before the retry back-off sleep)
- conflict-retries-exhausted teardown (before the fatal notify)
- disconnect() teardown (would hang gateway shutdown/restart)

Each is now wrapped in asyncio.wait_for(..., _UPDATER_STOP_TIMEOUT) with a
warning on timeout, matching the primary path, so no reconnect/teardown ladder
can wedge on a dead socket.

Also hoist the shared 15.0s bound to a single module constant
_UPDATER_STOP_TIMEOUT (self-documenting + DRY across all 4 sites), and update
the CLOSE-WAIT regression test to patch that constant instead of monkeypatching
asyncio.wait_for process-wide.

Fatal-notify idempotency: bounding the conflict-exhausted teardown stop() adds
an await AFTER _set_fatal_error, which yields the loop and lets a concurrent
retry task (scheduled by an earlier conflict, already suspended past the entry
guard) reach the fatal branch too — double-firing the fatal handler (surfaced
as a Python 3.11 CI failure in test_polling_conflict_becomes_fatal_after_retries).
Snapshot the pre-transition fatal state and only notify on the first transition.
2026-07-04 21:33:50 +05:30
terry197913
8645b34303 fix(telegram): bound updater.stop() with timeout to prevent CLOSE-WAIT reconnect hang
When the TCP connection enters CLOSE-WAIT the PTB polling task is blocked
on epoll on a dead socket and never wakes.  updater.stop() awaits that task
and therefore hangs indefinitely.

Consequence: _polling_error_task stays alive-but-blocked forever; every
subsequent heartbeat probe sees it as "in-flight" and skips triggering a
new reconnect; the gateway silently drops messages for hours until a manual
restart.  Field incident: 11-hour outage on 2026-07-04 UTC despite the
heartbeat loop firing a reconnect at 01:11 — stop() blocked the entire
ladder.

Fix: wrap the updater.stop() call inside asyncio.wait_for(timeout=15).
On TimeoutError log a warning and continue to _drain_polling_connections()
+ start_polling() — same recovery path, just unblocked.

The heartbeat loop (PR #48496) correctly detects the dead socket and fires
_handle_polling_network_error.  This commit is the missing second half:
ensuring the reconnect itself always completes.

Test: test_handle_polling_network_error_updater_stop_timeout() simulates
a hang by making stop() sleep forever and verifies that drain + start_polling
are still reached after the timeout.

Fixes #58270
2026-07-04 21:33:50 +05:30
kshitijk4poor
dba585c179 fix(agent): deduplicate tool_call_id across the pre-API sanitizers (#58327)
Strict providers (DeepSeek) reject a payload where the same tool_call_id
appears more than once with HTTP 400 'Duplicate value for tool_call_id'.
The issue was filed as an 'orphaned tool message' compression bug, but the
pasted error is a DUPLICATE tool_call_id — orphans are already handled on
main; duplicates were not. Reproduced live on main: both shapes leaked
through repair_message_sequence and sanitize_api_messages.

Two chokepoints, two shapes:
- repair_message_sequence: consume the id from known_tool_ids on first
  match so a SECOND tool result reusing it falls into the drop branch
  (duplicate tool-result shape). This is @Robinlovelace's kernel from
  #55436 (applied manually — that PR was ~800 commits stale and bundled
  an unrelated duplicate-DB-write change for #860, which is dropped here).
- sanitize_api_messages (final pre-API pass): add a dedup pass covering
  BOTH (a) duplicate tool_calls sharing an id WITHIN one assistant message
  (the message[6] shape) and (b) later tool result messages reusing an
  already-seen id. #55436 covered neither of these at this chokepoint.

Tests: duplicate-tool-result dedup at both functions, duplicate-assistant-
tool_call-id collapse, and a negative control proving distinct ids are
never dropped (no over-dedup).

Credit: @Robinlovelace (#55436) for the repair_message_sequence dedup kernel.
Closes #58327.
2026-07-04 21:14:37 +05:30
yoma
6e176e4c21 fix(compression): preserve user turn after compaction 2026-07-04 21:04:27 +05:30
srojk34
2d3eac5fbd fix(moa): apply prompt-caching decoration to the aggregator's one-shot synthesis call
22c5048d9 restored Anthropic-style cache_control for two of MoA's three
call paths: the acting aggregator (MoAChatCompletions.create, the
persistent `provider: moa` model) and the advisor fan-out (_run_reference).
aggregate_moa_context() -- the /moa <prompt> one-shot command's synthesis
call -- is the third, independent call path and was never covered: its
call_llm(task="moa_aggregator", ...) sent a single undecorated user message
containing the full joined reference output, re-billing the entire input on
every invocation even when the resolved aggregator slot is a cache-honoring
route (Claude on OpenRouter/native Anthropic, MiniMax, Qwen/DashScope).

- Generalize _maybe_apply_advisor_cache_control to
  _maybe_apply_moa_cache_control (it never had advisor-specific logic --
  same policy function, same breakpoint layout as the main loop, judged
  purely on the passed-in runtime) and reuse it in aggregate_moa_context
  the same way _run_reference already does.
- Compute _slot_runtime(aggregator) once and reuse it for both the
  decoration call and the call_llm kwargs, instead of calling it twice.

Mutation-verified: reverting the moa_loop.py change makes the new
regression test fail by asserting a plain string aggregator-message
content where the cache-honoring case expects native cache_control
content blocks.
2026-07-04 20:59:02 +05:30
kshitijk4poor
a37fd66dec fix(telegram): shut down abandoned init app + AUTHOR_MAP + cover the deadline helper
Follow-up to @msh01's wall-deadline init-timeout fix.

- Resource leak: on timeout the initialize() task is abandoned without
  awaiting its (shielded, possibly-never-completing) cancellation, so the
  half-built PTB app's httpx client / connection pool was never closed —
  up to 8x across the retry ladder. Add an optional on_abandon cleanup to
  _await_with_thread_deadline that best-effort app.shutdown()s the abandoned
  app, run detached + exception-swallowed so it can never re-block or re-hang
  the ladder (mirrors _close_client_on_timeout in agent/auxiliary_client.py).
- Cover the helper itself: the salvaged test monkeypatched out the real
  _await_with_thread_deadline, so its abandonment/cleanup path was untested.
  Add direct tests for happy-path return, prompt-timeout-with-cleanup, and
  cleanup-error-swallowed; the wedged coroutines swallow cancellation for a
  bounded window (proving the helper returns before cancellation completes,
  the #58236 shielded-scope behavior) without leaving an immortal task that
  would wedge pytest teardown. Widen the salvaged stub to accept on_abandon.
- Attribution: add yingwaizhiying@gmail.com -> msh01 to AUTHOR_MAP (bare
  gmail does not auto-resolve the check-attribution gate).

Known follow-up (not addressed here): the retry ladder reuses the same
self._app across all 8 attempts; a fresh app per attempt would fully close
the coherence risk if an abandoned initialize() completes in the background.
That is a larger restructure of the ~130-line builder+handler setup, left
for a separate change.
2026-07-04 20:45:48 +05:30
yoma
d50aae0e35 fix(telegram): use wall deadline for init timeout 2026-07-04 19:06:44 +05:30
Shashwat Gokhe
86a0c5553e feat: allow suppressing Codex gpt-5.5 autoraise notice 2026-07-04 18:55:27 +05:30
kshitijk4poor
8b24376d63 fix(dashboard): close credential-dir-tree gap + .git-credentials in managed-files guard
Follow-up to @srojk34's basename-denylist widening. Two gaps the
basename-only guard left, both covered by the two canonical guards it
mirrors:

- Directory-tree stores mcp-tokens/ (live MCP OAuth tokens) and pairing/
  are denied as whole trees by gateway.platforms.base._ROOT_CREDENTIAL_DIRS
  and agent.file_safety, but the dashboard files API descends into subdirs,
  so mcp-tokens/<server>.json (non-canonical basename) stayed
  listable/readable/downloadable. Add _is_sensitive_path(), a path-aware
  check that blocks any path with a credential-directory component, and
  route all three call sites (list/read/download) through it.
- Add .git-credentials to the basename set (agent.file_safety blocks it too).
- Correct the docstring: it now says it mirrors the credential-FILE basenames
  of the canonical guards, with the directory trees handled by the new
  path-aware helper (the prior wording overstated parity).

Scope stays on the read/list/download exfil surface (#57505); the write
endpoints (upload/mkdir/delete) are a separate threat and out of scope.

Tests: dir-tree descent blocked (mcp-tokens/pairing per-server files),
.git-credentials blocked, plus a positive control that a benign subdir file
stays browsable. Mutation-checked (neuter _is_sensitive_path -> new tests
fail). 39 web_server_files + fs tests pass, ruff clean.
2026-07-04 17:58:05 +05:30
srojk34
43ec69cef3 security(dashboard): widen managed-files sensitive-filename guard past .env
_is_sensitive_filename() only blocked .env / .env.<suffix>, but the
dashboard Files tab's managed root is operator-configurable and, per the
docker-mount scenario #57505 was filed against, can point directly at
HERMES_HOME — where the canonical credential stores enforced elsewhere
in the codebase (gateway.platforms.base._ROOT_CREDENTIAL_FILES,
agent.file_safety.get_read_block_error) all live: auth.json, OAuth
token stores, webhook HMAC secrets, the Bitwarden disk cache. None of
those basenames were blocked, so the Files tab could still list, read,
and download them. .envrc (direnv) also slipped past the old check
since it doesn't equal ".env" or start with ".env.".

Widen the basename set to mirror both existing guards so the dashboard
doesn't lag behind them.
2026-07-04 16:32:46 +05:30
teknium1
14cbbd541e
Merge remote-tracking branch 'origin/main' into iron-proxy-followups
# Conflicts:
#	hermes_cli/config.py
#	hermes_cli/main.py
#	website/docs/reference/cli-commands.md
2026-07-04 03:09:43 -07:00
teknium1
86fcb2fe5f
feat(egress): first-class x-api-key providers + hot reload via management API
Both wired against features the iron-proxy author (@mslipper) confirmed on
PR #30179 — and both verified present in the pinned v0.39.0 source.

Header-auth providers (match_headers):
- New _HEADER_AUTH_PROVIDERS: Anthropic native (x-api-key), Azure OpenAI
  (api-key on *.openai.azure.com / *.cognitiveservices / *.services.ai),
  Gemini (x-goog-api-key + ?key= query param via match_query).
- TokenMapping grows match_headers + alias_env_names; per-provider header
  sets flow into the secrets rules; mappings.json roundtrips them
  (legacy files load with the Authorization default).
- GEMINI_API_KEY / GOOGLE_API_KEY collapse into ONE mapping (two
  require-rules on the same host would reject each other); the sandbox
  gets the token under both names, and the proxy child env mirrors the
  alias into the canonical name when only the alias is set.
- Docker backend injects alias env names alongside canonical ones.
- The fail-closed tier is now empty, so fail_on_uncovered_providers and
  discover_blocked_providers are deleted (dead toggle otherwise);
  _NON_BEARER_PROVIDERS shrinks to genuinely-unswappable signature auth
  (AWS SigV4, GCP service-account OAuth) — warn-only, as before.

Management API (hot reload):
- Generated proxy.yaml enables the v0.39 management listener: loopback
  only at tunnel_port+2, bearer key from HERMES_IRON_PROXY_MGMT_KEY.
- Key minted at setup (management.token, 0600); start_proxy injects it
  (v0.39 refuses to start when api_key_env is empty).
- hermes egress reload -> POST /v1/reload: re-reads proxy.yaml and
  atomically swaps the pipeline; 422 leaves the running ruleset
  untouched; actionable errors for not-running / pre-management config /
  key mismatch. Secrets changes still require restart (daemon env is
  read at spawn) — the CLI says so.

Validation: 218/218 unit+CLI+docker tests; 3/3 gated live E2E against the
real v0.39.0 binary (Authorization swap, x-api-key swap, live reload with
token rotation on the same pid). Docs updated.
2026-07-04 02:49:31 -07:00
kshitijk4poor
09693cd3a3 fix: complete OAuth-UA salvage follow-up (stale comment + test keychain isolation)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / TypeScript (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Two review findings on the #57922 salvage:

1. Stale inline comment at the login-exchange site still claimed the token
   endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now
   contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT.

2. The inherited Path.home test isolation on the three TestRefreshOauthToken
   tests only stubbed the ~/.claude *file* source, not the macOS Keychain.
   _refresh_oauth_token re-reads read_claude_code_credentials() (keychain
   first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner
   with real Claude Code creds the branch short-circuits and the 3 tests fail.
   Stub read_claude_code_credentials -> None so the tests are hermetic.

(The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken
failures on macOS are the same pre-existing keychain-leak class on origin/main,
unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
2026-07-04 15:16:13 +05:30
Michael Steuer
4c5b4417bb fix(anthropic): OAuth token endpoint UA must not be claude-code/ (login 429, #48534)
hermes auth add anthropic fails 100% at token exchange with HTTP 429 while
Claude Code /login succeeds through the same client_id/redirect/scope. The
discriminator is the User-Agent on the /v1/oauth/token request.

Verified live against platform.claude.com (throwaway code, nothing burned):
  claude-code/2.1.200 (external, cli)  -> 429 rate_limit   (Hermes, blocked)
  Mozilla/5.0                          -> 429 rate_limit
  axios/1.7.9                          -> 400 invalid_grant (reached validation)
  node / empty / SDK-style UAs         -> 400 invalid_grant

Anthropic now rate-limits token-endpoint requests whose UA starts with
claude-code/ (the anti-abuse net for Max-sub-as-API-key). This is the same
prefix-block shape that #48534 first hit on claude-cli/, then #56263 dodged by
switching to claude-code/ — which held ~2 weeks and is now blocked too. Bumping
_CLAUDE_CODE_VERSION_FALLBACK cannot help; the gate is prefix-based.

Fix: shared _OAUTH_TOKEN_USER_AGENT (axios/) on the token endpoint only — the
two refresh POSTs (refresh_anthropic_oauth_pure) and the login exchange POST
(run_hermes_oauth_login_pure). The real Claude Code CLI exchanges the auth code
with a bare axios client, NOT its claude-code/ inference UA.

The INFERENCE client (build_anthropic_kwargs, /v1/messages) is deliberately left
on claude-code/ + x-app: cli — that fingerprint is required there and is NOT
throttled on the messages API. Two endpoints, opposite UA requirements.

Also isolate two _refresh_oauth_token tests from live ~/.claude creds and update
the UA regression tests to assert the split (token endpoint uses a
non-claude-code UA while inference keeps claude-code/).

Verified E2E: Hermes' own login path now returns 400 (past the 429 wall)
instead of 429, using the real _OAUTH_TOKEN_USER_AGENT constant against the live
platform.claude.com token endpoint.

Salvaged from #57922 (authorize-host + scope changes dropped as non-load-bearing;
they only add a redirect hop back to claude.ai and the UA fix alone clears 429).
2026-07-04 15:16:13 +05:30
kshitijk4poor
88f2c0caf6 fix(agent): match tool results on call_id||id in pre-request repair (#58168)
repair_message_sequence Pass 1 registered only tc.get("id") when building
the set of known assistant tool_call ids, then matched tool results against
it by tool_call_id. In the Codex Responses format an assistant tool_call
carries both id (fc_...) and a distinct call_id (call_...); a tool result's
tool_call_id may be keyed on either depending on which builder produced it.
Registering only id made a valid tool result whose tool_call_id matched
call_id look orphaned, so the pass dropped it and left the assistant
tool_call unanswered -- producing HTTP 400 on strict providers (DeepSeek,
Kimi): 'Messages with role tool must be a response to a preceding message
with tool_calls'. Long-running sessions that persisted such a sequence were
permanently broken, re-sending the orphan every turn.

Register both id and call_id for each assistant tool_call so a result
matching either key is recognized, consistent with
AIAgent._get_tool_call_id_static and the compressor's _sanitize_tool_pairs.
Apply the same call_id||id precedence to the corrupted-args sanitizer's
existing-result scan / stub insertion, which had the identical mismatch.

Adds 3 regression tests covering the codex id!=call_id case (match on
call_id, match on only call_id, match on id when both present).
2026-07-04 15:15:33 +05:30
Marxb85
fab6d4d1b7 Fix computer-use crash on X11 windows with null PID
On X11 a window's PID comes from the optional _NET_WM_PID property, so
cua-driver's list_windows legitimately returns pid: null for windows that
don't set it (desktop root, panels, override-redirect popups). capture()
and focus_app() coerced every entry via int(w["pid"]) inside a list
comprehension, so a single null-pid window raised TypeError and aborted
the whole enumeration before any screenshot — capture was impossible on
any X11 desktop with even one such window.

Route both ingestion sites through a new _ingest_windows() helper that
skips entries lacking a usable pid/window_id (uncapturable anyway) and
coerces the rest.

Adds tests/tools/test_computer_use_null_pid_windows.py covering the
helper's filtering/coercion and an end-to-end capture() regression that
reproduced the crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:37:33 -07:00
kshitijk4poor
fc31f14cda fix(cron): disambiguate Telegram forum vs channel DM topics at delivery time (#52060)
The #22773 heuristic classified any telegram:<positive_chat_id>:<numeric_thread_id>
cron target as a Bot API channel Direct-Messages topic and routed it via
direct_messages_topic_id, which nulls message_thread_id. A normal forum-style
topic inside a private chat has the identical shape, so every such cron
delivery landed in General instead of the target thread (#52060). It also
means the only way to address a genuine channel DM topic was this same
ambiguous guess.

Disambiguate with the real runtime signal instead: probe the live adapter's
get_chat_info once and route via direct_messages_topic_id only when the chat
is actually a channel; everything else (private forum topic, forum supergroup,
group) and any probe failure fall back to message_thread_id — parity with
0.16.0 and with live reply routing. This fixes forum-topic delivery AND makes
genuine channel DM-topic cron delivery work correctly.
2026-07-04 14:33:14 +05:30
kshitij
047b48dfdd
Merge pull request #58155 from kshitijk4poor/salvage/pr-28062-codex-max-output
fix: recover Codex max-output truncation + re-baseline mid-turn compaction flush
2026-07-04 14:22:37 +05:30
kshitijk4poor
67df958dbe fix(custom-provider): emit reasoning_effort at the live profile path
PR #57601's original branch added a top-level reasoning_effort emit to the
LEGACY build_kwargs path (agent/transports/chat_completions.py), but
provider=custom resolves to CustomProfile (plugins/model-providers/custom/),
so chat_completion_helpers takes the profile path and returns early — the
added branch was unreachable dead code for every custom endpoint.

Move the fix to its real site, CustomProfile.build_api_kwargs_extras(), and
follow the DeepSeek/Zai profile precedent:
  - disabled            -> extra_body.think = False (unchanged)
  - enabled + effort    -> TOP-LEVEL reasoning_effort (the OpenAI-compatible
                           format GLM-5.2/ARK expect), passed through verbatim
                           incl. max/xhigh
  - enabled + no effort -> omit, so the endpoint's server default applies
                           (avoids silently forcing 'medium' as the original
                           branch did)

Deliberately does NOT force think=True on enable — that flag is Ollama-only
and risks a 400 on GLM/vLLM endpoints that don't recognize it; thinking is
already server-default-on for these backends.

Verified end-to-end through the real profile dispatch (temp HERMES_HOME):
custom+high -> reasoning_effort=high; custom+max -> reasoning_effort=max;
custom+none -> think=False; custom+unset -> nothing; num_ctx composes.

Adds tests/plugins/model_providers/test_custom_profile.py (13 cases).
Addresses the custom-provider half of #55276.

Co-authored-by: huanshan5195 <huanshan5195@users.noreply.github.com>
2026-07-04 14:19:44 +05:30
kshitijk4poor
a1c17edcbb test: update reasoning-effort docstring guards for new 'max' level
Follow-up to salvaged #57601. Adding "max" to VALID_REASONING_EFFORTS
made parse_reasoning_effort("max") valid, so:
- test_unknown_levels_return_none no longer lists "max" (it is now valid;
  auto-covered by test_each_valid_level which iterates the tuple).
- test_known_supported_levels_are_documented and the parse_reasoning_effort
  docstring now include "max" so the doc-sync guard actually protects it.
2026-07-04 14:19:44 +05:30
kshitijk4poor
4651ac64a1 refactor(ssh): extract shared _is_ssh_remote_tilde_cwd predicate
Follow-up to the salvaged SSH-tilde-cwd fix. The predicate
"backend == ssh and (cwd == ~ or cwd.startswith(~/))" was inlined at
each expanduser guard site, which is how the test simulator drifted from
production (it grew an SSH guard on a top-level-alias branch that has no
production counterpart).

- Add tools/terminal_tool._is_ssh_remote_tilde_cwd(backend, cwd) as the
  single source of truth (case/whitespace-tolerant).
- Use it in _get_env_config and the gateway config bridge.
- Test simulator imports the real helper instead of re-implementing the
  predicate; revert the phantom SSH guard on the top-level-alias branch
  (production maps top-level cwd: to a plain env var, not TERMINAL_CWD via
  an SSH-guarded path — that branch tested nothing real).
2026-07-04 14:17:11 +05:30
helix4u
83fb8ec277 fix(ssh): preserve remote tilde cwd 2026-07-04 14:17:11 +05:30
kshitijk4poor
af0ce1cf8e refactor(mcp): DRY the non-interactive OAuth guard + positive-control test
Follow-up to the salvaged #58000 fix.

- Extract _raise_if_non_interactive(lead) so the shared 'hermes mcp login'
  next-step wording lives in one place across both OAuth boundaries
  (_redirect_handler, _wait_for_callback), rather than two copy-pasted
  inline raises. Boundary-specific lead sentences preserved verbatim, so
  existing message-match tests stay green.
- Add a positive-control test asserting the guard does NOT over-fire on the
  interactive path (valid/refreshable tokens keep working), satisfying an
  explicit regression-coverage line from #57836.
2026-07-04 14:10:33 +05:30
xxxigm
0c8441c880 test(mcp): cover non-interactive fail-fast at OAuth callback boundary (#57836)
Add TestNonInteractiveFailFastAtCallbackBoundary: the callback boundary must
reject before binding a listener and without entering the poll loop, the guard
must hold even when a (stale) token file exists on disk, the redirect handler
must not print a URL or open a browser, and both boundaries must point users at
`hermes mcp login`.

Mark the existing timeout test and the SSH-hint redirect tests interactive so
they exercise their intended paths rather than short-circuiting on the new
non-interactive guard.
2026-07-04 14:10:33 +05:30
kshitijk4poor
475dd97263 fix: re-baseline flush cursor after mid-turn pre-API compaction
The salvaged max-output/pressure fix set conversation_history=None after
the new pre-API compaction. That is only correct for legacy session-
rotation. Under the default in-place compaction (compression.in_place:
True), archive_and_compact inserts the compacted rows into the session DB
directly without stamping them with the intrinsic persisted-marker, so a
subsequent flush with conversation_history=None re-appends them — doubling
the active context and retriggering compression (the early-persist
duplicate-row trap).

Use conversation_history_after_compression(agent, messages), matching the
two existing compaction sites (post-response should_compress and the
turn-prologue preflight), which returns None for rotation and
list(messages) for in-place so the compacted dicts are skipped by identity.

Adds a regression test with a real SessionDB + real archive_and_compact
that asserts the compacted summary row is persisted exactly once (fails
with 2 copies on the None variant).
2026-07-04 13:55:26 +05:30
eliteworkstation94-ai
1f430e1aa2 fix: recover Codex max-output truncation 2026-07-04 13:55:26 +05:30
kshitijk4poor
52cf9dbada fix(prompt-caching): align _can_carry_marker with last-part-dict marking
Follow-up to the salvaged #57845 fix. _can_carry_marker used
any(isinstance(part, dict)) but _apply_cache_marker only marks the LAST
content part, so a list whose last element is a non-dict passed the carrier
gate yet received no marker — wasting one of the four breakpoints. Tighten
the predicate to require content[-1] to be a dict (mirroring the apply
logic) and add a regression test. Flagged by a 3-agent review.
2026-07-04 13:54:27 +05:30
Lavya Tandel
8b797f7a7b fix(prompt-caching): skip invalid top-level cache_control on empty assistant/tool messages on OpenRouter
- role:tool no longer gets top-level cache_control on OpenRouter
- empty/None assistant turns skip useless marker
- non-empty tool content wrapped so marker lands on a content part
- preserves native Anthropic behavior
2026-07-04 13:54:27 +05:30
helix4u
7a648a8bff fix(telegram): paginate model provider picker 2026-07-04 13:48:33 +05:30
Teknium
19d4174454
feat(gateway): add /sessions search <query> (#57685)
Gateway users can now search resumable sessions from messaging surfaces:
/sessions search <query> (alias: find) matches titles and session ids —
including every title/id in a row's forward compression chain, so a
compressed-away title still surfaces its live tip — plus a
punctuation-normalized variant so 'an94' matches 'AN-94'.

Implemented by generalizing the existing id_query chain-filter in
SessionDB.list_sessions_rich into a combined SQL-level filter (search
stays ORDER BY last-active + LIMIT at SQL level), threading a
search_query through the shared query_session_listing helper, and
teaching parse_session_listing_args to split off a search query.

Search results pass through the existing _resume_row_visible guard
unchanged: origin scoping, admin-only 'all', and the fail-closed
legacy-row posture from the July 1 hardening are preserved exactly.
Over-fetch (50) before the visibility cut so origin-invisible matches
can't starve the page.

Salvages the feature direction of PR #57595 by @GodsBoy with a minimal
implementation that keeps the resume authorization surface untouched.
2026-07-03 13:44:00 -07:00
brooklyn!
20c83af664
Merge pull request #57590 from NousResearch/bb/skills-renovate
Capabilities page (Skills/Tools/MCP + Hub) + responsive overlay nav & mobile polish (desktop)
2026-07-03 15:28:44 -05:00
Brooklyn Nicholson
914d19b3a9 fix(desktop,gateway,mcp): post-merge — CI contract, review corrections, hub search
Post-merge follow-ups + several review rounds + a hub-search rework, folded together.

Merge-scuff restores (a stale-base refactor had reverted two live-on-main fixes):
- gateway: SessionStore compression-tip healing + its regression test.
- desktop: messaging session/transcript polling in desktop-controller
  (MESSAGING_POLL / ACTIVE_MESSAGING_SESSION_POLL, refreshMessagingSessions,
  refreshActiveMessagingTranscript, the richer sameCronSignature) so inbound
  platform traffic updates live again instead of freezing until manual refresh.

Profile-switch isolation (epoch/close/guard on every profile-scoped async):
- Hub store clears + in-flight runHubAction bails (and swallows the post-switch
  404 instead of a phantom toast); hub preview/scan/search/sources profile-scoped.
- MCP: probe/auth epoch guards, dirty-draft reset, sidebar mutations blocked
  until config resettles AND every persist re-checks the epoch post-await;
  profilePending clears on config settle incl. error; logs re-key on profile.
- Model settings reload on switch and epoch-guard setModelAssignment /
  saveMoaModels / API-key activation.
- Config draft resets + cancels its autosave on switch; skill editor/archive and
  star-map node dialogs close on switch; openSkillEditor / star-map openEdit
  discard stale fetches; tool-usage analytics loads are profile-guarded/keyed.

Correctness + UX:
- Unique per-skill action names for hub install AND uninstall; hub/​catalog rows
  flip only on a clean exit_code; catalog install polls the background bootstrap
  to completion, reconciles the mcp.json draft (no dropped server), and fails
  loudly on non-zero exit; MCP catalog query keyed by profile.
- /test reports needs-auth for anonymous auth:oauth servers; /auth snapshots +
  restores tokens on a failed re-auth and clears the full 300s callback window.
- config-settings shows a retry on load failure; CodeEditor/JsonDocumentEditor
  go read-only while saving so edits typed mid-save aren't dropped.
- Deep-link highlighter deletes its param only after a successful scroll.
- Restored the PageSearchShell trailing slot → Artifacts refresh button/spinner.
- /settings?tab=mcp redirect keeps server=.

Progressive hub search: fan out one query per backend-searchable source
(index-covered API sources stay unsearchable → no ~70-call GitHub re-hammer),
merge/dedupe by trust as each lands, per-source spinner overlaid on the dimmed
chip — results stream in without blocking on the slowest, no layout shift.

test(web): /api/skills list carries usage + provenance (CI contract).
2026-07-03 15:22:43 -05:00