Skip to content

chore: sync with upstream main (2026-05-22)#41

Merged
bot-ted merged 2260 commits into
mainfrom
sync/upstream-20260522
May 22, 2026
Merged

chore: sync with upstream main (2026-05-22)#41
bot-ted merged 2260 commits into
mainfrom
sync/upstream-20260522

Conversation

@bot-ted

@bot-ted bot-ted commented May 22, 2026

Copy link
Copy Markdown
Owner

Daily sync with upstream. Auto-created by cron job.

Commits: 2259 new commits from upstream

Recent commits (first 30):
1e71b71 infographic: PR NousResearch#14157 control-plane write-deny salvage
4210421 fix(file-safety): also write-deny /control-files in profile mode
1f5219f fix(security): protect Hermes control-plane files from prompt injection
6f436a4 infographic: PR NousResearch#27784 anthropic adapter refactor salvage
9d61408 refactor: extract 7 helpers from convert_messages_to_anthropic
ec2ab5b infographic: PR NousResearch#8056 hash pairing codes salvage
82c2035 fix(pairing): handle legacy plaintext pending entries during upgrade
2e50942 fix(security): hash gateway pairing codes instead of storing plaintext
3ac2125 refactor(image_gen): port FAL backend to plugins/image_gen/fal
7dea333 infographic: PR NousResearch#30373 aux model picker parity salvage
d246f9a fix(aux-picker): drop stale session_search slot
c1e93aa fix: add missing aux model slots to model picker
8b49012 infographic: PR NousResearch#8306 webhook HMAC bypass salvage
3fc715d test(webhook): regression cases for empty-secret HMAC bypass
9c90b3a fix(security): validate secret in _reload_dynamic_routes to prevent HMAC bypass
22b0d6d test(tools): centralize disable_lazy_stt_install fixture in conftest
5dc232a test(tools): disarm lazy-install probe so _HAS_FASTER_WHISPER patches work
c25f9d1 feat(secrets): label detected credentials with their source (Bitwarden) (NousResearch#30364)
d617858 fix(openviking): target-aware mirror subdir, drop private-attr access, dedupe URI builder
2d587c5 fix(openviking): store memories via content/write API instead of session messages
caf0f30 chore(release): add sgtworkman to AUTHOR_MAP
70d53d8 fix: run computer use post-setup when enabling tool
fbdca64 fix(computer-use): skip capture_after when action failed (ok=False)
07b7cf6 chore(release): add rodrigoeqnit to AUTHOR_MAP
c52cd48 fix(computer-use): add set_value to ComputerUseBackend ABC and _NoopBackend stub
d3f62c6 fix(cli): clamp curses color 8 for 8-color terminals (Docker)
c769be3 fix(agent): recover from providers rejecting list-type tool content (NousResearch#27344) (NousResearch#30259)
372e9a1 fixup: log lazy-install errors at debug + AUTHOR_MAP for CipherFrame
b5c6d9a fix: wire STT lazy-install into transcription_tools.py
f6f25b9 fix(agent): fail fast on small Ollama runtime context

... (2224 more commits) ...

Oldest commits:
242659f fix(tui): don't hardcode /home/bb
42df7ec fix(tui): update comments
42e166c refactor(docker): drop manual @hermes/ink build, rely on esbuild bundle
279504d fix(nix): refresh npm lockfile hashes
42627b4 refactor(tui): bundle with esbuild, drop runtime node_modules

soynchux and others added 30 commits May 18, 2026 22:43
…jected

When a DM topic lane's message_thread_id is rejected by Telegram
(e.g. stale or deleted topic), send_typing now falls back to sending
the typing indicator without thread_id so it at least appears in the
main DM view, rather than being silently swallowed.

Also adds test for the fallback behavior.
The gmail-triage skill's Telegram inline buttons emit callback_data of the
form `gt:<verb>:<arg>`, but `_handle_callback_query` had no `gt:` branch —
taps fell through silently and the spinner sat there until Telegram timed it
out.

Add `_handle_gmail_triage_callback`, dispatched from the existing callback
router, that:

- Authorizes the caller via the same `_is_callback_user_authorized` path as
  the approval / slash-confirm / clarify handlers.
- Maps each verb to a script under `~/.hermes/scripts/gmail-triage/` and runs
  it async with a 60s timeout.
- Splits verbs into one-shots (send / archive / draft / spam) — append the
  confirmation and strip the keyboard so the action can't fire twice — and
  sticky-state changes (mute / trust / vip ± -domain) — append the
  confirmation but leave the keyboard tappable so the user can stack actions
  on one email.
- On failure: toast only, keyboard preserved so the user can retry.
- Logs every callback outcome to gateway.log for debugging.
When a user sends a message on Telegram, the incoming message is now
automatically pinned at the start of processing and unpinned when the
agent finishes its turn. This gives the user a visual indicator that
their message is being worked on, and keeps the conversation anchored.

Changes:
- telegram.py: Added pinChatMessage in on_processing_start and
  unpinChatMessage in on_processing_complete. Restructured both
  hooks so pin/unpin runs independently of the reactions feature
  (reactions are optional; pinning is always on).
- telegram.py: Pass message_id through SessionSource so it's
  available in the session context.
- session_context.py: Added HERMES_SESSION_MESSAGE_ID context var.
- run.py: Pass source.message_id through set_session_vars.

Pinning is silent (disable_notification=True) and failures are
logged at debug level without interrupting message processing.
Only the user's incoming message is pinned -- never the agent's
replies. Auto-resume events (which have no message_id) are
correctly skipped.
Two coordinated changes that unblock downstream audio pipelines
(diarization, custom transcription, archival) on attachments larger
than the public Bot API's 20MB getFile ceiling.

- `stt.enabled: false` no longer drops voice/audio with a generic
  "transcription disabled" note. The gateway probes the cached file's
  duration (wave → mutagen → ffprobe ladder) and surfaces
  `[The user sent a voice message: <abs path> (duration: M:SS)]` to
  the agent so a skill or tool can pick up the raw file. The previous
  placeholder is replaced rather than appended when present.

- `platforms.telegram.extra.base_url` set → adapter auto-lifts its
  document size cap from 20MB to 2GB (the local telegram-bot-api
  `--local` ceiling) and the "too large" reply reports the active
  limit dynamically. No new config knob; presence of `base_url` is the
  opt-in.

- `platforms.telegram.extra.local_mode: true` wires
  `Application.builder().local_mode(True)` on the python-telegram-bot
  builder. PTB then reads files from disk instead of HTTP, which is
  required when telegram-bot-api runs in `--local` mode (the server
  returns absolute filesystem paths, not `/file/bot...` URLs).

- gateway/run.py: rewrites the `stt.enabled: false` branch of
  `_enrich_message_with_transcription`. New `_format_duration` +
  `_probe_audio_duration` helpers.
- gateway/platforms/telegram.py: `_max_doc_bytes` instance attribute
  derived from `extra.base_url`; `local_mode` builder wiring;
  dynamic "too large" message.
- tests/gateway/test_stt_config.py: covers path-surfacing with and
  without an existing user message, and placeholder replacement.
- tests/gateway/test_telegram_max_doc_bytes.py: 3 cases — default 20MB
  without base_url, 2GB when set, empty-string base_url keeps default.
- website/docs/user-guide/messaging/telegram.md: new "Skipping STT"
  subsection under Voice Messages and a full "Large Files (>20MB) via
  Local Bot API Server" walkthrough (api_id/api_hash, docker-compose,
  one-time `logOut` migration, `platforms.telegram.extra` config, the
  `local_mode` disk-access requirement, the silent HTTP-fallback 404).
- website/docs/user-guide/features/voice-mode.md: documents the
  `stt.enabled` knob in the config reference.

- `pytest tests/gateway/test_telegram_max_doc_bytes.py
  tests/gateway/test_stt_config.py` → 9/9 passing.
- Verified end-to-end on a live deployment: gateway log shows
  `Using custom Telegram base_url: http://...` and
  `Using Telegram local_mode (read files from disk)` on startup;
  voice messages above 20MB cache to disk and surface their path to
  the agent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#28549)

columnLabels and columnHelp in en.ts include a scheduled entry but the
Translations interface in types.ts did not declare it, causing a
TypeScript build failure in the Nix derivation. Made the field optional
since only en.ts provides it currently.
…earch#28497)

Catch the website docs up to two weeks of merged work (May 4 – May 18, 2026,
roughly 1,080 PRs). The audit found ~50 user-visible features that had landed
in code with no docs footprint, plus a handful of stale pages. This PR closes
every gap the scan turned up.

New pages
- user-guide/features/deliverable-mode.md — extension list, agent triggers,
  kanban_complete artifacts pattern, [[as_document]] override (PR NousResearch#27813).
- developer-guide/web-search-provider-plugin.md — authoring guide modeled on
  image-gen-provider-plugin, covering brave_free / ddgs / etc. (PR NousResearch#25448).

Providers / auth
- Rename "Alibaba Cloud" → "Qwen Cloud (Alibaba DashScope)" everywhere the
  display label shows up; provider id stays `alibaba` (PR NousResearch#24835).
- Document OAuth refresh-token quarantine for xAI / MiniMax / Codex (PRs
  NousResearch#28116 / NousResearch#28118 / NousResearch#28119).
- Document Nous JWT minting from refresh token + invalid-refresh quarantine
  + cross-profile shared token store (PRs NousResearch#27663 / NousResearch#19712).
- Add `## Microsoft Entra ID authentication (keyless)` section to
  azure-foundry guide — DefaultAzureCredential, RBAC, OpenAI + Anthropic
  routing details (PR NousResearch#28101 / #9df9816da).
- Custom providers `api_mode` is now prompted-and-persisted, not just URL
  autodetected (PR NousResearch#25068).
- Delegation honours `api_mode` + auto-detects anthropic_messages base URLs
  (PR NousResearch#26824).
- `x_search` auto-enables when xAI credentials are present (PR NousResearch#27376).
- Add `xAI Grok OAuth (SuperGrok)` row to providers headline table (PR
  NousResearch#26534).
- NVIDIA NIM billing-origin header is set automatically (PR NousResearch#26585).

Windows / installer
- `install.ps1`: document `-Commit <sha>` and `-Tag <v>` pin params plus
  the BOM-strip / git-retry hardening (PR NousResearch#28169).
- Document Hermes Desktop thin installer + first-launch bootstrap (PR
  NousResearch#27822).
- Document `dep_ensure` Windows bootstrap (PR NousResearch#27845).
- Document install-method auto-detection (pip / git / homebrew / nixos) and
  the matching update command (PR NousResearch#27843).

Gateway / messaging
- `/platform list|pause|resume` full description + circuit-breaker
  semantics (PR NousResearch#26600).
- Slack / Matrix / Mattermost get parallel `allowed_channels` /
  `allowed_rooms` allowlist sections matching Telegram/Discord/DingTalk
  (PR NousResearch#21251).
- Discord `allow_any_attachment` + `max_attachment_bytes` (config and env
  vars) (PR NousResearch#27245).
- Discord clarify-choice button rendering (PR NousResearch#25485).
- Telegram `guest_mode` @mention bypass for allowlisted groups (PR
  NousResearch#22759).
- Telegram `notifications` mode (`important` vs `all`) (PR NousResearch#22793).
- `[[as_document]]` skill / response directive for forcing
  document-style media delivery (PR NousResearch#21210).

CLI / TUI
- `/new [name]` argument (PR NousResearch#19637).
- `/subgoal` user-supplied criteria appended to `/goal` (PR NousResearch#25449).
- `/exit --delete` flag confirmation prompts for destructive slash
  commands (PR NousResearch#22687).
- Status-bar additions: ▶ N background indicator (PR NousResearch#27175), context
  compression count (PR NousResearch#21218), YOLO mode banner+statusbar warning (PR
  NousResearch#26238).
- `display.timestamps` + `docker_extra_args` config keys (PR NousResearch#23599).
- TUI collapsible startup banner sections (PR NousResearch#20625).
- `HERMES_SESSION_ID` exported to tool subprocesses (PR NousResearch#23847).

i18n
- Refresh display.language locale list from 8 → 16 (en, zh, zh-hant, ja,
  de, es, fr, tr, uk, af, ko, it, ga, pt, ru, hu) — matches
  `agent/i18n.py:SUPPORTED_LANGUAGES`.

Tools / features
- `vision_analyze` native-pixel passthrough for vision-capable callers,
  with auxiliary text-describer fallback (PR NousResearch#22955).
- `session_search` rewrite to the single-shape tool (discovery / scroll /
  browse modes) (PRs NousResearch#27590 / NousResearch#27840).
- Clarify MCP transport scope: client supports stdio + SSE; embedded
  `hermes mcp serve` is stdio-only (PR NousResearch#21227).
- Web search backends table: add Brave Search (free tier) and DDGS rows
  (PR NousResearch#21337).
- ACP session-scoped edit auto-approval modes (PR NousResearch#27862).
- Curator rename map in the user-visible per-run summary (PR NousResearch#22910).
- Prompt caching feature page reference in features/overview.md — Claude
  cross-session 1-hour prefix cache on native Anthropic / OpenRouter /
  Nous Portal (PR NousResearch#23828).
- Cron per-job profile parameter (PR NousResearch#28124).
- `--no-skills` flag for `hermes profile create` (PR NousResearch#20986).

Build
- Verified with `npm run build` in `website/`; both `en` and `zh-Hans`
  locales compile. Remaining broken-link/anchor warnings are pre-existing
  (`rl-training.md` from learning-path / overview; the
  zh-Hans translation lag the docs skill already calls out).
…ousResearch#28571)

Pre-stages AUTHOR_MAP entries for 9 new/under-mapped contributors whose
PRs are being salvaged in the May 2026 LHF batch group 9.

Contributors:
- jdelmerico (NousResearch#28278 — signal require_mention filter)
- justemu (NousResearch#27996 — matrix thread_require_mention)
- YuanHanzhong (NousResearch#28029 — dashboard browser scrollback)
- noctilust (NousResearch#28080 — drop stale TUI resume env)
- MoonJuhan (NousResearch#28288 — tolerate unreadable JSONL transcripts)
- outsourc-e (NousResearch#28164 — cron emoji ZWJ sequences)
- Zyrixtrex (NousResearch#28275 — Google OAuth urlopen timeout)
- ooovenenoso (NousResearch#28256 — tool loop recovery hints)
- vanthinh6886 (NousResearch#28018 — yaml/flock/atomic write guards; non-noreply email)

Per references/batch-pr-salvage-may14-additions.md.
Add a configurable mention filter to the Signal adapter so the bot
only responds in groups when it is explicitly @mentioned.

Changes:
- gateway/platforms/signal.py: read require_mention from adapter
  extra config or SIGNAL_REQUIRE_MENTION env var; skip group messages
  that don't mention the bot account (checked in rendered text and
  raw mention metadata)
- gateway/config.py: map signal.require_mention YAML key to the
  SIGNAL_REQUIRE_MENTION env var (env var takes precedence)

Config example:
  signal:
    require_mention: true

Or via env var:
  SIGNAL_REQUIRE_MENTION=true
Tranquil-Flow and others added 27 commits May 21, 2026 23:40
curses.init_pair(N, 8, -1) uses extended color 8 ("bright black" /
dim gray) which does not exist on 8-color terminals (COLORS == 8,
valid range 0-7).  This crashes the entire plugins UI, session
browser, and radio picker in Docker containers with:

    curses.error: init_pair() : color number is greater than COLORS-1

Replace all 5 occurrences across plugins_cmd.py, main.py, and
curses_ui.py with min(8, curses.COLORS - 1), which falls back to
COLOR_WHITE (7) on 8-color terminals.

Closes NousResearch#13688
…ackend stub

_dispatch() routes action="set_value" to backend.set_value(), but:
- ComputerUseBackend did not declare set_value as @AbstractMethod, so
  subclasses could silently omit it without a TypeError at class load time.
- _NoopBackend (the test/CI stub) had no set_value method at all, causing
  AttributeError in any test that exercises the set_value action path.

Fix:
- Add set_value as @AbstractMethod to ComputerUseBackend in backend.py.
- Add a recording stub in _NoopBackend in tool.py.
- Add two TestDispatch cases: one verifying the call reaches the backend,
  one verifying the missing-value guard returns a clean error.
_maybe_follow_capture() issued a follow-up screenshot unconditionally
when capture_after=True, even when res.ok=False. The model then received
a normal-looking screenshot alongside an error message, and in practice
it often ignored ok=False and proceeded as if the action had succeeded.

Fix: return _text_response(res) early when res.ok is False so the model
receives only the error and can decide how to recover.

Tests added:
- test_capture_after_skipped_when_action_failed: patches click to return
  ok=False and asserts no capture call is issued.
- test_capture_after_fires_when_action_succeeds: ensures the happy path
  still triggers the follow-up capture.
…ion messages

_tool_remember and on_memory_write were posting memories as session
messages that depend on commit-time VLM extraction to persist. With
extraction_enabled: false (no VLM configured), the extraction pipeline
never processes these messages, causing memories to be silently lost.

Replace both paths with direct POST to /api/v1/content/write?mode=create,
which creates the file, stores the content, and queues vector indexing
in a single API call. Error reporting is immediate — no silent failures.

- Maps viking_remember category to viking:// subdirectory
- Generates UUID-based URIs via uuid4().hex[:12]
- Returns byte count in confirmation message
…, dedupe URI builder

- on_memory_write: map target='memory' -> patterns/, 'user' -> preferences/
  (was hardcoded to preferences/ for both)
- Replace client._user with self._user (no private-attr leakage)
- Extract _build_memory_uri() helper + module-level subdir maps
- Restore on_memory_write signature parity with MemoryProvider base
  (metadata kwarg; eliminates Pyright incompatible-override warning)
- AUTHOR_MAP entry for chrisdlc119@outlook.com
…n) (NousResearch#30364)

When Bitwarden Secrets Manager supplies a provider key, 'hermes model'
and the setup wizard show 'credentials ✓' with no hint of where the
key came from — identical to the .env case. Users assume the integration
isn't wired up and re-enter the key (or hit Enter and cancel).

env_loader now tracks which env vars were injected by an external secret
source and exposes get_secret_source() / format_secret_source_suffix() so
the provider flows can render 'Anthropic credentials: sk-ant-... ✓
(from Bitwarden)' instead of an unlabeled checkmark.

Wired into _prompt_api_key (kimi, z.ai, minimax, opencode, ...), the
Anthropic provider flow, the Bedrock flow, and the GitHub Copilot token
display.

Future secret sources (Vault, 1Password, etc.) drop in by setting their
own label in _SECRET_SOURCES; format_secret_source_suffix() has a generic
fallback so no call sites need updating.
… work

`b5c6d9ac0` ("fix: wire STT lazy-install into transcription_tools.py")
added `_try_lazy_install_stt()`, which calls
`importlib.util.find_spec("faster_whisper")` after `ensure()` runs.
In the dev / CI environment `faster_whisper` is already installed, so
the probe returns truthy and `_get_provider()` returns "local" even
when the test has patched `_HAS_FASTER_WHISPER=False` to simulate
"not installed".

Add a per-file autouse fixture that patches `_try_lazy_install_stt`
to return False so the simulation stays accurate. The 16 baseline
failures across `test_transcription_tools.py`,
`test_transcription.py`, and `test_transcription_dotenv_fallback.py`
disappear; the production lazy-install path is unaffected at runtime.
Move the autouse `_disable_lazy_stt_install` fixture out of the three
transcription test files and into `tests/tools/conftest.py` as a regular
(non-autouse) fixture. Each transcription test module opts in once at
the top via `pytestmark = pytest.mark.usefixtures(...)`.

Why: addresses three Copilot inline review comments on this PR that
flagged the verbatim duplication across files. Centralizing also keeps
the patch target in a single place, so a future rename of
`_try_lazy_install_stt` only updates one location.

Why opt-in (not autouse in conftest): other `tests/tools/` files do not
patch `_HAS_FASTER_WHISPER` and have no reason to bypass the runtime
lazy-install probe; making the fixture autouse globally would silently
mask any future test that wants to exercise the real lazy-install path.
Covers _reload_dynamic_routes() rejecting empty or missing per-route
secrets when no global fallback exists, preserving the INSECURE_NO_AUTH
opt-in, inheriting a global secret when only the per-route value is
missing, and partial-skip when only one of multiple routes is bad.
triage_specifier, kanban_decomposer, profile_describer exist in
DEFAULT_CONFIG auxiliary section but weren't in _AUX_TASK_SLOTS,
_AUX_TASKS, or the dashboard AUX_TASKS array — so users couldn't
configure them through hermes model or the web dashboard.

9â\x86\x9212 aux slots across all three UI surfaces.
PR NousResearch#27590 removed auxiliary.session_search from DEFAULT_CONFIG (single-shape
tool now returns DB content directly without an aux LLM), but the slot
remained in _AUX_TASK_SLOTS (web_server.py) and AUX_TASKS (ModelsPage.tsx).
Removing the dead entries while we're touching these tables.
Mirrors the architecture established by the web (NousResearch#25182), browser
(NousResearch#25214), and video_gen (NousResearch#25126) plugin migrations:

* `tools/fal_common.py` — stateless atoms shared by both FAL-backed
  plugins (image_gen + video_gen). Holds the lazy `fal_client` import
  helper, `_ManagedFalSyncClient`, `_normalize_fal_queue_url_format`,
  `_extract_http_status`. Stateful pieces (`fal_client` module global,
  `_managed_fal_client*` cache, `_submit_fal_request`,
  `_resolve_managed_fal_gateway`, `_get_managed_fal_client`)
  intentionally stay on `tools.image_generation_tool` so the existing
  `monkeypatch.setattr(image_tool, ...)` patch sites keep working
  unchanged.

* `plugins/video_gen/fal/__init__.py` — drops its inline
  `_load_fal_client` duplicate; consumes `tools.fal_common.import_fal_client`.

* `plugins/image_gen/fal/{plugin.yaml,__init__.py}` — new plugin.
  `FalImageGenProvider` is a thin registration adapter that resolves
  the legacy module via `import tools.image_generation_tool as _it`
  and calls `_it.image_generate_tool` + `_it._resolve_fal_model` at
  call time. The 18-model catalog, `_build_fal_payload`, managed-
  gateway selection, and Clarity Upscaler chaining all remain in
  `tools.image_generation_tool` as the single source of truth —
  the plugin is a registration adapter, not a parallel implementation.

* `tools/image_generation_tool.py::_dispatch_to_plugin_provider` —
  drops the `configured == "fal"` skip. Setting `image_gen.provider:
  fal` now routes through the registry like any other provider; the
  plugin re-enters this module's pipeline so behavior is identical.
  Unset `image_gen.provider` still falls through to the in-tree
  pipeline (preserves no-config-with-FAL_KEY UX from NousResearch#15696).

* `hermes_cli/tools_config.py` — drops the hardcoded "FAL.ai" row from
  `TOOL_CATEGORIES["image_gen"]["providers"]` (now injected by
  `_plugin_image_gen_providers` like every other backend) and the
  `getattr(provider, "name") == "fal"` skip that protected against
  duplication with the hardcoded row. The "Nous Subscription" row
  stays as a setup-flow entry — same shape browser kept "Nous
  Subscription (Browser Use cloud)" after NousResearch#25214.

* `tests/plugins/image_gen/test_fal_provider.py` — 14 cases covering
  the ABC surface, call-time indirection (verifying
  `monkeypatch.setattr(image_tool, "image_generate_tool", ...)` takes
  effect through the plugin), response-shape stamping, exception
  handling, and registry wiring.

* `tests/plugins/image_gen/check_parity_vs_main.py` — subprocess
  harness mirroring `tests/plugins/browser/check_parity_vs_main.py`.
  Pins one path to origin/main, one to the worktree; runs six
  scenarios (unset, explicit-fal-no-creds, explicit-fal-with-creds,
  explicit-fal-with-model, typo provider, managed-gateway-only) and
  diffs the reduced shape `{dispatch_kind, provider_name, model}`
  per scenario. The only acceptable diff is "legacy_fal → plugin
  (fal)" for explicit-FAL paths — every other delta is flagged as
  a regression.

* `tests/hermes_cli/test_image_gen_picker.py::test_fal_surfaced_alongside_other_plugins`
  — flips the previous `test_fal_skipped_to_avoid_duplicate` to
  match the new shape (FAL is a plugin now, no dedup needed).

Verified: 195/195 tests across
`tests/{tools/test_image_generation*,tools/test_managed_media_gateways,plugins/image_gen,plugins/video_gen,hermes_cli/test_image_gen_picker}.py`
pass on this branch with no test patches modified outside the picker
test that asserted the old skip behaviour.

Fixes NousResearch#26241
Pairing codes were stored as plaintext keys in JSON files. Now uses
sha256 + random salt hashing with constant-time comparison.

Fixes NousResearch#8036

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an existing install upgrades to the hashed-pending schema, its
on-disk pending.json still has the old {code: entry} format with no
hash/salt fields. The original PR NousResearch#8056 assumed every entry had both
fields and would have KeyErrored in approve_code, list_pending, and
_cleanup_expired.

Guard each consumer:
  - approve_code: skip entries that are not a dict, lack salt/hash,
    or have a non-hex salt. Legacy entries simply fail to match.
  - list_pending: tolerate missing 'hash' (show "legacy" placeholder)
    and non-numeric created_at (skip the row).
  - _cleanup_expired: treat malformed/legacy entries as expired so
    they get pruned on the next call rather than wedging the file.

Regression tests cover all three consumers plus a mixed-malformed
case.
Split convert_messages_to_anthropic (complexity 79) into 7 focused helpers:

- _convert_assistant_message    — assistant msg to content blocks
- _convert_tool_message_to_result — tool msg to tool_result + merge
- _convert_user_message         — user msg validation + conversion
- _strip_orphaned_tool_blocks   — orphan tool_use + tool_result removal
- _merge_consecutive_roles      — role alternation enforcement
- _manage_thinking_signatures   — strip/preserve/downgrade by endpoint
- _evict_old_screenshots        — keep only 3 most recent images

Main function complexity: 79 → 10 (below C901 threshold).
Zero logic changes — pure extraction. Net -4 lines (refactor itself);
+45/-17 follow-up polish for annotation tightening (List[Dict] →
List[Dict[str, Any]]), restored rationale comments in
_manage_thinking_signatures (third-party endpoint examples, NousResearch#13848/NousResearch#16748
issue refs, redacted_thinking 'data'-as-signature note), and "Mutates
``result`` in place." docstring lines on the four mutating helpers.
Adds active-HERMES_HOME control-plane files to the write deny list:
auth.json, config.yaml, webhook_subscriptions.json, and any path
under mcp-tokens/. realpath() resolves before comparison so
directory-traversal and symlink targets are normalised, preventing
trivial deny-list bypass via ../ tricks.

Without this, a prompt-injected agent could rewrite Hermes' own
auth state or routing config via write_file / patch — without
triggering the terminal dangerous-command approval — and persist
attacker-controlled behaviour across sessions.

Fixes NousResearch#14072
PR NousResearch#14157 added control-plane write-deny against the ACTIVE HERMES_HOME,
which is fine in non-profile mode but leaves a gap once a profile is
active: HERMES_HOME points at <root>/profiles/<name>, so the global
<root>/auth.json + <root>/config.yaml + <root>/webhook_subscriptions.json
+ <root>/mcp-tokens/ remain writable. Same shape as the .env gap PR
NousResearch#15981 closed via _hermes_root_path().

Apply the same widening pattern here. The control-file/mcp-tokens check
now iterates BOTH _hermes_home_path() and _hermes_root_path() (dedupes
when they coincide in non-profile mode). Also tightens the mcp-tokens
check from "startswith dir + os.sep" to "==dir OR startswith dir + os.sep"
so writing the directory entry itself is blocked, not just files inside.

Regression tests cover both protections in a real profile-mode layout
(<tmp>/hermes/profiles/coder as HERMES_HOME, <tmp>/hermes as root).
Sync with upstream NousResearch/hermes-agent main.
Auto-resolved merge conflicts by accepting upstream version.
@github-actions

Copy link
Copy Markdown

🔎 Lint report: sync/upstream-20260522 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9023 on HEAD, 9000 on base (🆕 +23)

🆕 New issues (40):

Rule Count
unsupported-operator 14
unresolved-import 7
unresolved-attribute 7
invalid-argument-type 4
invalid-assignment 4
invalid-return-type 1
call-non-callable 1
unused-type-ignore-comment 1
not-subscriptable 1
First entries
tests/cli/test_fast_command.py:480: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/tools/test_computer_use_vision_routing.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/tools_config.py:2654: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `dict[Unknown, Unknown]` and `str | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[Unknown]] | dict[str, str | list[dict[str, str]]]] | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[dict[str, str]]]] | ... omitted 5 union elements`
tests/plugins/image_gen/test_fal_provider.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_kanban_core_functionality.py:3364: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/hermes_cli/test_mcp_reload_confirm_gate.py:34: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
hermes_cli/config.py:3291: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `tuple[int, int]`, found `tuple[Any, str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements]`
tests/test_bitwarden_secrets.py:24: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/run_agent/test_multimodal_tool_content_recovery.py:28: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/config.py:3928: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/agent/test_curator.py:855: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/tools/test_browser_console.py:265: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements`
tests/tools/test_computer_use_capture_routing.py:31: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/test_bitwarden_secrets.py:434: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["bad token"]` and `str | None`
tools/computer_use/cua_backend.py:685: [invalid-assignment] invalid-assignment: Object of type `Any | int` is not assignable to attribute `_last_app` of type `str | None`
tests/gateway/test_whatsapp_reply_prefix.py:121: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 28 union elements` and `int`
gateway/pairing.py:277: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `None | (Unknown & Top[dict[Unknown, Unknown]])`
tests/hermes_cli/test_curses_color_compat.py:17: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/test_bitwarden_secrets.py:348: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["project_id"]` and `str | None`
tests/tools/test_computer_use.py:1111: [invalid-assignment] invalid-assignment: Object of type `TrackingBackend` is not assignable to attribute `_backend` of type `ComputerUseBackend | None`
hermes_cli/runtime_provider.py:607: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["extra_body"]` and value of type `dict[Unknown, Unknown]` on object of type `dict[str, str]`
... and 15 more

✅ Fixed issues (23):

Rule Count
unsupported-operator 11
unresolved-attribute 7
invalid-argument-type 2
invalid-method-override 1
invalid-return-type 1
call-non-callable 1
First entries
tests/cli/test_resume_display.py:648: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/gateway/test_whatsapp_reply_prefix.py:121: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements` and `int`
tests/hermes_cli/test_aux_config.py:37: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["title_generation"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
hermes_cli/tools_config.py:2598: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `dict[Unknown, Unknown]` and `str | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[Unknown]] | dict[str, str | list[dict[str, str]]]] | list[dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[dict[str, str]]]] | ... omitted 4 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
hermes_cli/config.py:3883: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/hermes_cli/test_kanban_core_functionality.py:3364: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
plugins/memory/openviking/__init__.py:610: [invalid-method-override] invalid-method-override: Invalid override of method `on_memory_write`: Definition is incompatible with `MemoryProvider.on_memory_write`
tests/tools/test_web_providers.py:229: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["extract_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
hermes_cli/config.py:3256: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `tuple[int, int]`, found `tuple[Any, str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements]`
tests/hermes_cli/test_mcp_reload_confirm_gate.py:34: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/agent/test_curator.py:855: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/tools/test_web_providers.py:228: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
hermes_cli/config.py:3893: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/tools/test_web_providers.py:227: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/tools/test_browser_console.py:265: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tools/computer_use/tool.py:394: [unresolved-attribute] unresolved-attribute: Object of type `ComputerUseBackend` has no attribute `set_value`
tests/cli/test_fast_command.py:480: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tools/image_generation_tool.py:443: [call-non-callable] call-non-callable: Object of type `None` is not callable
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 27 union elements`

Unchanged: 4732 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@bot-ted bot-ted merged commit f506ebd into main May 22, 2026
15 of 19 checks passed
@bot-ted bot-ted deleted the sync/upstream-20260522 branch May 22, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.