Commit graph

2228 commits

Author SHA1 Message Date
kshitijk4poor
86e603e7d6 fix(compression): verify cached prompt embeds current memory before retaining
The salvaged retention check compared the built-in memory snapshot
before vs after the disk reload. That holds for a long-lived CLI agent,
but on fresh-agent surfaces (gateway per-turn agents, TUI) the cached
prompt is restored from the session DB and can predate mid-session
memory writes that the fresh MemoryStore already absorbed at init: the
snapshot is then identical on both sides of the reload while the prompt
itself is stale, so compression would retain (and re-persist via
update_system_prompt) a prompt missing the new memory for the life of
the session.

Replace the equality check with a containment check
(_cached_prompt_reflects_builtin_memory): retain the cached prompt only
when the freshly-reloaded rendered blocks appear verbatim inside it,
and rebuild when a leftover block header remains for a target whose
entries have since been emptied or disabled. Block headers are shared
via MEMORY_BLOCK_HEADERS in tools/memory_tool.py so the check stays in
lockstep with MemoryStore._render_block.

Adds regression guards for the gateway stale-restore path and the
emptied-memory leftover-block path; verified with a real-MemoryStore
E2E matrix (9 scenarios) against a temp HERMES_HOME.
2026-07-20 13:13:02 +05:30
YAMAGUCHI Seiji
5befa15aba feat: configure X Search reasoning effort 2026-07-19 23:58:33 -07:00
Teknium
9b428ddd08
feat(x_search): default model grok-4.20-reasoning -> grok-4.5 (#67719)
grok-4.5 is xAI's newest release (their versioning is non-monotonic:
4.5 > 4.20) and is the model xAI's own docs use for the server-side
x_search tool. Users who explicitly pinned x_search.model keep their
choice; everyone else picks up the new default via the config
deep-merge — no _config_version bump needed.

- tools/x_search_tool.py: DEFAULT_X_SEARCH_MODEL
- hermes_cli/config.py: DEFAULT_CONFIG x_search.model + comment
- agent/reasoning_timeouts.py: 300s stale-timeout floor entry for
  grok-4.5 (grok-4.20-reasoning entry kept for pinned users)
- docs: x-search.md en + zh-Hans (config sample + troubleshooting)
- tests: default-model assertion + timeout-floor positive case
2026-07-19 16:32:20 -07:00
Teknium
299e409f15
feat(delegation): live-viewable subagent transcripts — tail your subagents while they work (#67479)
* feat(delegation): live-viewable subagent transcripts for delegate_task

Each child now streams an append-only, human-readable log to
<hermes_home>/cache/delegation/live/<delegation_id>/task-<n>.log while it
runs, and the dispatch return includes the paths so the caller can tail
them immediately instead of waiting blind for the consolidated summary.

- New tools/delegation_live_log.py: LiveTranscriptWriter (per-event append
  + flush, one-line rendering with truncation, never raises into the agent
  loop), wrap_progress_callback (tees the child's existing
  tool_progress_callback events into the log, preserves the _flush
  contract), dispatch-time creation with pre-headered files so tail -f
  attaches immediately, manifest.json (goals/task count/per-task status),
  and 7-day retention pruning on new dispatches.
- delegate_task: wraps each child's progress callback with the writer;
  sync results and background dispatch responses gain live_transcripts
  (+ hint field on dispatch); per-task result entries carry
  live_transcript; transcripts finalized with exit-reason markers.
- async_delegation: dispatch_async_delegation_batch accepts an optional
  delegation_id so the live/ dir name matches the returned handle; the
  completion event carries live_transcripts.
- process_registry: consolidated batch-completion block references each
  task's live transcript path.
- Tool schema description documents the live_transcripts return surface;
  docs gain a 'Live Transcripts' section with a tail -f example.

Placement under cache/delegation means the logs are mounted read-only
into remote terminal backends for free. Side-channel only: zero changes
to message content, so prompt caching is unaffected. Transcript-OUT only
— no overlap with the subagent control surfaces of PR #66046.

* fix(delegation): label the kickoff transcript line as user — it is the child's one user message
2026-07-19 10:29:14 -07:00
Jupiter363
3df8bd3478 fix(mcp): propagate stored timeouts from completed futures 2026-07-18 18:40:34 -04:00
Teknium
7a43ab042f
fix(computer_use): reconnect a dead cua-driver session instead of hanging (#67138)
Bug 1 of #55048: when the MCP connection dropped (driver crash / restart),
_lifecycle_coro exited but left _started=True, so the next list_apps/capture
passed _require_started() and then operated on a None session — hanging
forever instead of reconnecting.

- _lifecycle_coro's finally now resets _started=False on ANY exit, so a dead
  session is re-enterable (idempotent no-op on the normal stop() path; atomic
  bool write, safe from the bridge-loop thread without the lock stop() holds).
- call_tool() re-enters start() when the session isn't active, rebuilding it
  before the call. The start_session/end_session handshake (driven by start()/
  stop() themselves) is exempted so bootstrap doesn't recurse.

Tests: two cases in test_computer_use_delivery_ladder.py — finally resets
_started, and call_tool restarts a dead session exactly once. Full
computer_use suite green (233).

Refs #55048 (Bug 1). Bug 2 (expose foreground dispatch) is covered by the
delivery_mode work in #67123.
2026-07-18 15:07:04 -07:00
Teknium
9d6d772837
feat(computer_use): follow cua-driver's verify → escalate ladder (#67123)
Hermes' computer_use wrapper dropped cua-driver's structured action verdicts,
exposed no delivery_mode, and injected background-only guidance — so the agent
reported unverified no-ops as success and concluded cua-driver 'cannot drive'
Electron/Chromium surfaces (observed live on tldraw offline). Fixes #67052.

Phase A — preserve the result contract:
- ActionResult carries verified/effect/escalation/path/degraded/code/delivery_mode
- CuaDriverBackend._action() reads structuredContent (was data-only); a helper
  normalizes it, additive and None-safe on old drivers
- _text_response surfaces the fields additively (ok stays transport-only)

Phase B — bounded, model-reachable foreground:
- delivery_mode (background|foreground) + bring_to_front on the schema, dispatcher,
  ABC, and all input methods
- foreground is capability-gated (input.delivery_mode); old drivers get a
  structured foreground_unsupported refusal, never a silent background downgrade
- no automatic/hidden foreground retry — the model selects it from the signal

Phase C — guidance + isolation:
- system prompt (prompt_builder) and bundled skills/computer-use/SKILL.md go from
  background-ONLY to background-FIRST, teaching the AX→PX→foreground ladder driven
  by returned effect/escalation, not predicted from the app being Electron
- foreground approval scoped by (action, delivery_mode): a background approval
  never silently authorizes foreground
- approval state keyed per session_id so concurrent gateway runs don't leak unlocks

Tests: tests/tools/test_computer_use_delivery_ladder.py (15) cover confirmed/
unverifiable/suspected_noop/degraded/old-driver verdicts, delivery_mode gating +
foreground_unsupported, and session-scoped foreground approval. Existing 265
computer_use tests still green.

Live E2E (real cua-driver 0.8.3 + tldraw offline on Linux/X11): a background click
returned effect='unverifiable'/path='ax' (no fabricated success), and a foreground
request returned code='foreground_unsupported' — correct on a driver that predates
the input.delivery_mode capability.
2026-07-18 13:59:35 -07:00
Vishnu
29899c2aa9 Fix headed browser sessions being killed after every turn
The per-turn `_cleanup_task_resources` unconditionally calls
`cleanup_browser`, closing the browser window immediately after each
bot reply. This makes headed mode (`AGENT_BROWSER_HEADED=1`) unusable
— the window flashes up and disappears on every response.

This mirrors the existing VM persistence pattern: skip per-turn
cleanup when headed mode is active and let the inactivity reaper
handle idle sessions instead. Full-session teardown on gateway
shutdown remains unconditional.

Also adds `browser.headed` config.yaml support and passes `--headed`
to agent-browser in local mode when configured, so users don't need
to rely solely on the `AGENT_BROWSER_HEADED` env var.

Closes #11020 (lead bug)
2026-07-18 10:07:46 -07:00
19404
6236412294 fix(curator): guard background review writes against manually authored skills
Prevents the curator's LLM consolidation pass from archiving skills the
user placed manually (e.g. via URL install, direct SKILL.md authoring, or
Gitee source). These skills carry created_by=None in .usage.json rather
than created_by=agent, but the _background_review_write_guard only checked
pinned, external, bundled, hub, and protected built-in status — missing
the manual-skill case entirely.

The guard already caught a real case: the user's 'auto-dev' skill
(use_count=50, patch_count=119) was archived 28 seconds after its last
use during a curator auto-run.

Adds a check: if the skill has a usage record and its created_by is not
'agent', refuse the background curator write. Skills with no record at
all (new/unknown) are not blocked.
2026-07-18 20:17:22 +05:30
Bruce-anle
3d031bdb29 fix(mcp): use direct parent identity in stdio watchdog
Use the direct POSIX parent relationship instead of process creation time and pid_exists checks. Remove the dead create-time argument chain while preserving process-group cleanup and signal forwarding.\n\nRefs #62505
2026-07-18 04:15:13 -07:00
Mason Tanguay
1aadb02eaf fix(delegation): stop mixed platform bundles from re-exposing blocked tools to leaf children
A leaf subagent is meant to be denied delegate_task, execute_code, memory,
clarify, cronjob, and send_message. _strip_blocked_tools() only drops a
toolset when EVERY tool in it is blocked, so mixed platform bundles
(hermes-cli, hermes-telegram, and every other gateway bundle) survived
stripping and re-exposed the blocked tools after composite expansion. A
leaf child spawned from any gateway platform could recursively delegate,
run code, and write memory.

Pass exact one-tool deny toolsets into the child's disabled_toolsets so
model_tools subtracts the blocked names AFTER composite expansion, and the
restriction survives later registry/MCP refreshes. Orchestrators regain
only delegate_task.

Salvaged from #66036 by Mason Tanguay (@DictatorBacon); scoped to the
authority fix + its regressions (docs/interrupt changes dropped).

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-07-18 01:30:17 -07:00
Clifford Garwood
3d9be27895 fix(delegate): declare stateless channel in one-shot and cron so delegate_task returns results
run_agent._dispatch_delegate_task forces background=True for every top-level
delegation, and async_delivery_supported() returns True for any session that
never binds the capability. On runners that cannot receive a completion after
their turn ends, that combination silently discards every subagent result: the
model gets a dispatch handle, ends its turn, and reports 'waiting for results'.

Two such runners never bind the capability:

* hermes -z (one-shot) prints one final response and exits. It bypasses cli.py,
  so nothing drains process_registry.completion_queue (only the interactive
  process_loop and the gateway watchers do).

* cron run_job clears the HERMES_SESSION_* routing keys, so a completion event
  carries session_key="" — _enrich_async_delegation_routing cannot resolve it
  and _inject_watch_notification drops it ("no routing metadata"). By then
  run_job has already shipped the job's final response via _deliver_result;
  there is no turn left to re-enter. Worse, get_current_session_key() can fall
  back to the ambient os.environ HERMES_SESSION_KEY, so a cron subagent's output
  can be routed into an unrelated user chat rather than merely dropped.

Add declare_stateless_channel() and bind it in both runners, routing
delegate_task to its existing inline/synchronous path — the same fallback the
stateless HTTP adapter already relies on, and the fix suggested in #63142. The
helper binds only the capability: set_session_vars() would also latch
_session_context_engaged, which a pure single-process one-shot must not trigger.

Also correct two agent-facing strings that hardcoded 'stateless HTTP API' as the
only channel without async delivery (delegate_tool, terminal_tool); they now name
the actual condition.

Repro (before): hermes -z 'Use delegate_task to spawn a subagent that replies
BANANA. Report its reply.' -> "Waiting for the subagent's response...", exit 0,
no BANANA. After: BANANA is returned in-turn.

Fixes #53027
Fixes #63142
2026-07-18 00:05:25 -07:00
teknium1
b9267b50ea docs(delegation): fix stale internal batch-lifecycle comments
Two internal comments in delegate_tool.py still described the superseded
"N independent handles, no combined wait" model, contradicting the
authoritative batch contract (one async unit, one consolidated result
when all children finish). Aligns the comments with the runtime path in
_execute_and_aggregate / dispatch_async_delegation_batch.
2026-07-17 15:55:49 -07:00
Gille
35c578177b docs(delegation): align guidance with current contract 2026-07-17 15:55:49 -07:00
Gille
f29c28d6d0 docs(kanban): clarify unblock status routing 2026-07-17 15:47:39 -07:00
Dusk1e
9a7a43b5d6 fix(tools/kanban): sync kanban_unblock response status with DB state 2026-07-17 15:47:39 -07:00
Teknium
71252f0dcb
fix(terminal): fall back when the configured cwd is unenterable, not just missing (#66306)
A root-launched CLI session can leak /root into the terminal cwd state a
non-root gateway/cron process later resolves (#65583). os.path.isdir('/root')
is True for a non-root user — stat only needs search permission on / — so
_resolve_safe_cwd returned it and subprocess.Popen(cwd='/root') died with
PermissionError: [Errno 13], failing EVERY cron job's terminal/file/search
tool on every command until restart.

_resolve_safe_cwd now requires X_OK (new _cwd_usable helper) and climbs to
the nearest enterable ancestor, logging a WARNING that names the leak class
when an existing-but-denied cwd is skipped. Missing-cwd recovery (#17558)
behavior unchanged.

E2E-verified: LocalEnvironment constructed with an unenterable cwd now runs
commands from the fallback directory instead of raising.
2026-07-17 06:54:51 -07:00
Hermes Agent
a8ec41533c fix(mcp): treat non-string nextCursor as end of pagination
Per the MCP spec the cursor is an opaque string; anything else
(including MagicMock auto-attributes in tests) means no more pages.
Fixes test_mcp_tool_session_expired mock-session runaway.
2026-07-17 04:57:12 -07:00
Hermes Agent
6030ca8cea fix(mcp): follow nextCursor pagination in tools/resources/prompts discovery
Port from anomalyco/opencode#35439/#35500: preserve full MCP catalogs
across paginated tools/list responses.

The MCP spec allows servers to paginate tools/list, resources/list, and
prompts/list via an opaque nextCursor token. The Python SDK's
ClientSession.list_* methods fetch exactly one page per call, and hermes
never passed the cursor back — on a paginated server every tool,
resource, and prompt past page 1 was silently invisible to the agent.

Adds _paginate_full_list() (cursor-draining helper with a 50-page
runaway cap and spec-correct opaque-string cursor validation) and
applies it at all three discovery sites: _discover_tools(), the
tools/list_changed refresh handler, and the list_resources/list_prompts
utility handlers. The keepalive probe intentionally keeps its
single-page call (liveness only).

E2E: real stdio MCP server serving 3 pages (2/1/1 tools) — old code
discovered 2 tools, new code discovers all 4.
2026-07-17 04:57:12 -07:00
Teknium
17485cbcd2 fix(cli): sanitize terminal escapes when replaying stored history (/resume recap, /status recap)
Port from openai/codex#31494: user-visible history replay must strip CSI
sequences and control characters. Stored conversation history can carry
raw terminal escapes (pasted content, gateway-origin text, model output
echoing injected tool results). Replaying it via /resume's recap panel or
build_recap (/status on CLI + gateway) wrote those bytes straight to the
terminal — an injected message could clear the screen, retitle the window,
move the cursor, or restyle the recap UI. Rich's Text() does not neutralize
raw escape bytes.

- tools/ansi_strip.py: add sanitize_display_text() — strip_ansi() plus
  bare C0/C1 control removal, preserving \n and \t, normalizing \r to \n
  (adapted to Python from Codex's sanitize_user_text; reuses the existing
  ECMA-48 stripper instead of transcribing their char-walk)
- hermes_cli/cli_agent_setup_mixin.py: sanitize user + assistant text in
  _display_resumed_history() before building the Rich recap panel
- hermes_cli/session_recap.py: sanitize preview lines in build_recap()
  (_truncate choke point) so /status recaps are clean on every platform
- tests: 10 new sanitize_display_text cases (incl. the exact codex#31494
  fixture), recap + resume-display leak assertions
2026-07-17 04:53:38 -07:00
Teknium
b90dbac1d6 fix(approval): unify execution-bearing option detection
Co-authored-by: MorAlekss <mor.aleksandr@yahoo.com>
2026-07-17 04:53:04 -07:00
Teknium
780e098077 fix: widen UTF-8 BOM tolerance to all sibling frontmatter parsers
The previous commit fixes the canonical agent/skill_utils.parse_frontmatter.
Six more modules reimplement the '---' fence check locally and had the
same bug:

- tools/skill_manager_tool.py _validate_frontmatter — rejected BOM'd
  skill_manage create/edit content outright
- tools/skills_hub.py GitHubSource._parse_frontmatter_quick and
  OptionalSkillSource._parse_frontmatter — hub browse/install metadata
- hermes_cli/skills_hub.py — local skill install validation
- gateway/run.py — skill slug discovery for disabled-skill hints
- agent/prompt_builder.py _strip_yaml_frontmatter — BOM'd context files
  (AGENTS.md) leaked raw frontmatter into the system prompt
- tools/blueprints.py _split_frontmatter — str.lstrip() does not strip
  U+FEFF (not whitespace), so the existing lstrip never covered it

Sibling-surface regression tests added.
Bug class also fixed upstream in cline/cline#12218 (found by the weekly
Cline PR scout).
2026-07-17 04:52:02 -07:00
Teknium
cf3ae7c59c fix(mcp): preserve live OAuth state during reauth 2026-07-17 04:50:47 -07:00
Teknium
ebd737f4d9 fix(mcp): close hosted OAuth lifecycle gaps 2026-07-17 04:50:47 -07:00
Teknium
6045529724 fix(mcp): harden hosted OAuth across profiles and clients 2026-07-17 04:50:47 -07:00
Ben Barclay
11eaa77daf fix(mcp): serialize hosted oauth reauthorization 2026-07-17 04:50:47 -07:00
Ben Barclay
b09f1ba770 fix(mcp): reject invalid dashboard oauth callbacks 2026-07-17 04:50:47 -07:00
Ben Barclay
05dea7be04 fix(mcp): complete OAuth through hosted dashboards 2026-07-17 04:50:47 -07:00
Teknium
0f102fa4dc
feat(browser): store full snapshots on truncation; make eval denylist opt-in (#65923)
* feat(browser): store full snapshots on truncation; make eval denylist opt-in

Two harness fixes motivated by BU_Bench results where fixed-verb + lossy
observation cost Hermes heavily vs code-driven browser agents:

1. Snapshot truncation no longer loses content. When a snapshot exceeds
   the 8000-char threshold, the complete accessibility tree is saved to
   cache/web (same truncate-and-store pattern as web_extract) and the
   truncated view / LLM summary includes the file path plus a ready-made
   read_file call. Element refs beyond the cut are recoverable without
   re-snapshotting. Stored copies are force-redacted and capped at 2MB;
   content-hash filenames dedupe repeated snapshots of the same page.

2. The browser_console(expression=...) sensitive-primitive denylist is
   now opt-in via browser.restrict_evaluate (default false). The
   names-based denylist blocked legitimate DOM extraction — any selector
   or expression containing 'fetch', 'cookie', 'input', etc. — which
   crippled the agent's only programmatic page-inspection path. The
   SSRF/private-URL egress guards in _browser_eval are independent of
   this policy and remain always-on. browser.allow_unsafe_evaluate keeps
   its meaning (bypass the denylist) for configs that already set it.

* test: update None-guard test for stored-snapshot pointer in _extract_relevant_content

test_normal_content_returned pinned the exact return value; the summary
now carries a pointer to the stored full snapshot. Assert the summary
passes through and the pointer is present instead.

* feat(browser): align snapshot threshold with web_extract's 15k char budget

SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching
web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract
truncate-and-store paths give the model the same per-page budget.
_truncate_snapshot's default max_chars now follows the constant.
Invariant test added; docs (en+zh) and CLI tip updated.
2026-07-16 23:41:26 -07:00
cresslank
34837597d2 fix(auth): make xAI OAuth pools multi-account resilient
Keep each xAI OAuth auth-add login as an independent manual device-code pool entry and recognize xAI personal-team spending-limit 403 responses as billing exhaustion. Preserve the structured top-level error message so the failed credential is quarantined and the next healthy account is selected without attempting a pointless token refresh.

Route direct xAI HTTP consumers through the credential pool as well. Proactive and 401-reactive refreshes update the exact issuing manual entry, preserve validated xAI base URL overrides, and serialize single-use refresh-token rotation across concurrent pool instances.
2026-07-17 11:37:25 +05:30
Gille
1f7d2be22f
fix(install): detect Git Bash Mandatory ASLR failures (#64651) 2026-07-16 22:46:46 -04:00
Erosika
29e0471708 fix(honcho): update SDK and restore CI coverage 2026-07-16 12:48:48 -07:00
Teknium
23526148d1 fix(terminal): bridge terminal.backend config in serve/desktop processes lacking a launcher env bridge
terminal_tool reads all settings from TERMINAL_* env vars, bridged from
config.yaml by the CLI, gateway, and TUI-PTY launchers. Processes that
skip every launcher bridge — hermes serve / the Desktop app backend's
in-process agents, the desktop cron ticker — saw an unset TERMINAL_ENV
and silently ran every command on the host even when config.yaml selects
terminal.backend: docker. A user who configured Docker isolation got
unsandboxed host execution with no warning.

Two layers:
- _ensure_terminal_env_bridged() in _get_env_config(): when TERMINAL_ENV
  is unset, backfill TERMINAL_* from config.yaml via
  apply_terminal_config_to_env(override=False). Explicit env always wins
  (honor explicit choice; only fix the accidental fallback). One-shot,
  fail-open to the historical local default.
- cmd_dashboard/serve: run the same bridge at startup so every consumer
  in the backend process (in-process agents, desktop cron ticker,
  tui_gateway cwd resolution) sees the bridged env directly.

Fixes #63141, #54449, #61115, #65696.
2026-07-16 08:55:10 -07:00
Tim Roth
58010c8b3d fix(mcp): reuse cached oauth redirect port 2026-07-16 07:56:46 -07:00
Teknium
c2e11bf418 fix(kanban): guard kanban_attach_url against SSRF via tools.url_safety
_download_url_with_cap called urlopen() after only a scheme check, so a
model-controlled URL could reach loopback services, RFC1918/CGNAT hosts,
or cloud metadata endpoints (169.254.169.254), and a public host could
302 to any of those unvalidated.

Route the fetch through the repo's canonical SSRF guard instead:
validate every hop with tools.url_safety.is_safe_url() and follow
redirects manually (httpx, follow_redirects=False, 5-hop limit) so each
Location target is re-checked before it is fetched — the same pattern
as tools/skills_hub._guarded_http_get. The streaming size cap is
unchanged. Local-fixture tests opt in via HERMES_ALLOW_PRIVATE_URLS
(the guard's documented escape hatch); new tests pin rejection of
loopback, cloud-metadata, and private-range URLs, a mocked
public→loopback redirect, and a mocked public happy path.
2026-07-16 07:33:14 -07:00
Teknium
f3cbe45605 refactor(kanban): unify attachment size cap on KANBAN_ATTACHMENT_MAX_BYTES
The salvaged attachment-toolset commit predated main centralizing the
25 MB cap as kanban_db.KANBAN_ATTACHMENT_MAX_BYTES and re-introduced a
private _MAX_ATTACHMENT_BYTES alias. Drop the duplicate: kanban_db's
store_attachment_bytes(), the dashboard upload endpoint, and the
kanban_attach_url tool all reference the one shared constant now, and
the tests monkeypatch that same name.
2026-07-16 07:33:14 -07:00
otsune
3fccd698fd feat(kanban): attachment toolset + CLI to match the dashboard surface
The kanban board has had full attachment storage and a dashboard HTTP
API (upload/list/download/delete) since #35338, but there was no agent
toolset tool and no `hermes kanban` CLI verb for attachments. Agents and
scripts that don't go through the dashboard server (or can't touch the DB
directly) had no way to create or read real attachments — only links in
comments.

Close that gap by mirroring the existing comment surface:

- `kanban_db.store_attachment_bytes()` — one shared write path (validate
  name, enforce the 25 MB cap, write the blob under the per-task dir with
  collision-free naming, insert the metadata row, clean up an orphan blob
  if the insert fails). `_MAX_ATTACHMENT_BYTES`, `_safe_attachment_name`,
  and a new `_collision_free_path` move here so the dashboard, the tool,
  and the CLI all share one implementation and can't drift.
- Tools (`tools/kanban_tools.py`): `kanban_attach` (inline base64),
  `kanban_attach_url` (server-side http/https fetch with the same cap),
  `kanban_attachments` (list). Write tools respect worker task-ownership;
  list is read-only. Registered in the `kanban` toolset.
- CLI (`hermes_cli/kanban.py`): `attach <id> <path>`, `attachments <id>`,
  `attach-rm <attachment_id>`.
- Dashboard `upload_task_attachment` now imports the shared helpers and
  uses `_collision_free_path` — behavior identical (still streams to disk
  with the cap, still 413 on overflow).
- Docs (AGENTS.md, kanban-worker skill) and toolset membership updated.

Tests: tool round-trip + oversize + bad base64 + ownership; attach_url
against a local HTTP fixture incl. oversize-mid-stream and non-http
scheme rejection; CLI attach/attachments/attach-rm; shared-helper unit
tests; dashboard parity preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 07:33:14 -07:00
Teknium
a6d9d1d2cf fix(security): widen non-ASCII compare_digest crash fix to all sibling sites
Same bug class as the salvaged #65305/#65307: hmac.compare_digest (and
secrets.compare_digest) raise TypeError when given a str containing
non-ASCII characters, and these call sites feed it raw request input.
Compare as UTF-8 bytes everywhere:

- gateway/platforms/msgraph_webhook.py: clientState from request body
- gateway/platforms/whatsapp_cloud.py: hub.verify_token query param +
  X-Hub-Signature-256 header (comment claimed 'works on str' — it
  doesn't for non-ASCII)
- plugins/platforms/feishu: verification token + x-lark-signature
- plugins/platforms/raft: bridge token header
- plugins/platforms/line: X-Line-Signature
- plugins/platforms/sms: X-Twilio-Signature
- tools/code_execution_tool.py: sandbox RPC token (both loops)

Regression tests for the two gateway-core sites (msgraph, whatsapp).
2026-07-16 07:22:24 -07:00
teknium1
d34cc4093a fix(mcp): per-flow callback waiters so concurrent OAuth flows cannot cross ports
_wait_for_callback still read the legacy module-level _oauth_port, so
with two concurrent OAuth flows, flow A's callback wait bound flow B's
port while A's redirect URI pointed at A's port — the callback-side
half of the cross-flow collision that #65622 fixed on the redirect
side. _make_callback_waiter(port) closes over each flow's resolved
port; both provider construction sites (build_oauth_auth and
MCPOAuthManager._build_provider) now wire per-flow waiters. The legacy
_wait_for_callback delegates for backwards compatibility.

Direction credit to @LeonSGP43 (#34280) and the #34260 analysis.
2026-07-16 07:11:21 -07:00
doxe0x
454d553d34 fix(mcp): report a clear error when the OAuth callback port is in use
_wait_for_callback catches OSError on bind with a comment claiming the port is
held by a server build_oauth_auth started, and promising to fall back to polling
it. build_oauth_auth never starts a callback server (this is the only listener),
so there is nothing to poll: the branch just raised a misleading "OAuth callback
timed out" when the real cause is a busy port (a concurrent login, a leftover
listener, or a fixed oauth.redirect_port that collided).

Fix the stale comment and raise an accurate, actionable message (names the port,
suggests freeing it or setting a free oauth.redirect_port), chained from the
original OSError. Behavior is otherwise unchanged. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 07:11:21 -07:00
Peter Skaronis
dc419d6e80 mcp_oauth: configurable redirect_host (WAF-safe localhost redirect URIs)
Reclaim.ai's AWS API Gateway WAF 403s any /oauth2/authorize request whose
query string contains a literal 127.0.0.1, so the SDK's hardcoded
redirect_uri made the browser flow impossible. New optional oauth config
key redirect_host (default 127.0.0.1, unchanged behavior) lets a server
entry use localhost instead.

Integrated into _resolve_redirect_uri so it composes with redirect_uri:
an explicit redirect_uri wins; redirect_host only rewrites the loopback
default's hostname.
2026-07-16 06:14:34 -07:00
Florian Burka
d0afcb125c test(mcp-oauth): cover configurable redirect_uri + fix misleading SSH hint
The PR added a configurable `redirect_uri` (proxy/Funnel callbacks) but
shipped without tests, and the loopback SSH-tunnel hint stayed hardcoded —
actively misleading the exact proxy user the feature targets.

- Extract `_resolve_redirect_uri(cfg, port)` so the client-metadata and
  pre-registration paths derive an identical callback (a mismatch makes the
  authorization server reject the redirect).
- Make `_redirect_handler` redirect_uri-aware: a configured proxy callback
  reaches this machine on its own, so it no longer prints the `ssh -N -L`
  loopback guidance. Wired via `functools.partial` — no new global state.
- Document `redirect_uri` in the config block.
- 14 new tests (red/green TDD): helper resolution + empty-string fallback,
  metadata + pre-registration for configured/default, AnyUrl normalization,
  no-client_id skip, client_secret combo, and both SSH-hint branches.

ruff clean · 81 passed (tests/tools/test_mcp_oauth.py) · ty baseline unchanged

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 06:14:34 -07:00
Florian Burka
6297634d22 fix(mcp-oauth): allow configurable redirect_uri for MCP OAuth flows 2026-07-16 06:14:34 -07:00
liuhao1024
6160a80253 fix(gateway/platforms): migrate all Weixin fallbacks to get_secret() for consistent profile-scoped resolution
Per egilewski's security review, WEIXIN_BASE_URL and WEIXIN_CDN_BASE_URL
were still resolved from process-global environment variables, leaving
mixed-scope bypasses in multiplex mode.

Changed files:
- gateway/platforms/weixin.py: Added get_secret import, replaced os.getenv()
  with get_secret() for WEIXIN_ACCOUNT_ID, WEIXIN_TOKEN, WEIXIN_BASE_URL,
  WEIXIN_CDN_BASE_URL in WeixinAdapter.__init__() and send_weixin_direct()
- tools/send_message_tool.py: Added get_secret import, replaced os.getenv()
  with get_secret() for all WEIXIN_* fallbacks in _handle_send()

All runtime Weixin send paths now resolve both credentials and endpoint
configuration from the same profile-scoped source.
2026-07-16 05:39:58 -07:00
teknium1
95a0f9c836 fix(mcp): close select-to-bind TOCTOU on the OAuth callback port
_find_free_port() closed its probe socket before HTTPServer re-bound
the port minutes later, leaving a window where another process could
steal it (#22161 by @amathxbt). _reserve_callback_port() now keeps the
selected socket bound (bounded FIFO pool) until _wait_for_callback
adopts it via bind_and_activate=False. Also sets allow_reuse_address
BEFORE binding — the cherry-picked #44872 set it after the constructor
had already bound, where it is a no-op.

Also updates the three #57836 non-interactive-guard tests to the
closure-factory API from #44872.
2026-07-16 05:39:52 -07:00
XuefeiLi
f4c7caa70c fix(mcp): remove unreachable dead code after return in _make_redirect_handler 2026-07-16 05:39:52 -07:00
Code-suphub
13e19a9092 fix(mcp): use per-provider closures and allow_reuse_address for OAuth (#44588, #44590)
Two related OAuth fixes:

1. Replace module-level _redirect_handler with _make_redirect_handler()
   closure factory that closes over the resolved port. This prevents
   cross-server state pollution when multiple MCP servers run OAuth
   concurrently (#44588).

2. Set server.allow_reuse_address = True on the ephemeral callback
   HTTPServer so the socket doesn't stay in TIME_WAIT after the flow
   completes. This prevents 'Address already in use' errors on the
   next OAuth flow for the same port (#44590).

Fixes #44588
Fixes #44590
2026-07-16 05:39:52 -07:00
szafranski
d73a6f5ac2 fix(telegram): include duration in standalone sends 2026-07-16 04:40:35 -07:00
nima20002000
193871f1a6 fix(code-exec): expose truncated stdout metadata 2026-07-16 04:25:19 -07:00
Gaurav Saxena
4d6686c18a fix(execute-code): honor session cwd overrides 2026-07-16 04:24:07 -07:00