Commit Graph

1388 Commits

Author SHA1 Message Date
webdevtodayjason fdd45323bf feat(plugins): redaction pattern registry — vendor token formats as plugins
Every new vendor token format has required a core PR appending to
_PREFIX_PATTERNS in agent/redact.py (fw_, retaindb_, hsk-, mem0_, brv_
all landed that way; #58466/#58501 are the latest of the class). This
adds an additive-only registry so provider plugins own their format:

- agent/redact.py: register_redaction_patterns(patterns, source) —
  validates each pattern (must compile; must start with >=2 literal
  characters so the pre-screen substring gate keeps working and
  redact-everything patterns like `.*` are structurally impossible),
  dedupes against built-ins and prior registrations, then atomically
  rebuilds _PREFIX_RE and _PREFIX_SUBSTRINGS. Registered patterns get
  identical treatment to built-ins everywhere: same head/tail masking,
  same non-reusable «redacted:label…» sentinel on file_read, same
  security.redact_secrets operator opt-out. Additive-only by design —
  a plugin can extend masking, never weaken it. Includes a
  test/teardown reset helper.
- hermes_cli/plugins.py: PluginContext.register_redaction_patterns()
  delegating with per-plugin attribution; warns and returns 0 on any
  failure so a broken plugin can never break startup.
- Bundled reference plugin `nvapi-redaction` (opt-in): masks NVIDIA
  API keys (nvapi-, used by NIM / build.nvidia.com) — a real format
  missing from core, shipped as the one-liner plugin that previously
  would have been a one-line core PR.

13 new tests: baseline gap, masking + built-ins unaffected, invalid
regex / no-literal-prefix / dedupe / non-string rejection, file_read
sentinel labeling, reset semantics, PluginContext wiring incl.
exception isolation, and a no-mocks end-to-end through the demo
plugin. Existing redaction suites (tests/agent/test_redact.py,
tests/tools/test_kanban_redaction.py) pass untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
2026-08-12 18:55:14 -07:00
Teknium 1e46e09bbd fix(gateway): scope reaction observers to routed profiles 2026-08-12 16:42:28 -07:00
Teknium 9994bc9ec9 fix(gateway): enforce post-auth normalized reaction observer
Builds on Paolo Antinori's #68431 salvage for #64176. Move plugin dispatch behind the profile-scoped runner authorization boundary, fail closed on malformed reaction identities, preserve observer registration across Telegram app rebuilds, and document the deliberately observer-only contract.

Co-authored-by: Paolo Antinori <pantinor@redhat.com>
2026-08-12 16:42:28 -07:00
Paolo Antinori 24e3aa180f fix(plugins): gateway_platform_event error logs include traceback; pin handler groups
- _on_platform_update: log the normalize and auth errors with exc_info=True so
  a regression that silently drops reactions leaves a traceback, not just a
  one-line message (matches the intake auth fallback's exc_info usage).
- TestRegisterHandlers: also assert five core handlers land in the default
  group and only the observer is in group 99.

DoD: hook + auth tests green (34 passed). For a log-line + assertion change
the substantive gate is the test run; /simplify and /code-review were applied
proportionately.
2026-08-12 16:42:28 -07:00
Paolo Antinori c0a4535a26 fix(plugins): address #64176 review on gateway_platform_event (#68431)
Response to teknium1's hermes-sweeper review (keep_open, salvageability=medium).

1. Post-auth gate. The group-99 catch-all fired gateway_platform_event before
   the authorization boundary. Extract _is_source_authorized(source) from
   _is_user_authorized_from_message and add _source_from_reaction_for_auth;
   reactions whose actor the intake would reject no longer reach plugins.
   Fails closed if source extraction raises, so a future non-reaction event
   type cannot silently bypass auth before its own extraction is wired.

2. Shared registration. Extract _register_handlers(app) from connect() so the
   gateway_platform_event observer (group 99) is re-registered alongside the
   core handlers on any rebuild path.

3. Trim inert hook surface. Drop the three reserved gateway_* names from
   VALID_HOOKS (keep only gateway_platform_event). The others land with their
   real contracts and fire-sites when #64231 is finalized.

Tests: unauthorized/authorized/open reaction gating, fail-closed for a future
non-reaction event type, and _register_handlers re-registration.

Ran /simplify and /code-review (high) before pushing.
2026-08-12 16:42:28 -07:00
Paolo Antinori 929be4d1aa feat(plugins): gateway_platform_event observer hook (normalized envelopes)
First slice of #64176's observer-hook half — a normalized-envelope inbound
event hook, replacing raw-SDK handler args with a stable contract (per #64176's
"normalized versioned envelopes only; raw SDK gated behind a capability" rule).

- VALID_HOOKS: register the four gateway_* names from #64176
  (gateway_platform_event fires today; gateway_session_titled /
  gateway_message_delivered / gateway_thread_created reserved pending #64176's
  fire-sites).
- BasePlatformAdapter._fire_gateway_hook: reusable, has_hook-guarded,
  per-call-isolated fire helper (the no-subscriber common case short-circuits).
- TelegramAdapter: a group-99 catch-all TypeHandler normalizes inbound updates
  into gateway_platform_event envelopes. message_reaction -> {platform,
  event_type:"reaction", payload{emojis, custom_emoji_ids, chat_id, message_id,
  thread_id}} (custom-emoji reactions captured via custom_emoji_id; standard via
  .emoji — no None in consumer-facing lists). Other update types return None
  pending #64176's taxonomy (#64231). Normalization is wrapped so a malformed
  update can't raise into PTB dispatch.

Observer-only — zero behavioral change to core dispatch. Supersedes the raw
inbound half of #62584 (telegram:update -> normalized gateway_platform_event).

Tests: VALID_HOOKS registration; _fire_gateway_hook routing/has_hook/isolation;
_normalize for standard, custom-emoji, and mixed reactions + non-reaction;
_on_platform_update firing + normalize-error isolation.

Ran code-review (high) + simplify before pushing.
2026-08-12 16:42:28 -07:00
Teknium 7a5062fbcd feat(plugins): add runtime-backed plugin Doctor
Validate plugin manifests, imports, hook signatures, and runtime registrations through the real plugin loader in an isolated temporary home.
2026-08-12 16:27:07 -07:00
峯岸 亮 1636206ff0 feat: add Plugin Doctor plugin 2026-08-12 16:27:07 -07:00
kshitij d8e3b4f516 fix: map Hermes reasoning efforts onto K3's low/high/max vocabulary
K3 only recognizes low/high/max. Previously the Kimi provider only
forwarded low/medium/high verbatim and dropped every other level
(xhigh/max/ultra/minimal) to the thinking toggle, silently ignoring
the user's requested effort.

Now maps the full Hermes vocabulary onto K3's set, matching K3's own
server-side mapping:
  low, minimal       → low
  medium, high       → high
  xhigh, max, ultra  → max

ref: https://www.kimi.com/code/docs/en/kimi-code/models.html
2026-08-13 01:05:49 +05:30
kshitij c0106e50e7 fix(kimi): send Hermes attribution headers instead of claude-code/0.1.0
The Kimi team noticed that traffic from Hermes Coding Plan users
identifies itself as Claude (User-Agent: claude-code/0.1.0) rather
than the actual client. They asked us to update the UA so they can
properly attribute traffic and understand how their services are
accessed — especially important as they open up to more third-party
agents.

Three code paths were sending wrong/attribution-less headers to Kimi:

1. run_agent.py — _apply_client_headers_for_base_url sent
   {"User-Agent": "claude-code/0.1.0"} for api.kimi.com. Now sends
   the same _AI_GATEWAY_HEADERS set used for Vercel AI Gateway:
   HTTP-Referer + X-Title + HermesAgent/{version} User-Agent.

2. agent/anthropic_adapter.py — the Anthropic Messages path for
   api.kimi.com/coding sent 'claude-code/0.1.0'. Now sends the same
   three-header attribution set.

3. plugins/model-providers/kimi-coding/__init__.py — both kimi and
   kimi_cn profiles sent a static 'hermes-agent/1.0' with no
   HTTP-Referer or X-Title. Now sends the full three-header set with
   a dynamic version, matching the pattern used by the gmi, fireworks,
   xai, and ai-gateway provider profiles.

The attribution header set (HTTP-Referer + X-Title + User-Agent) is
the canonical Hermes pattern used for OpenRouter, Vercel AI Gateway,
Fireworks, and other providers that read these headers for traffic
attribution.
2026-08-11 13:25:37 +05:30
Teknium a98aee47ce fix(kanban): move descendant invalidation to domain layer, make it non-silent
Ancestor-reopen descendant invalidation previously lived only in the
dashboard plugin (_set_status_direct), so board semantics diverged by
surface and the retraction was silent: completed work snapped back to
todo and live workers were killed with no operator-visible signal.

Move it into kanban_db.invalidate_descendants_for_parent_reopen as THE
single domain implementation (recursive-CTE discovery and per-run
_retry_status_for_run handling preserved). It composes under a caller's
open transaction via write_txn(allow_nested=True) — the ancestor flip
and the descendant retractions must commit atomically — and opens its
own transaction standalone. The dashboard shim now delegates; the CLI
deliberately has no done-reopen verb (reopen-review is review-phase
only), so the DB-layer function being the single implementation is the
fix, documented in its docstring.

Non-silent: every invalidated descendant gets a descendant_invalidated
event ({ancestor, prior_status, new_status, resume_status}), the legacy
status event for existing live-feed consumers, and a task comment
naming the reopened ancestor. Running descendants keep the termination
behavior (a child building on a retracted premise is wasted spend), but
the events/comment are committed BEFORE the kill, which routes through
_terminate_reclaimed_worker — the same helper the reclaim paths use.

consecutive_failures resets to 0 on invalidated descendants: operator-
initiated invalidation is a deliberate fresh start, deliberately the
opposite of the review-loop rule (reopen_review_task preserves the
counter, #35072) so the autonomous review loop can't launder its own
failure streak.

Regression: DB-function reopen demotes done descendants with events +
comments; running descendant's audit trail is durable before its worker
dies; counter resets; dashboard and DB paths produce identical task
states, event kinds, and comment counts.
2026-08-10 12:43:46 -07:00
Teknium 1810cfc8dd fix(kanban): guard request_review against live-claim theft
request_review on a running task under a live claim now requires the
caller to prove ownership (expected_run_id, the unchanged worker path)
or pass an explicit force=True override (CLI --force; dashboard human
actions pass force=True) instead of silently clearing claim_lock /
worker_pid of a live run.

Failures now carry distinct diagnostic reasons via with_reason=True
(mirroring request_changes' tuple pattern): live-claim refusal,
malformed re-review provenance, unsatisfied parents, unknown task, and
CAS miss. Tool/CLI handlers surface the specific reason instead of the
generic 'unknown id or not in running/ready'.

Regression tests: live-claim refusal + force/worker paths; malformed
provenance gets a distinct reason and explicit reviewer= recovers.
2026-08-10 12:43:46 -07:00
Jakub Wolniewicz 0acf49b16f fix(kanban): isolate review handoff ownership 2026-08-10 12:43:46 -07:00
Jakub Wolniewicz 6d7e86c262 fix(kanban): enforce review lifecycle invariants 2026-08-10 12:43:46 -07:00
Jakub Wolniewicz 4ab998a7de fix(kanban): close review graph race gaps 2026-08-10 12:43:46 -07:00
Jakub Wolniewicz ae23b1f676 fix: complete kanban review lifecycle
Close the autonomous implement-review-rework loop, preserve parent gating and implementer provenance, distinguish downstream review cards, and surface legacy review dependency deadlocks immediately.

Co-authored-by: kaishi00 <6590895+kaishi00@users.noreply.github.com>
2026-08-10 12:43:46 -07:00
Nikita Barkov 16accefd2f feat(kanban): add first-class "review" handoff lifecycle
Add a non-terminal "review" status so a worker that finished implementation
can hand off for human review without abusing kanban_block. The old
kanban_block(reason="review-required: ...") convention routed the handoff
through the unblock-loop breaker, so a normal review -> changes -> review
cycle was falsely escalated to triage.

- kanban_db: request_review (running/ready -> review, non-block, emits
  review_requested), reopen_review_task (review -> ready/todo, review_reopened),
  complete_task accepts review -> done, and a review_dispatch gate (default off,
  shared by the dispatcher loop and the gateway health probe).
- kanban_request_review worker tool + `request-review` / `reopen-review` CLI
  verbs; tool wired through toolsets, EXPOSED_TOOLS, _POLISHED_TOOLS.
- Gateway notifier wakes the origin subscriber on review_requested and
  block_loop_detected; the subscription survives until done/archived, so every
  review cycle re-notifies.
- Dashboard PATCH + bulk route the review transitions (request_review /
  reopen_review_task) and render the review column.
- goals.py goal-loop and KANBAN_GUIDANCE recognize review as a terminator.
- Docs (reference tables, user guide, AGENTS.md, zh-Hans mirrors) + tests.

needs_input / failed are unchanged: they still route through kanban_block,
still count toward block_recurrences, and still escalate to triage.
2026-08-10 12:43:46 -07:00
ethernet 8789cf9f0c fix(sec): patch the npm advisories main left open
Main (7537de9e7) moved most of the vulnerable locked versions, but some
fixes live only in the lockfiles and some advisories stayed open. This
commit closes the rest:

website/package.json gets durable overrides for js-yaml 4.3.1,
dompurify 3.4.13, mermaid 11.16.1, and tar 7.5.22. The root workspace
gets the same tar override, which moves the tar 6.2.1 copies under
get-windows and @mapbox/node-pre-gyp past twelve open advisories.
Without an override, a reinstall can pull an old transitive copy back
in.

image-size <=2.0.2 has two infinite-loop DoS advisories and no fixed
release upstream. An override points it at @nous-research/image-size
2.0.3, our maintained fork of the real repo. The OSV scanner resolves
the aliased fork cleanly, so no ignore entries are needed.

The photon sidecar moves @opentelemetry/core to 2.10.0. The
whatsapp-bridge gets a body-parser 1.20.6 override, so the lockfile-only
fix from main cannot regress on reinstall.

website/.npmrc gets matching min-release-age exclusions for the fix
releases that are less than two weeks old.

electron stays at 40.10.2. The 41.x fix for GHSA-9f4c-93c8-jc8g brings
back the install failure that bb8280b75 reverted: install.js in 40.10.3+
extracts with an MSVC native binding, which fails on Windows machines
without the VC++ Redistributable. Upstream tracks this in
electron/electron#52481, with no fix released.
2026-08-10 13:49:37 -04:00
Laith Weinberger a1835c8c17 feat(browser): integrate Browser Use CLI 3.0 2026-08-10 10:45:44 -07:00
joaomarcos 82255fa8ef fix(telegram): reset failed primary transport pool
Retryable primary errors can leave pooled sockets in CLOSE_WAIT while fallback retries continue. Replace and close failed primary generation before fallback selection.\n\nRefs #82920
2026-08-10 13:46:48 +05:30
aameobius 100219f664 fix(cron): surface exception type and traceback for standalone Discord delivery errors 2026-08-10 10:58:05 +05:30
ethernet 0e2b5f3835 chore: remove old plan files 2026-08-09 15:17:01 -04:00
Slobaka 3332ad4dbf fix(telegram): avoid MDV2 draft preview when rich_messages lacks rich_drafts
When rich_messages is on and rich_drafts is off, transport=auto used
sendMessageDraft (MarkdownV2 tables→bullets) then finalized via
sendRichMessage. Users saw a crooked first bubble and a second wiki-style
final. Decline drafts in that config so auto uses edit-in-place + rich
finalize on one message.

Fixes #78524
2026-08-09 12:53:45 +05:30
Teknium 05330e804a fix(video): bind managed SeedVR to source request 2026-08-08 17:01:59 -07:00
rob-maron 7065407411
Add more FAL models to nous portal (#82019)
* add more FAL models to nous portal

* fix test

* minor fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 19:59:12 -04:00
Brooklyn Nicholson e358eaf44a perf(sessions): resolve the titling model from the provider's live catalog
Titling ran on the user's main chat model, so a five-word title was billed
to a frontier reasoning model and inherited its latency. Pinning a cheap
model id instead just moves the problem: the hardcoded default was already
dead upstream and every call paid a 404 before the retry net caught it.

Match model FAMILIES against the provider's live /v1/models catalog,
preferring rolling '-latest' aliases where a provider publishes them, and
order the families by measured latency. Nothing to bump when a provider
ships a new mini/flash/haiku. Opt-in per task, so compression, vision, and
search keep 'auto means my chat model'.
2026-08-08 17:07:21 -05:00
Teknium 66ea4e686d feat(media): default-on upscaling for sub-2MP image models (FAL + Krea)
Per review: upscaling should be the default behavior (like the original
flux-2-pro chain), not agent opt-in. Policy: every image model whose
native output is below ~2MP now sets upscale=True in its catalog —
users never silently get low-res images. Native hi-res models
(Seedream 5 Pro/Lite, Krea 2 Large) stay off to avoid paying to
upscale already-large output.

- FAL catalog: 16 models flipped to upscale=True (klein, z-image,
  nano-banana pro/2/2-lite, gpt-image 1.5/2, ideogram v3/v4, recraft
  v4/v4.1, qwen image/3, krea-2 medium on FAL, MAI 2.5 pro).
- Krea plugin: per-model upscale defaults (medium + medium-turbo ON at
  1.5K native; large OFF at 2K native), precedence explicit kwarg >
  image_gen.krea.upscale config > catalog default.
- The 'upscale' tool param remains as a per-call override in both
  directions (false = fast draft, true = force on hi-res/edits).
- Video unchanged: opt-in only (default-on would double every video's
  cost and latency).
- Sibling tests updated: routing/payload tests pass upscale=False where
  the assertion targets the generation submit; catalog test now pins
  the native-resolution policy instead of the flux-2-pro snapshot.
2026-08-08 14:49:28 -07:00
Teknium 137960c9aa feat(media): opt-in upscale pass for image_generate and video_generate across FAL and Krea
The generated-media surface previously had almost no upscaler coverage:
only fal-ai/flux-2-pro chained Clarity Upscaler (hardcoded catalog
default), every other image model returned ~1MP output with no high-res
path, and video had no upscaler at all. Krea's API treats the enhancer
as a standard second pass; this brings the same shape to Hermes.

- image_generate: new optional 'upscale' boolean in the tool schema.
  Explicit true chains the backend upscaler on ANY model (including
  edits); explicit false disables flux-2-pro's automatic default;
  omitted keeps per-model catalog behavior. Response now reports
  'upscaled' so the agent knows which resolution it got.
- FAL image path: explicit flag overrides the catalog 'upscale' default
  (Clarity Upscaler, 2x). Failure falls back to the native image.
- Krea plugin: upscale=true chains Krea Enhance
  (/generate/enhance/krea/enhance, 2x, prompt-guided) through the same
  BYO/managed base URL + auth as generation, with a best-effort poll
  loop that never fails a successful generation.
- video_generate: new optional 'upscale' boolean; FAL video plugin
  chains ByteDance SeedVR2 (fal-ai/seedvr/upscale/video, 2x factor
  mode). Providers without upscalers ignore the kwarg per the ABC
  contract (documented in both ABCs).

Validation: targeted suites green (123 tests across 6 files, including
new coverage for override-wins/default-kept/failure-fallback on all
three paths); live E2E on direct FAL verified both chains end-to-end
(klein 9b + Clarity upscaled image; pixverse-v6 1s 360p + SeedVR2
upscaled video).
2026-08-08 14:49:28 -07:00
Teknium 7537de9e74 fix(deps): patch 31 known CVEs across Python and npm lockfiles
OSV weekly scan reported 50 known vulnerabilities in pinned deps.
This bumps everything with a released, semver-compatible fix:

Python (uv.lock):
- aiohttp 3.14.1 -> 3.14.3 (GHSA-cq5v-8q36-5273, GHSA-mfx4-hv73-q22v,
  GHSA-mq44-7p77-q5h7)
- h2 4.3.0 -> 4.4.1 (CVE-2026-71554 request smuggling; exclude-newer
  exception documented in pyproject, remove after 2026-08-17)

npm (root workspace):
- brace-expansion 5.0.8 -> 5.0.9, undici 6.27->6.28 / 7.28->7.29,
  js-yaml 4.3.1, nanoid 3.3.17/3.3.18, ip-address 10.4.0,
  mermaid 11.16.1 + dompurify 3.4.13 (root overrides so the
  streamdown transitive copy is pinned too)
- electron 40.10.2 -> 40.10.6 (GHSA-r4w5-6pfg-jxp5; the 41.x major
  for GHSA-9f4c-93c8-jc8g is deferred to its own PR)

npm (website): mermaid, dompurify, js-yaml, nanoid, fast-uri 3.1.5,
postcss 8.5.23, undici 7.29.0
npm (photon sidecar): @opentelemetry/core 2.8.0 via override, undici
npm (whatsapp-bridge): body-parser 1.20.6

min-release-age excludes added to .npmrc/website/.npmrc for the
sub-2wk CVE-fix releases, each with a removal date.

Remaining findings are blocked upstream: cryptography <49 cap
(alibabacloud-tea-openapi), image-size (no fixed release), tar 6.x
transitive majors, electron 41.

Local rescan: 50 -> 19 known vulns, 0 introduced.
2026-08-08 14:06:48 -07:00
Teknium 65f407184d fix(email): never let unknown or malformed charsets abort the IMAP fetch
Unknown charset labels (QQ Mail's RFC 1428 'unknown-8bit' placeholder,
misspelled names, garbage encoded-word charsets) raised LookupError from
bytes.decode — errors='replace' only guards decode errors, not a missing
codec — aborting the whole fetch batch. UIDs are marked seen before the
fetch, so the crash permanently dropped every message in the batch.

- _safe_decode(): alias table (unknown-8bit→utf-8, gb2312/gbk→gb18030,
  ks_c_5601-1987→cp949, ...) then utf-8, then latin-1 last resort.
- _decode_header_value(): wraps decode_header() so a malformed RFC 2047
  header degrades to the raw string instead of crashing.
- _extract_text_body(): all three decode sites now use _safe_decode.

Fixes #35901, fixes #55381, fixes #55383.
2026-08-08 12:30:19 -07:00
峯岸 亮 022d196f38 fix(telegram): honor UTF-16 entity offsets 2026-08-08 12:30:19 -07:00
Drexuxux 7c02bfce89 fix(slack): insert resolved display names literally when humanizing mentions
_humanize_user_mentions rewrites <@UID> to @DisplayName by passing the
resolved name as re.sub's replacement, where re parses it as a template.
A display name is arbitrary user-set text, so the escapes in it are the
user's characters, not regex syntax:

  dev\ops  -> re.error: bad escape \o
  a\1b     -> re.error: invalid group reference 1
  \g<0>    -> expands to the whole match, silently putting the opaque
              <@UID> back — the token this method exists to remove

The trigger-text call site sits in _handle_slack_message outside any try,
and both Bolt event handlers await it bare, so the raise takes the whole
inbound message down: every message mentioning that person is dropped.

Pass the replacement as a function instead — re does no template parsing
on the return value, so the name lands verbatim. Same shape the Matrix
adapter already uses for its outbound mention rewrite.
2026-08-08 05:58:43 -07:00
michaelsam94 2b16a6b03c feat(image_gen): add FAL Nano Banana 2 model 2026-08-08 05:31:38 -07:00
Teknium 70c6cf8e7e feat: add new FAL video families and image models
Video (plugins/video_gen/fal): Seedance 2.5, MiniMax H3, Seedance 2.0
Mini, FLUX 3, Grok Imagine 1.5, Gemini Omni Flash (i2v-only). New
family capability flags:
- duration_int: endpoints that take duration as a JSON integer
- resolution_aliases: maps 720p/1080p-style values onto non-standard
  enums (H3's 768P/2K/4K)
- image_drop_keys: strips keys the family's i2v endpoint rejects
  (aspect_ratio on Seedance 2.5 / H3 / Grok 1.5)

Image (tools/image_generation_tool): Seedream 5.0 Pro (+edit) and
Lite, Ideogram V4 instant + fast, Qwen Image 3 (+edit), MAI Image 2.5
Pro, Nano Banana 2 Lite (+edit), Recraft V4.1.

Every new endpoint live-tested against fal.run through the real
payload builders + submit path: 18/18 pass (t2v, i2v, t2i, and edit
probes). Note: several new endpoints return HTTP 409 from the Nous
Portal FAL proxy allowlist until it is updated portal-side; BYOK
FAL_KEY works today and the existing 4xx guidance message covers it.
2026-08-08 04:31:55 -07:00
kshitij b3344502f8 chore: map Axmr1 email + update stale Go routing docstring
- Add contributors/emails/Axmr1@users.noreply.github.com for CI
  attribution check (bare noreply format needs explicit mapping)
- Update opencode-zen plugin docstring: Go routing now includes
  GPT → codex_responses and Qwen → anthropic_messages (was stale,
  only listed MiniMax and GLM/Kimi)
2026-08-08 14:47:11 +05:30
kshitij 520a1e7812 fix(honcho): keep _pop_auth_notice tolerant of minimal fake managers; make fast-path test binding
Gap-fill from the follow-up commit's own review:

- __init__.py: restore getattr tolerance in _pop_auth_notice — test
  fixtures outside tests/honcho_plugin/ install minimal fake managers
  without pop_auth_notice (tests/test_honcho_startup_fail_open.py's
  SlowManager failed with AttributeError). Exceptions still propagate;
  only the blanket except was dropped.
- test_auth_recovery.py: the fast-path test used a raising stub, but
  _reauth_required swallows all exceptions — the test passed even with
  the fast path removed. Rewritten as a recording spy with a call-count
  assertion; mutation-verified (removing the fast path now fails it).
- test_auth_recovery.py: autouse fixture resetting oauth module dicts
  (_dead_grants, _refresh_failure_at, _reauth_check_cache,
  _expiry_cache) so state can't leak between tests.

honcho_plugin 293 + test_honcho_startup_fail_open 7 + plugins/memory
285 = 585 passed.
2026-08-08 14:40:46 +05:30
kshitij edfe4f5136 refactor(honcho): dedupe refresh-failure handling; harden exchange budget, dogpile cooldown, and rebuild race
Follow-ups from review of #80590:

- oauth.py: extract _rotate_and_persist() — the twin ~18-line
  OAuthRefreshError permanent/transient handling blocks in
  ensure_fresh_token and force_refresh_token were byte-identical
  except the log verb.
- oauth.py: cap the exchange cycle at _REFRESH_TOTAL_BUDGET_SECONDS
  (20s). The retry runs while holding the global refresh locks on the
  path to a memory call; a timed-out first attempt no longer earns a
  second full 15s exchange (~32s lock hold -> <=20s).
- oauth.py: transient-failure cooldown (_refresh_failure_at, 30s).
  Waiting threads and later turns fail open to the stale token instead
  of serializing their own full exchange cycles against an endpoint
  that just failed. Cleared on successful rotation and re-login.
- oauth.py: mtime-gate reauth_required()'s config read — the dead-grant
  state persists until re-login, and the verdict can only change when
  the config file is rewritten; drop the per-call read+parse.
- oauth.py: derive _TOKEN_VALUE_RE from ACCESS_TOKEN_PREFIX /
  REFRESH_TOKEN_PREFIX so a prefix change can't silently break
  redaction; promote redact_tokens to public (session.py imported the
  private name).
- session.py: fast path in _reauth_required — skip config-path
  resolution entirely while no grant is dead (runs before every SDK
  call).
- session.py: client-generation counter closes the fetch/store race in
  _sdk_session/_get_or_create_peer — an object resolved from the old
  client mid-rebuild is no longer cached (it would 401 forever and burn
  a token rotation per retry).
- __init__.py: drop the getattr/callable/except triple-guard in
  _pop_auth_notice; the manager is always None or HonchoSessionManager.

7 new tests (budget, cooldown x3, generation guard, fast path); all
mutation-checked (disabling each guard fails its test). honcho_plugin
293 passed; plugins/memory 285 passed; live E2E against a real HTTP
token endpoint re-verified.
2026-08-08 14:40:46 +05:30
Erosika 086dc8b880 fix(honcho): surface the auth notice when session init itself fails
An init-time HonchoAuthError discarded the manager that recorded it, so
context/hybrid prefetch returned nothing and tools mode returned the
generic init error. The provider now keeps the failure detail across the
manager discard, prefetch emits the one-time notice at the readiness
guard, tools mode returns an explicit authentication error, and a
successful re-login retry clears the stored failure. Non-auth init
failures keep failing open with no notice.
2026-08-08 14:40:46 +05:30
Erosika 864035b241 fix(honcho): route every authenticated sdk call through one 401-recovery helper
_authed_call checks the dead-grant marker before calling, retries a
confirmed auth failure once after a forced refresh, and records the
failure for the one-time notice. Operations re-resolve their peer and
session objects inside the call, so a retry after a client rebuild no
longer reuses objects bound to the old transport. Tool handlers now
return an explicit auth error instead of an empty result, and non-auth
failures keep their fail-open behavior.
2026-08-08 14:40:46 +05:30
Erosika da1f8779ef style(honcho): trim auth recovery comments to one line each 2026-08-08 14:40:46 +05:30
Erosika b1414baa09 fix(honcho): stop classifying bare '401' digits as auth errors; redact session-side auth logs
_is_auth_error matched the substring '401' anywhere in an error string,
so a latency figure ('retry after 4010 ms'), a request id, or a
workspace name containing those digits classified as an auth failure.
A false positive calls _force_reauth, which runs a real token exchange;
the server rotates the refresh token on every exchange, and a lost
rotation response leaves Hermes holding a superseded token whose later
replay revokes the whole grant — the exact wedge this branch fixes.

The status attribute check (SDK AuthenticationError carries status=401)
does the real work and stays first. A concrete non-401 status now wins
over ambiguous text. The text fallback keeps only specific markers:
'invalid or expired access token', 'authentication failed' (not bare
'authentication', which also matches auth-infrastructure outage
messages), 'unauthorized', and '401' only with HTTP context ('HTTP
401', 'status 401'), never as a bare number. The classifier is biased
toward false negatives: a missed auth error costs one un-recovered
call, a false positive spends a rotation.

Also redacts token values in _record_auth_failure, _auth_error_message,
and the two retry warnings, matching oauth.py. The SDK's auth errors
carry no token values today, but this is the one credential path where
an upstream regression would leak silently.

Tests: the four false-positive strings stay non-auth, HTTP-context 401s
still match, a concrete 429 status beats 'authentication failed' text,
and the recorded failure plus notice redact token values.
2026-08-08 14:40:46 +05:30
Erosika ecfc427b28 fix(honcho): skip memory calls while the oauth grant is dead
reauth_required() existed but nothing called it, so after a grant died
every dialectic fire and sync flush still sent a Honcho API call that
401ed. dialectic_query and _flush_session now check the dead-grant flag
first and skip the call: dialectic raises HonchoAuthError (exempt from
cadence backoff), sync returns False with the failure recorded so the
one-time notice still fires.

The check compares the on-disk refresh-token digest, so a re-login flips
it back with no network call and the next cadence resumes immediately.
Transient auth errors keep the existing force-refresh-and-retry path.

Four new tests: a dead grant issues no dialectic or sync call, and a
re-login resumes both without waiting.
2026-08-08 14:40:46 +05:30
Erosika 6ea01262fc fix(honcho): recover memory from mid-session oauth 401s and tell the user once
An expired access token could pause Honcho memory for hours with no
user-facing signal: ensure_fresh_token swallowed every exchange failure
and returned the stale token, no code handled a 401 from the Honcho API,
and each failed dialectic cycle widened the cadence backoff. Hypothesis
for the trigger (not confirmed): the refresh POST times out after the
server already rotated the token pair, Hermes keeps the old refresh
token, and the eventual replay lands outside the server's 60-second
rotation grace window, which revokes the whole grant.

- oauth: the exchange reads the token endpoint's error body instead of
  discarding it. invalid_grant and other permanent OAuth errors mark the
  grant dead so no code retries a revoked grant; transient failures retry
  once immediately, which keeps a replayed refresh token inside the grace
  window. Log lines redact token values.
- oauth: force_refresh_token() rotates the token now, ignoring local
  expiry, to recover from a server-side 401.
- session: dialectic_query and _flush_session treat a 401 as a trigger to
  force one token rotation and retry the call exactly once. A persistent
  auth failure raises HonchoAuthError (dialectic) or records the failure
  (sync) instead of being returned as an empty result.
- provider: injects a one-time notice into the memory context so the
  model tells the user memory is paused and 'hermes honcho setup'
  restores it. Auth failures no longer widen the dialectic cadence
  backoff.

New tests cover the exchange retry, invalid_grant terminality plus
re-login recovery, forced refresh, 401 retry on both the sync and
dialectic paths, the one-time notice, and the backoff exemption.
2026-08-08 14:40:46 +05:30
kshitij 0041fc6946 refactor(simplex): hoist json.dumps above branch in _standalone_send
Deduplicate the composed JSON payload construction — both the group
and DM branches build the identical json.dumps result, so hoist it
above the if/else to match the pattern already used in send().
2026-08-08 13:56:12 +05:30
liuhao1024 eb048772f6 fix(simplex): use structured /_send for standalone DM text sends
The _standalone_send() function (used by the send_message tool for
proactive/scheduled sends) has the same bare `@<id> text` bug that
send() had before PR #44444. SimpleX's `@<x>` syntax resolves x as a
display name, not a contactId — the daemon silently drops messages
when it cannot find a contact named "6".

Use the structured `/_send @<id> json [...]` form, matching what
send_image, send_document, and the send() fix already use.

Fixes #46265
2026-08-08 13:56:12 +05:30
liuhao1024 3a04d9c4d7 fix(simplex): use structured /_send for DM text messages to prevent silent drops 2026-08-08 13:56:12 +05:30
rob-maron b3aa561faf
add Hermes headers to Fireworks provider (#81321) 2026-08-07 20:56:29 +00:00
kshitij e5e96e8bb5 fix: harden _await_disconnect_step against outer cancellation + add claim keys
Follow-up to #80700:

1. _await_disconnect_step was missing the try/except CancelledError around
   asyncio.wait() that _await_adapter_cleanup_with_timeout already has.
   When the outer fatal-handler timeout cancels disconnect() mid-step,
   asyncio.wait does NOT cancel its inner task — the task was orphaned
   with no observer. Add the same cancel+detach+re-raise pattern.

2. _queue_retryable_fatal_platform omitted credential_claim/listener_claim
   keys that all 3 startup-path queue sites include. These are consumed by
   the multiplex reservation logic to prevent secondary profiles from
   taking the endpoint while a primary is queued. Pre-existing latent bug
   — now fixed since the extraction makes it trivial.
2026-08-07 18:32:45 +05:30
HexLab98 7141a6dc3a fix(gateway): queue reconnect before fatal disconnect wedges (#80598)
After a network outage the Telegram fatal handler could hang inside
disconnect() and never populate _failed_platforms, so the reconnect
watcher had nothing to retry and the process stayed permanently deaf.
Queue retryable platforms before any disconnect await, bound the fatal
handler with an outer detach deadline, and release the Telegram token
lock / PTB close steps with detach-on-timeout so recovery cannot stall.
2026-08-07 18:32:45 +05:30
Gille f346458f29 fix(cron): surface initial scheduler registration failures 2026-08-07 17:45:06 +05:30
kshitij 6ab7528c33 refactor(matrix): extract _strip_reply_fallback to deduplicate text+media handlers
The reply fallback stripping loop was copy-pasted between _handle_text_message
and _handle_media_message. Extract into the module-level _strip_reply_fallback
helper, matching _extract_reply_fallback's existing pattern.
2026-08-07 14:34:55 +05:30
kshitij e8511efe75 fix(matrix): propagate sender + reply context to media MessageEvents too
_handle_media_message had the same gap as _handle_text_message: it didn't
set user_id/user_name or any reply_to_* fields on MessageEvent. A user
replying to a photo/video/audio in Matrix got null sender metadata and
null reply context — the exact bug PR #80293 fixes for text messages.

Mirrors the text handler's reply fallback parsing + sender propagation
into the media handler, and adds a test covering a media reply event.

Also fixes test env mutation: _make_adapter now accepts monkeypatch
and uses monkeypatch.setenv() instead of bare os.environ assignment,
preventing env var leakage across tests in the same xdist worker.
2026-08-07 14:34:55 +05:30
WintleChoung e245a98781 fix(matrix): propagate sender MXID + reply context to MessageEvent
The Matrix adapter built MessageEvent from inbound room events but dropped
the sender's MXID and display name on the event itself -- only 'source'
carried them. Other adapters (signal/slack/telegram/discord/mattermost/irc)
have the same gap; this PR fixes matrix and adds the supporting
top-level MessageEvent fields so the rest can follow.

Downstream effects for matrix specifically:
  - gateway prompt assembly can now read event.user_name (or source)
    without having to dig into source per platform
  - reply context (reply_to_text / reply_to_author_id /
    reply_to_author_name) is parsed from the inline > <@user:server> ...
    Matrix fallback format before stripping, instead of discarded
  - the gateway's existing [Replying to: "..."] renderer can now show
    who the user was replying to (was always anonymous for matrix)

MessageEvent gains two optional top-level fields (user_id, user_name,
both default None) so non-IM producers (cron/webhook/autonomous) remain
unaffected. Source still carries the same values for callers that
already read from there.

Tests cover:
  - non-reply message carries sender user_id/user_name on MessageEvent
  - different senders (alice, bob) both propagate
  - reply message carries reply_to_message_id + reply_to_text +
    reply_to_author_id + reply_to_author_name, parsed from the
    > <@carol:example.org> original question\n\nactual reply shape
  - non-reply message does NOT spuriously set reply_to_* fields

Sibling matrix tests (148 across test_matrix*.py) remain green.

Authored by WintleChoung <cwt@users.noreply.github.com>
Salvaged from PR #80293.
2026-08-07 14:34:55 +05:30
kshitij 99237a4444 refactor: derive teams install hint via feature_install_command(venv_pip=True)
Fold the remaining simplify-code reuse finding: teams' _install_hint()
duplicated lazy_deps' spec-fetch + quote + join (feature_install_command
already builds pip commands from LAZY_DEPS). Add a venv_pip=True variant
to feature_install_command — sys.executable -m pip targeting, correct in
every install layout and immune to PEP 668 — and shrink the teams helper
to a one-line call.  Also gives matrix and the other platforms a shared
derived hint to adopt later.  New test mutation-checked (fails when
venv_pip returns the uv form).
2026-08-07 13:28:43 +05:30
kshitij f5784617e8 refactor: fold simplify-code review findings
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps,
  ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior
  check_*_requirements combined credential env checks with the install,
  so a platform configured via PlatformConfig.extra (which is_connected
  accepts) would pass enablement, reach create_adapter(), and have the
  'installer' veto on env-var grounds before installing anything —
  re-creating the #79812 deadlock for extra-configured setups.  The
  combined deps+credentials functions remain for setup/status callers.
- matrix/feishu passive probes: use the existing lazy_deps.is_available()
  instead of hand-rolling 'not feature_missing(...)' (reuse finding).
- teams: module docstring no longer recommends bare system pip (the
  PEP 668 trap purged everywhere else); docs troubleshooting row updated
  to match the new hint text.
- wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE'
  (ensure_and_bind mutates the module dict directly; nothing assigns).
- tests: parametrized wiring contract for all 8 lazy-installable
  platforms — ensure_deps_fn present and distinct from check_fn
  (behavior contract, not identity snapshot, so renames don't churn it).
2026-08-07 13:28:43 +05:30
kshitij a658dfe509 fix: address self-review findings on the check_fn/ensure_deps_fn split
- gateway/config.py: rewrite the stale enablement-pass header comment that
  still described check_fn as 'the single source of truth for are-my-env-
  vars-set' / 'lazy-installs it' — both false under the new contract.
- teams: check_requirements docstring wrongly claimed credential checks
  (body checks only SDK/aiohttp presence); derive install_hint from the
  canonical LAZY_DEPS pins + sys.executable instead of hardcoding
  '~/.hermes/hermes-agent/venv/bin/pip' and version pins (wrong under
  HERMES_HOME overrides / profile installs; pins go stale on CVE bumps);
  connect() fatal-error hints now point at the venv pip instead of bare
  system pip (the PEP 668 trap the docs warn about).
- teams docs: drop exact version pins from the two manual-install commands
  (LAZY_DEPS is the source of truth; unpinned installs still work and the
  text can't go stale).
- hermes_cli/status.py: per-entry exception guard around check_fn so one
  raising probe can't abort the listing of all remaining plugin platforms
  (aligns with the other three call sites).
- tests: rename test_register_check_fn_is_active_lazy_installer ->
  test_register_splits_passive_probe_from_active_installer (name said the
  opposite of what it verifies).
2026-08-07 13:28:43 +05:30
kshitij 0d32607c62 fix(gateway): split check_fn (passive probe) from ensure_deps_fn (active installer)
PlatformEntry.check_fn served three contradictory roles: adapter-creation
gate, config auto-enablement gate, and status display. Plugins had to pick
one function for all three:

- Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/
  feishu): every status display could pip-install SDKs as a side effect
  (the desktop 94% boot-loop class).
- Passive probe as check_fn (teams, wecom_callback): create_adapter()
  returned None before connect() could lazy-install, so the SDK never
  installed (#79812 deadlock; wecom_callback's platform.wecom_callback
  LAZY_DEPS entry was dead code).

The split makes both call sites correct by construction:

- check_fn is now contractually PASSIVE (probe only, never installs).
- New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer;
  create_adapter() runs it exactly when check_fn is False — the platform
  is enabled+configured and the gateway is about to connect it.
- Config enablement keeps a configured platform whose deps are missing
  but installable; the install itself is deferred to create_adapter().
- Status surfaces (_platform_status, hermes status) read only the
  passive probe and can never trigger pip.

Migrated all lazy-installable platform plugins to the split; platforms
with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged
— no ensure_deps_fn means a False check_fn stays a hard block.
wecom_callback gains a working installer for the first time.

Builds on @xxxigm's #79812 (both commits cherry-picked with authorship
preserved), reworking the check_fn swap into the two-field split so the
Teams fix doesn't reintroduce install-on-status.
2026-08-07 13:28:43 +05:30
xxxigm 98408f713b fix(teams): lazy-install SDK via registry check_fn
Platform registry create_adapter() gates on check_fn before the adapter
exists, so wiring the passive probe permanently blocked connect() and
the existing check_teams_requirements() lazy-install never ran.
2026-08-07 13:28:43 +05:30
liuhao1024 358d55051e fix(plugins): use asyncio.wait_for instead of ClientTimeout in Matrix standalone send
Fixes #61495

When manually triggering cron jobs from a live Matrix session, delivery
would fail with "Timeout context manager should be used inside a task"
because the aiohttp.ClientTimeout context manager requires a proper asyncio
task context.

Use asyncio.wait_for() instead of aiohttp.ClientTimeout to avoid this error,
following the same pattern as the Weixin platform (gateway/platforms/weixin.py).

Changes:
- Remove aiohttp.ClientTimeout(total=30) from ClientSession constructor
- Wrap the send operation in a nested async function (_do_send)
- Use asyncio.wait_for(_do_send(), timeout=30) for timeout handling
- Catch asyncio.TimeoutError explicitly and return clear error message
2026-08-06 23:14:55 -07:00
rob-maron 226b095a59
Fireworks user agent (#80422) 2026-08-07 01:49:57 +00:00
Teknium b6d55a790e fix: adapt Actual provider salvage to current main
- fetch_models(): accept base_url kwarg (interface grew on main since May)
- runtime_provider: config-driven loopback base_url now reaches the local
  no-auth placeholder before the usable-secret gate (added on main in the
  interim, would otherwise AuthError on keyless local setups)
- test: fetch is now called with base_url by the generic live-fetch path
2026-08-05 14:08:32 -07:00
Justin Bennington a9acb400ba feat(providers): add Actual Computer inference provider 2026-08-05 14:08:32 -07:00
joaomarcos 34c3f06f91 fix(cache): scope prompt_cache_key by session to stop cross-session bucket sharing
Cherry-picked from PR #78959 by @JoaoMarcos44 with authorship preserved.
Follow-up: hoist _cache_scope_from_session_id(session_id) to a local in
build_kwargs so it's computed once instead of 4 times per call.

Closes #78941. Closes #79012. Closes #79013. Closes #79014. Closes #79015.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-08-05 12:42:46 +05:30
brooklyn! b3e45a3d46
Discord drops an empty outbound message instead of sending it (#78815)
* fix(discord): reject empty outbound messages

* test(discord): cover empty final reply backfill state

Missed-message backfill decides what to replay from discord_messages, so
a dropped final reply must be recorded as failed by the new guard the
same way the exception path records one — otherwise the reply is both
never sent and never retried.

Co-authored-by: Jony <619963502@qq.com>

* chore: map 619963502@qq.com to zyz619963502zyz for PR #73449 salvage

---------

Co-authored-by: Jony <619963502@qq.com>
2026-08-04 18:23:04 +00:00
kshitij e05eba26a3 fix(telegram+sqlite): resolve polling conflict loop + misleading WAL warning
#75017: Telegram polling conflict retry used drop_pending_updates=False,
starting a new getUpdates session that immediately got 409'd by the
previous still-expiring session — creating the very conflict it was
trying to recover from. Switch to drop_pending_updates=True so Telegram
terminates stale sessions. Also add a recovery-generation guard so the
first transient getUpdates success after a retry doesn't reset the
conflict counter back to 0 (defense-in-depth from PR #75096).

#75153: The WAL-reset warning always said 'hermes update can repair'
even for git/pip/system Python installs where it can't. Now uses
detect_install_method() + recommended_update_command_for_method() to
give a context-appropriate hint (hermes update for git, docker pull for
docker, nix message for nix, generic install hint as fallback).
2026-08-04 14:34:50 +05:30
Brin Shadewater e6f1d613b6 fix(discord): leave voice channels before cancelling the bot task
`DiscordAdapter.disconnect()` cancelled the bot task before tearing down voice
clients. `leave_voice_channel()` ends in `await vc.disconnect()`, and discord.py
sends a voice state update over the main gateway websocket and then waits for the
voice socket to close. The bot task is the loop running that gateway connection,
so cancelling it first left the handshake with no transport: it could never
complete and blocked until the caller's shutdown timeout fired.

The effect was a fixed ~5s penalty on every shutdown with a voice connection
open, ending in "discord disconnect timed out after 5.0s - forcing continue",
with the voice disconnect abandoned rather than completed.

Measured on a live gateway with a voice connection open in both cases:

  before: timed out after 5.0s, all adapters disconnected at +5.29s
  after:  discord disconnected (0.12s), all adapters disconnected at +0.46s

Moving the voice-cleanup loop above `_cancel_bot_task()` preserves the
zombie-client protection its comment describes: the bot task is still cancelled
before `client.close()`, just after voice teardown rather than before it. Voice
teardown is the one step that still requires a live gateway.

Adds a regression test asserting the ordering. It fails on the previous ordering
at index 1 with `cancel_bot_task != leave_voice_channel:111`.

Fixes #76044
2026-08-03 22:47:14 +05:30
kshitij 41cc4a13fe fix(openviking): catch endpoint errors in setup validation functions
Review follow-up for salvaged PR #76782. Three setup-wizard
validation functions called _normalize_openviking_url outside their
try/except blocks. Since _normalize_openviking_url now raises
_OpenVikingEndpointError for blocked or malformed endpoints, an
invalid endpoint would crash the wizard instead of returning a
friendly (False, message) tuple.

- _validate_openviking_auth: move _normalize_openviking_url inside try
- _validate_openviking_root_access: same
- _validate_openviking_setup_values: catch _OpenVikingEndpointError explicitly
- Remove dead ternary in _normalize_openviking_url safety check (candidate
  always has http/https scheme by that point)
- Replace redundant float('-inf') < x < float('inf') with math.isfinite()
  in _setting_float; drop the redundant infinity check from _setting_int
  (is_integer() already rejects inf/nan)
2026-08-03 20:35:47 +05:30
ehz0ah a49a9e5e37 fix(openviking): verify servers before sending credentials 2026-08-03 20:35:47 +05:30
ehz0ah e443d32718 test(retaindb): guard scoped secret config resolution 2026-08-03 20:35:47 +05:30
ehz0ah e43bc0b7aa fix(openviking): integrate reliability and configuration hardening 2026-08-03 20:35:47 +05:30
justemu 4ebe9904f8 fix(openviking): read recall settings from config.yaml first, env vars as fallback
_recall_config() previously read all settings (recall_limit, score_threshold,
recall_resources, etc.) exclusively from environment variables. This forced
users to store behavioural configuration in .env, violating the Hermes
convention that .env is for secrets only.

The infrastructure to load config.yaml -> memory.openviking was already in
place via _load_hermes_openviking_config(), but _recall_config() never
called it.

Fix: call _load_hermes_openviking_config() and pass its values as the
default parameter to _env_int/_env_float/_env_bool. Env vars still override
config.yaml values, preserving backward compatibility.

Closes #62540

(cherry picked from commit 6aadf12568)
2026-08-03 20:35:47 +05:30
PRATHAMESH75 5396dd8f02 fix(memory): read non-secret provider config from config.yaml for OpenViking and RetainDB
OpenViking is_available() only consulted env vars and use_ovcli_config, so an
endpoint saved to config.yaml (e.g. by the Dashboard) reported needs_config;
_resolve_connection_settings() likewise never folded config.yaml's non-secret
fields into its chain. RetainDB initialize() read base_url/project from the
environment only, ignoring the values the Dashboard writes to config.yaml.

Both now resolve non-secret fields as env -> (ovcli ->) config.yaml -> default;
secrets still come from the environment. Adds regression tests for both.

Fixes #68209

(cherry picked from commit dca57915b9)
2026-08-03 20:35:47 +05:30
Jeff Mettel f0cb219e5e fix(openviking): re-arm the commit guard after in-place compression
`_committed_session_ids` is a permanent per-sid latch, and
`_session_needs_commit` checks it before the turn counter by design — a
racing sync_turn can re-increment `_turn_count` after commit+reset, so
the guard must win to stop a double-commit.

That is correct for a session being left behind. It is wrong for one
that keeps its id. `compress_context()` commits before rewriting the
transcript in both modes, and with `compression.in_place: true` (the
default) `on_session_switch` receives the same id and does not rotate.
The latch then rejects every later commit for a still-live session — the
next compression, /new, normal session end, startup recovery — so every
post-compression turn is silently never extracted.

Rotation mode is unaffected because a fresh child id is minted and
starts clean, which is what confirms the latch's intent was only ever to
dedupe the departing id.

Clear the latch when compression completes without rotation. Turns
arriving after that point are genuinely new, and this is a defined
moment rather than a race. The rotation path is untouched, so the old
id stays latched and its _finalize_session_async still dedupes against
the compression commit.

Fixes #74695

(cherry picked from commit d1e5c3dc33)
2026-08-03 20:35:47 +05:30
ddy4633 9014aa0263 fix(openviking): drop stale "disabled for this Hermes run" warnings
The provider used to disable OpenViking permanently when the server was
unreachable. That was fixed: `_ensure_client()` now reconnects lazily,
with a 30s cooldown gate in `_ensure_client_locked`.

Only one of the seven user-facing warnings was updated to match. The
other six still told the user memory was "disabled for this Hermes run",
which is no longer true — every one of those paths is retried on the next
access. A user who reads the old message has no reason to retry, which is
very likely how #5721 ("never recovers") came to be filed against
behaviour that already recovers.

All six sites were traced to confirm none is terminal for the run: the
`initialize()`-time and waiter-thread failures never arm `_failed_refresh`
(only line 2439 does), so they retry on the very next access with no
cooldown at all.

The replacement wording deliberately omits the "(after cooldown)"
parenthetical used at the already-correct site — that detail is only
accurate where `_failed_refresh` was just armed. The neutral phrasing is
true at all six.

Also promotes two clause separators to periods to avoid "…; …disabled;"
collisions.

(cherry picked from commit 8346403a4b)
2026-08-03 20:35:47 +05:30
Jeff Mettel a3f6953f1a fix(openviking): don't spawn a second server onto a live port
`_start_local_openviking_server()` spawned `openviking-server`
unconditionally. Both callers — `initialize()` and the runtime
unreachable handler — reach it from a health probe, and that probe can
time out client-side while the server is up and serving. The spawned
process then loses the data-directory lock and exits immediately with
`DataDirectoryLocked`; because the probe keeps timing out, the cycle
repeats every cooldown window (~5 min observed).

The existing 30s `_failed_refresh` cooldown paces the loop but cannot
stop it, since it expires while the underlying condition persists.

Probe the target host:port before spawning and treat an occupied port as
already-started. This guards both call sites at their single convergence
point. The probe deliberately tests only that a listener owns the port —
enough to know a second server would lose the lock — and says nothing
about that listener's health.

The parse/probe now precedes the PATH lookup, so a reachable server is
reported as running even when `openviking-server` is not on PATH.

Fixes #74846

(cherry picked from commit b49427d85f)
2026-08-03 20:35:47 +05:30
峯岸 亮 65bcca650b fix(openviking): fail closed on blocked endpoints
(cherry picked from commit 389a90b81c)
2026-08-03 20:35:47 +05:30
峯岸 亮 c7fd21add3 fix(security): reject always-blocked OpenViking endpoints
## Summary
- Normalize OpenViking endpoints through `is_always_blocked_url` and fall back to the default local endpoint when poisoned.
- Keep intentional loopback / LAN self-host working.
- Add focused unit tests.

## Salvage / credit
Memory-provider endpoint floor sibling of RetainDB/Supermemory always-blocked hardening (avoids over-broad #4984-style private-IP bans).

(cherry picked from commit 8fa607d0ae)
2026-08-03 20:35:47 +05:30
kshitijk4poor 34d6095e41 refactor(honcho): delegate _is_trivial_prompt wholly to the shared classifier
Simplify-pass finding: sharing only the REGEX left the wrapper logic
(empty/strip/slash checks) duplicated, half-defeating the no-drift goal.
The classmethod now calls agent/memory_provider.is_trivial_prompt directly;
_TRIVIAL_PROMPT_RE stays as a class attr for backward compatibility with
any external referents.
2026-08-03 17:53:55 +05:30
kshitijk4poor c093492b06 refactor(memory): single shared trivial-prompt classifier + gate tests
Rebase fold on the salvaged gate:
- is_trivial_prompt/TRIVIAL_PROMPT_RE move to agent/memory_provider (the
  ABC both the core gate and providers already import) — one source of
  truth; honcho's _TRIVIAL_PROMPT_RE now aliases it, turn_context and the
  queue_prefetch_all warm path (a sibling site main grew after the PR's
  base) both use it
- tests: gate tests at the prefetch call site (mutation-checked), shared
  classifier tests incl. prefix-collision guards (k8s/yolo/note/supper),
  and honcho dialectic-machinery tests re-driven with a substantive prompt
  ("hello" became trivial by design — those tests exercise thread cadence,
  not the classifier)
2026-08-03 17:53:55 +05:30
Ayush Sahay Chaudhary 2f14c3e5b0 fix: skip memory prefetch on trivial user prompts (greetings)
Salvage of PR #25350 (commits 88ffede2d + 2b848a0b2 + 3136dc63a, squashed
and ported): the run_agent.py prefetch site the PR gated has since moved
into agent/turn_context.py's build_turn_context(), so the trivial-query
gate lands there instead.

- Gate the per-turn memory_manager.prefetch_all() on a trivial-prompt
  check so greetings/acknowledgements ('hi!', 'thanks', 'ok') no longer
  block the turn on provider network round-trips or inject stale context.
- Extend honcho's _TRIVIAL_PROMPT_RE with greetings and a trailing
  punctuation class so 'hey!' / 'hello.' classify as trivial.
- Add honcho classifier tests for greeting forms.
2026-08-03 17:53:55 +05:30
kshitijk4poor e80b7aeda1 fix(feishu): test SDK globals by None-ness, not globals() membership
The no-SDK fallback guards check '"Name" in globals()' — correct on
main where a failed module-level import leaves those names undefined,
but the deferred-import port pre-binds every SDK name to None, so the
guard was always true and the fallback paths called .builder() on None
(AttributeError) wherever lark_oapi isn't installed. Local runs passed
because lark IS installed here; CI's default env has no feishu extra.
Rewrote all 14 guards to 'is not None', which is correct under both
conditions. Verified by simulating CI with a lark-blocking meta_path
hook: 74 passed, 18 skipped (the skipUnless set), zero failures.
2026-08-03 17:24:11 +05:30
baau b51c4e6a78 fix(feishu): defer the lark_oapi import off the startup path
Salvage of #57657, ported onto the plugin layout (the adapter moved
from gateway/platforms/feishu.py to plugins/platforms/feishu/adapter.py
since the PR's base). lark_oapi takes seconds to import and holds the
GIL doing it; the module-level import made every gateway boot pay that
cost even with Feishu unconfigured.

- _load_lark_oapi() with double-checked locking binds the SDK globals
  on first use; connect() and _standalone_send() call it via
  asyncio.to_thread so the loop never blocks on the import.
- probe_bot() also calls _load_lark_oapi() (sync context) so the SDK
  probe path is preserved rather than silently degrading to the HTTP
  fallback before a first connect.
- check_feishu_requirements() is install-only and no longer rebinds
  globals; test_feishu.py gets a setUpModule that binds them eagerly
  for tests that inject fake clients.

Includes the dedicated lazy-import test file (check-does-not-import,
connect-loads-on-worker-thread).
2026-08-03 17:24:11 +05:30
ErnestHysa 425c54b51e fix(platforms/line): fix broken import of non-existent config functions
_adapter_config_interactive() imported get_env_var and set_env_var from
hermes_cli.config, but these do not exist — the actual functions are
get_env_value and save_env_value. This caused an ImportError at runtime,
breaking the entire LINE platform adapter setup.

Pain before: Any user who ran the LINE adapter setup function would get:
    ImportError: cannot import name 'get_env_var' from 'hermes_cli.config'

Fix: Import the correct functions with aliased local names:
    from hermes_cli.config import get_env_value as _get_env, save_env_value as _set_env

Also fixed an indentation bug introduced during the fix: the 'if value: _set_env()'
block was incorrectly nested inside the except clause.

PR: N32 (hermes-agent audit)
2026-08-03 16:54:02 +05:30
Teknium dd600d1ace fix(discord): suppress link embeds in tool preview markdown links
Wrap the masked-link destination in angle brackets so Discord does not
unfurl an OG-preview embed under every tool progress bubble. quote()
percent-encodes any <> inside the URL itself, so the wrapper cannot be
broken out of.
2026-08-02 21:48:06 -07:00
Coffee☕️ e599f100ec fix(discord): avoid truncated URL link targets 2026-08-02 21:48:06 -07:00
Coffee☕️ 7af104b37b refactor(discord): keep link formatting adapter-local 2026-08-02 21:48:06 -07:00
Coffee☕️ 56941eb329 refactor(gateway): share markdown link formatting 2026-08-02 21:48:06 -07:00
Coffee☕️ 911d8dfbf4 refactor(discord): simplify tool preview links 2026-08-02 21:48:06 -07:00
Coffee☕️ df9e039d2d fix(discord): preserve links in truncated tool previews 2026-08-02 21:48:06 -07:00
kshitij 648c01c693 fix(matrix): pickle key from resolved device ID, skip migration after reset, cache enc info
Follow-up fixes for the combined Matrix crypto salvage (#71073,
#71543, #71547):

1. Construct _pickle_key from client.device_id (resolved from whoami)
   instead of self._device_id (the configured value). Without this, when
   #71543 makes the token's real device win over a stale
   MATRIX_DEVICE_ID, the pickle key is built from the stale value and
   the Olm account is stored under a key that can never be looked up
   again — perpetuating the same decryption failure the PRs aim to fix.

2. Skip _migrate_legacy_crypto_pickle when the store was just deleted
   by _reset_crypto_store_if_device_changed — there is no account to
   migrate. Also check the migration return value and log a warning on
   failure instead of proceeding to olm.load() which fails with a
   cryptic BAD_ACCOUNT_KEY.

3. Add a local dict cache (_enc_info_cache) to _CryptoStateStore so the
   homeserver fallback in get_encryption_info() does not make a network
   round-trip on every is_encrypted() call. MemoryStateStore does not
   implement set_encryption_info, so the existing cache-back is a no-op.

4. Log homeserver encryption-info query failures at DEBUG level instead
   of silently returning None (which would cause OlmMachine to treat an
   encrypted room as unencrypted).

5. Update _CryptoStateStore docstring to mention the homeserver fallback.
2026-08-03 10:03:35 +05:30
ckaznocha 80d5a57b94 fix(matrix): commit the migrated account only after the session sweep
The account was written under the new pickle key before sessions were
re-pickled. The account is effectively the migration's commit marker —
once it reads under the current key, the fast path short-circuits every
later startup — so a sweep that errored or was interrupted left the
remaining legacy-key sessions stranded permanently with no retry.

Sweep first, commit the account last, and return False on sweep failure
so the migration is retried on the next start.

Also corrects the unreadable-row log: it claimed rows were being dropped
while no DELETE was ever issued. Such rows are left in place (already
unusable; deleting crypto material on a guess is not worth it) and the
message now says so.

Adds session-sweep coverage, which was previously absent: rows rewritten
under the current key, rows already current left alone, unreadable rows
left in place, and a failed sweep that leaves the account uncommitted.
The existing migration test now fakes the olm C-extension so the suite
no longer requires libolm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:03:35 +05:30
ckaznocha 3a43422065 fix(matrix): migrate crypto store when the Olm pickle key changes
The Olm account pickle key is derived from the account ID plus the
configured device ID (acct:device_id). If the crypto store's account was
created before MATRIX_DEVICE_ID was set — e.g. the very first password
login, where the device ID is only known after connecting — it gets
pickled under "<acct>:default". Setting MATRIX_DEVICE_ID afterwards (a
reasonable thing to do once you know the device ID you want to pin)
changes the derived pickle key, and every subsequent unpickle attempt
fails with BAD_ACCOUNT_KEY. In optional-E2EE mode that failure is
swallowed and encryption silently stays disabled instead of surfacing an
actionable error.

_migrate_legacy_crypto_pickle() detects the BAD_ACCOUNT_KEY failure,
tries the known legacy pickle keys, and re-pickles the account (plus
every stored olm/megolm session — sessions share the same pickle key, so
migrating only the account would leave them unreadable on the next
decrypt and silently break key sharing with peers) under the current
key. It only reports failure when no known key can unpickle the
account, with a log message pointing at what changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 10:03:35 +05:30
ckaznocha 907bdc7856 fix(matrix): let the token's own device win over a stale MATRIX_DEVICE_ID
connect() resolved client.device_id as `self._device_id or resolved_device_id`,
so a configured MATRIX_DEVICE_ID masked the live whoami() device. With
persisted A, configured A and a rotated token reporting B, the reset
compared A to A and never fired — exactly the token-rotation case this PR
claims to handle.

An access token is bound to one device and the homeserver only accepts key
uploads for that device, so a configured value naming a different one
cannot work. The live whoami() device now wins on conflict and logs an
error naming both. The configured value is still preferred when whoami()
reports no device.

Adds a connect()-level regression for persisted A + configured A +
whoami B, and corrects test_connect_uses_configured_device_id_over_whoami,
whose stated premise this inverts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:03:35 +05:30
ckaznocha a4b686c29e fix(matrix): reset Olm crypto store when the access token's device ID changes
The crypto store is keyed by Matrix user ID, not device ID, so swapping in
a new access token (which mints a new device_id) silently inherits the
previous device's Olm account. That account's identity keys can never be
published under the new device ID, and the pickle key embeds the old
device ID anyway — the result is stale-key mismatches and cross-signing
signatures the homeserver refuses to replace, degrading E2EE in ways that
are hard to diagnose (peers silently withhold room keys).

_reset_crypto_store_if_device_changed() compares the store's persisted
device ID against the live one at connect time and wipes the store on
mismatch, so a fresh Olm account is generated for the new device instead
of reusing stale key material.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 10:03:35 +05:30
webtecnica 799102a4d8 fix(matrix): fallback to homeserver query for room encryption detection (#71067)
_CryptoStateStore.get_encryption_info only consulted mautrix's in-memory
MemoryStateStore, which has no record of m.room.encryption for rooms the
bot joined in the past (the raw-sync path never feeds those state events
through set_encryption_info). On a fresh crypto store this returns None
for all previously-joined rooms, so OlmMachine reports them as unencrypted,
never tracks peer devices, and silently drops all inbound messages.

Fix: pass the mautrix Client into _CryptoStateStore so get_encryption_info
can fall back to a live GET /_matrix/client/v3/rooms/{room_id}/state/
m.room.encryption query when the in-memory store returns None. The result
is cached back via set_encryption_info so subsequent lookups (and
OlmMachine device tracking) hit the fast path.
2026-08-03 10:03:35 +05:30
Teknium 81c7e5de48 docs(a2a): website docs page + canonical agent-card.json path in prose
- New website/docs/user-guide/messaging/a2a.md: when/where to use A2A
  (cross-machine, specialist peers, being callable) vs delegation/kanban
  for same-machine multi-agent; enable, outbound tools, inbound surface,
  security model, env reference, quick test, troubleshooting. Registered
  in sidebars.ts and the messaging index.
- README/DESIGN/plugin.yaml/protocol.py prose updated to name the A2A
  v1.0 canonical discovery path /.well-known/agent-card.json (the code
  already served both; only the docs lagged).
2026-08-02 15:10:15 -07:00
Ben Kamholtz 3271cb6907 fix(a2a): override authorization_is_upstream for A2A peers
A2A authenticates every inbound request via bearer token in do_POST
(401 before dispatch). Without overriding authorization_is_upstream=True,
the gateway's per-platform user allow-list ({PLATFORM}_ALLOWED_USERS)
rejects A2A peers because their identity is a token-derived name or
pod IP, not a platform account in any configured allow-list. Messages
never reach the agent and callers get empty replies.

This is authorization delegated to the bearer-token transport, not a
fail-open: every request is 401'd if the credential is wrong.

Reported by kuangmi-bit (PR #41711 comment, Jun 27).
Attribution: gfdsa's a2a-hermes repro fixture (LOCAL PATCH triad-hermoperator).

The other two patches from gfdsa's fixture were already in our branch:
- k8s Agent Card URL derivation (_request_public_url, commit ea59b85c2)
- send() gating on metadata['notify'] (reply-capture fix, commit ea59b85c2)

All 168 tests pass (151 unit + 17 integration).
2026-08-02 15:10:15 -07:00
Ben Kamholtz 5a8102d71c fix(a2a): JSON-RPC conformance for a2a-sdk 1.1.0 compatibility
Two bugs reported by gfdsa (PR #41711 comment, Jul 12) that break the
official a2a-sdk 1.1.0 Python client:

1. Task objects serialized non-spec createdAt/lastModified fields.
   The A2A v1.0 Task proto (lf.a2a.v1.Task) only has id, contextId,
   status, artifacts, history, metadata. Strict ProtoJSON parsers
   reject unknown fields with ParseError. Removed both fields from
   build_task(); created_at param kept for call-site compatibility.

2. SSE streaming frames were not JSON-RPC wrapped. A2A v1.0 §9.4
   requires data: {"jsonrpc":"2.0","id":...,"result":{StreamResponse}}.
   sse_data() now accepts req_id and wraps in JSON-RPC envelope.
   sse_done() changed from 'data: {}' to SSE comment ': done' so
   SDK doesn't try to parse an empty JSON-RPC response.

All call sites in adapter.py (_emit_terminal, _rpc_message_stream,
_rpc_tasks_subscribe) updated to thread req_id through.

Tests updated: 153 pass (151 unit + 17 integration, including 2 new
tests for JSON-RPC envelope wrapping and fallback behavior).

Refs: gfdsa/a2a-hermes reproduction repo
2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot) b1819ceb7d fix(a2a): align multiplexer with v1 protocol and tenant isolation 2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot) fe1aca5770 feat(a2a): file/data Parts + push config full CRUD
## File/data Parts (v1.0 unified Part)
- file_part(url=, raw=, filename=, media_type=) builds v1.0 file Parts
- data_part(data, media_type=) builds v1.0 data Parts
- message_with_parts(role, parts, context_id=) builds Messages with mixed Part types
- extract_text now renders file/data Parts into the text stream:
  - File with URL: '[file: name] https://url (mediaType)'
  - File with raw: '[file: name] N bytes base64-encoded (mediaType)'
  - Data: '[data (mediaType)]\n{json}'
  - v0.3 file (file.fileWithUri) and data (kind=data) still accepted
- Outbound replies stay text-only (agent produces text)

## Push notification config full CRUD
- get_push_config(task_id, config_id) — retrieve by task, optionally by configId
- list_push_configs(task_id) — list all configs for a task (max 1 per task)
- delete_push_config(task_id, config_id) — remove a config
- New JSON-RPC methods: tasks/pushNotificationConfig/get, /list, /delete
- New adapter handlers: _rpc_push_config_get, _list, _delete
- All return spec-shaped PushNotificationConfig with configId + createdAt

## Tests
- 6 new unit tests for Part builders + extract_text with file/data
- 13 new unit tests for push config get/list/delete (happy + error paths)
- 2 new integration tests over real HTTP:
  - test_mixed_parts_delivered_to_agent: file URL + data JSON reach agent
  - test_push_config_crud_over_http: full create→get→list→delete cycle
- Old test_extract_text_skips_non_text_parts replaced (now renders, not skips)

Total: 151 tests (134 unit + 17 integration), 0 failed.
DESIGN.md updated: file/data Parts and push config CRUD removed from
out-of-scope list.
2026-08-02 15:10:15 -07:00