Commit graph

17625 commits

Author SHA1 Message Date
webtecnica
2a32fe8914 fix(macos): use launchctl submit instead of start_new_session for plist reload helper (#69098)
The deferred launchd reload helper used start_new_session=True to detach
from the gateway's process group. However, setsid(2) alone does NOT move
the child outside the launchd job's process coalition — when launchctl
bootout fires on the gateway label, launchd terminates ALL processes in
that coalition, including the setsid-detached helper, leaving the service
permanently unloaded.

Fix by spawning the helper via launchctl submit, which creates a
transient launchd one-shot job that is wholly independent of the
gateway's coalition. This ensures the helper survives bootout and can
complete the bootstrap+verify cycle.

Also writes a durable pre-bootout marker to the reload log so the
distinction between 'helper never started' and 'helper ran but
bootout/bootstrap failed' can be diagnosed.

Fixes #69098
2026-07-24 15:49:29 -07:00
teknium1
b16e2be88f chore: map contributor akb4q 2026-07-24 15:48:23 -07:00
akb4q
835de6f764 test(compaction): byte-pin every frozen prefix generation
Hardening follow-up to the #69619 review fix. The previous regression
byte-pinned only the rescued pre-#69619 generation; older frozen entries
were covered solely by fragment assertions and a self-matching loop that
cannot detect a frozen entry mutating (the loop tests each entry against
itself).

- Pin all four _HISTORICAL_SUMMARY_PREFIXES generations as literals in
  _FROZEN_PREFIX_GENERATIONS and assert order-sensitive tuple equality
  plus detect/strip for each
- State the prepend-only contract explicitly on the tuple: never mutate
  or reorder existing entries

Negative controls verified: mutating, dropping, or reordering a frozen
entry each fail the new test, while the legacy self-matching loop still
passes under mutation — confirming the closed coverage gap.
2026-07-24 15:48:23 -07:00
akb4q
8204b27618 fix(compaction): freeze pre-change SUMMARY_PREFIX generation, restore mutated entry
Address review on #69619: the previous commit mutated the newest frozen
entry in _HISTORICAL_SUMMARY_PREFIXES and never froze the live prefix it
retired (the generation with both the four-heading discard clause and
the tools-active clause). A summary persisted immediately before
upgrading was therefore treated as an ordinary message on
resume/re-compaction, keeping the old handoff text embedded in the body.

- Prepend the exact pre-change live prefix as a new frozen entry
  (newest-first), leaving all existing frozen entries byte-identical
- Restore the Jul 2026 (#65848 class) frozen entry to its original
  four-heading text
- Pin the retired generation as a literal in
  test_summary_prefix_semantics.py so mutating or dropping it fails CI
- Make the #65848 tool-use regression position-agnostic (match the
  pre-clause generation by content, not tuple index)

Verified byte-identity of both rescued generations against the parent
commit. 233 focused prefix/resume/compressor tests pass.
2026-07-24 15:48:23 -07:00
akb4q
b59cce9178 fix(compaction): strip proactive section headers from summary template
Remove three directive-heavy section headers from both the LLM
and deterministic summary templates that caused the agent to
resume stale tasks after context compression:

- Historical In-Progress State
- Historical Pending User Asks
- Historical Remaining Work

These sections read as actionable instructions even within a
REFERENCE-ONLY wrapper, hijacking the user's latest message.
The remaining sections are purely descriptive/past-tense.

Frozen prefix copies in _HISTORICAL_SUMMARY_PREFIXES updated
to match. Test 8/8 passed.
2026-07-24 15:48:23 -07:00
teknium1
0fb0ba475d fix(windows): platform._syscmd_ver stub in bootstrap + PYTHONUTF8 in desktop backend env
Two gaps found auditing the decode-crash cluster:

1. suppress_platform_ver_console() only ran in hermes_cli.main processes;
   slash workers, tui_gateway/entry, run_agent, batch_runner, and cli.py
   import only hermes_bootstrap and were exposed to both the console
   flash and (on Python 3.11.0/3.11.1, which lack CPython's
   encoding='locale' fix) a UnicodeDecodeError inside platform.win32_ver()
   under PEP 540 — the crash #69413 reported. Move the stub into
   hermes_bootstrap so every entry point gets it; the _subprocess_compat
   copy stays for non-bootstrap callers.

2. The desktop Electron spawn built the backend env without PYTHONUTF8,
   so anything the Python child emitted before hermes_bootstrap ran
   (interpreter startup errors, pre-bootstrap tracebacks) decoded with
   the locale default. Re-port of PR #56499's env half (echoriver89) to
   backend-env.ts (original targeted the deleted backend-env.cjs);
   explicit user setting wins.
2026-07-24 15:47:12 -07:00
Icather
8516324f84 fix(tools): utf-8 decode for STT/TTS command-provider popen_kwargs
Salvaged from PR #45099 — the two popen_kwargs dict sites the #70875
AST sweep missed because the kwargs are built indirectly
(_run_command_stt, _run_command_tts).
2026-07-24 15:47:12 -07:00
teknium1
cc6e8fa757 fix(gateway): extend the utf-8 file-I/O guard to google_chat + whatsapp
Follow-up to the salvaged #38985: guard the 4 bare read_text/write_text
sites its allowlist missed (google_chat thread-count store + oauth JSON)
and add whatsapp/google_chat to the AST guard test's file list.
2026-07-24 15:47:12 -07:00
Rod Boev
7b8a4d74f9 fix(gateway): cover discord update-response utf-8 path (#37423) 2026-07-24 15:47:12 -07:00
Rod Boev
09910bc3a5 fix(gateway): add utf-8 encoding to dead target registry 2026-07-24 15:47:12 -07:00
Rod Boev
5b76ce169b fix(gateway): pass encoding="utf-8" to read_text/write_text in update path (#37423) 2026-07-24 15:47:12 -07:00
liuhao1024
cbb1457606 fix(mcp): use encoding_error_handler='replace' for stdio transport
On Windows, pipe I/O can deliver non-UTF-8 bytes at chunk boundaries,
causing `UnicodeDecodeError` when the MCP SDK's `TextReceiveStream`
uses `errors="strict"`. Set `encoding_error_handler="replace"` on
`StdioServerParameters` so undecodable bytes become U+FFFD instead
of crashing.
2026-07-24 15:47:12 -07:00
abundantbeing
7cd48733db feat(api): backend-acknowledged session model lock with runtime routing
Add a persisted, backend-confirmed provider/model lock for Hermes
Browser and other session API clients. A confirmed lock is an
execution contract rather than response metadata:

- POST /api/sessions/{session_id}/model validates and persists a
  confirmed browser_model_lock (advertised in /v1/capabilities)
- session chat + chat/stream consume the persisted lock on body-only
  follow-up turns; a confirmed lock wins over an older gateway session
  /model override and the session-persisted model
- a later successful session /model switch explicitly clears and
  replaces the lock while preserving lineage markers (_branched_from)
  and invalidating cached system-prompt model/provider metadata
- ordinary one-off request overrides never replace a confirmed lock
- provider-resolution failure fails closed as a typed provider-auth
  error (controlled response, never global-credential reuse)
- confirmed locks disable the global fallback model chain
- the completed agent's actual provider/model must match the locked
  route or the turn fails with a runtime-mismatch error
- responses carry sanitized runtime metadata reporting actual vs
  requested provider/model and lock state

Rebased onto the provider-aware request routing (#70853) and
session-model parity (#70931) that landed since the original branch;
the lock now slots into that precedence chain as the top rung.

Salvaged from PR #61236 by @abundantbeing.
2026-07-24 13:39:21 -07:00
teknium1
306c9f7661 feat(models): add anthropic/claude-opus-5 to OpenRouter and Nous Portal catalogs
Anthropic released Claude Opus 5 (+ -fast variant) — both are live on
OpenRouter and the Nous Portal /models endpoint (verified against both
live APIs). Opus 4.8 entries are kept.

- hermes_cli/models.py: opus-5 + opus-5-fast in OPENROUTER_MODELS;
  opus-5 in _PROVIDER_MODELS[nous] (Portal serves both, curated list
  carries the base model like the rest of the Nous Anthropic block).
  Ordering: below fable-5 flagship, above opus-4.8.
- agent/model_metadata.py: claude-opus-5 -> 1M context (matches live
  OpenRouter metadata).
- agent/reasoning_timeouts.py: claude-opus-5 -> 240s stale-timeout
  floor (same as the opus-4.x thinking family).
- website/static/api/model-catalog.json: regenerated via
  scripts/build_model_catalog.py.

Both providers bill via official_models_api (live pricing), so no
_OFFICIAL_DOCS_PRICING snapshot entry is needed for these routes.
2026-07-24 13:00:15 -07:00
Frederick
7dd00bb47d fix(api_server): close divergence gaps from gateway/run.py
Three parity fixes between the API server and the native gateway's
agent-runtime resolution, integrated with the provider-aware request
routing that landed in #70853:

- Session-persisted model is honored: POST /api/sessions {"model": ...}
  stores a model that the chat handlers previously fetched and threw
  away. A stored value that matches a model_routes alias goes through
  the route path (route provider/credentials apply); a raw model string
  threads through as session_model, pinning the session's turns ahead
  of per-request body values but below an explicit session /model
  override.
- Empty-model recovery: provider-catalog default when config has no
  model.default but a provider resolved, plus last-known-good model
  recovery (#35314) keyed on gateway_session_key only (never ephemeral
  session_id — no unbounded growth from one-off requests).
- Provider auth failures surface as controlled responses: RuntimeError
  from _resolve_runtime_agent_kwargs() is re-raised as a dedicated
  _ProviderAuthResolutionError at the call site, caught narrowly in
  _run_agent() and the /v1/runs executor to return run.py's response
  shape instead of an undifferentiated 500 (session-chat endpoints
  previously returned a raw aiohttp 500 with no JSON body).

Salvaged from PR #57947 by @FvanW; session-model route-alias resolution
from PR #59941 by @kaishi00.

Co-authored-by: kaishi00 <kaishi00@users.noreply.github.com>
2026-07-24 11:53:46 -07:00
teknium1
0f732cb3d6 fix(cron): respect the platform-conditional decode design in _run_job_script + taskkill kwarg snapshot
cron/scheduler.py deliberately applies utf-8/replace only on Windows via
popen_kwargs (non-Windows keeps locale default per its test contract) —
drop the sweep's unconditional inline kwargs there. Update the gateway
force-kill kwarg snapshot for the new guard.
2026-07-24 11:45:57 -07:00
teknium1
804ce793de test: update kwarg-snapshot assertions for the utf-8 subprocess guard
- whatsapp taskkill + webhook gh-comment assert_called_with: add the two
  new kwargs
- test_status fake_run: accept **kwargs so signature-strict stub doesn't
  TypeError on encoding/errors
2026-07-24 11:45:57 -07:00
teknium1
477cd09418 chore: map stoltemberg@users.noreply.github.com -> Stoltemberg 2026-07-24 11:45:57 -07:00
teknium1
a5147331ea fix: repair sweep fallout — duplicate encoding kwargs, non-subprocess call sites, kwarg-snapshot tests
- Strip the salvaged commit's inline encoding kwargs where main had since
  gained its own (process_registry, local env, cua doctor, gateway,
  commands, gateway_windows — the latter keeps its locale-aware
  _schtasks_encoding() from #38186)
- Revert encoding kwargs mistakenly applied to non-subprocess APIs
  (exa get_contents, tempfile.mkstemp in webhook.py)
- Guard the ddgs worker Popen (new on main since #55339)
- Update two kwarg-snapshot test assertions for the new kwargs
2026-07-24 11:45:57 -07:00
teknium1
ce2a4ac6c2 chore: map jinglun010@gmail.com -> jinglun010-cpu 2026-07-24 11:45:57 -07:00
teknium1
d4b867cf9f fix(windows): sweep remaining unguarded text-mode subprocess sites codebase-wide
AST-driven pass over every subprocess.run/Popen/check_output/check_call/call
with text=True (or universal_newlines=True) and no explicit encoding=:
append encoding='utf-8', errors='replace' at the kwarg site. 136 call
sites across 28 files (cli.py, hermes_cli/main.py, tools_config.py,
environments, computer_use, gateway, scripts, skills helpers, agent/*).

Together with the salvaged #55339/#60741 commits this closes out issue
#53428's bug class; the salvaged #60751 linter rule in
check-windows-footguns.py now enforces it repo-wide (verified: 807 files
scanned, zero findings).
2026-07-24 11:45:57 -07:00
jinglun010
051217342b feat(linter): detect subprocess text=True without explicit encoding=
Adds a new rule to scripts/check-windows-footguns.py that flags
subprocess.run/Popen/call/check_output/check_call(..., text=True, ...) calls
missing an explicit encoding= kwarg.

On Chinese Windows (cp936/GBK) and other non-UTF-8 default codepages,
text=True without encoding= decodes child output with
locale.getpreferredencoding(False), crashing _readerthread with
UnicodeDecodeError on non-default-codepage bytes (issues #47939, #53428,
rule prevents future regressions.

Rule design:
- Pattern matches 'text=True' / 'text = True'
- post_filter skips lines that:
  - already pass encoding= on the same line
  - are method definitions (def text)
  - contain text=True inside string literals
  - are not subprocess-shaped calls (heuristic via _is_likely_subprocess_call)
- Two helper functions: _is_likely_subprocess_call, _looks_like_string_literal
- Multi-line calls where subprocess.X( and text=True are on different lines
  are not flagged (acceptable false negative for a line-based scanner)

Also fixes the linter's own footgun: get_staged_files() and get_diff_files()
used subprocess.check_output(text=True) without encoding= — now fixed.

Suppresses 4 false positives on non-Windows platform-exclusive calls:
- tools/voice_mode.py (Termux/Android)
- tools/environments/singularity.py (Linux HPC)
- plugins/google_meet/cli.py (macOS system_profiler)

Test plan:
- 21 unit tests in tests/scripts/test_footgun_subprocess_encoding.py
- TestDetection: 6 cases verifying the rule flags real subprocess calls
- TestSuppression: 7 cases verifying false-positive avoidance
- TestHelpers: 7 cases for the two helper functions
- TestFullRepoScan: scans the whole tree and asserts the new rule finds
  only the 7 call sites that PR #60741 fixes (or zero, once #60741 merges)

Verified: full-repo scan reports 7 matches on main (the #60741 sites),
4 platform-exclusive calls correctly suppressed, zero false positives.
2026-07-24 11:45:57 -07:00
jinglun010-cpu
db66119676 fix: extend UTF-8 encoding to _op_version probe (#53428)
Hermes-sweeper review on #60741 flagged that _op_version (the paired
op probe used by the same setup/status CLI flow at lines 127 and 205)
still ran text=True without explicit encoding/errors.

Add encoding='utf-8', errors='replace' to match _op_whoami and the
production op read path at agent/secret_sources/onepassword.py:271-278.

Also extend the regression test to cover _op_version alongside
_op_whoami, and update the module docstring to reflect the widened
scope. Test sensitivity verified: reverting the source change makes
test_op_version_passes_utf8_encoding fail with encoding=None.
2026-07-24 11:45:57 -07:00
jinglun010
a23115414a fix: add explicit UTF-8 encoding to _op_whoami subprocess call (#53428)
PR #55339 adds encoding='utf-8', errors='replace' to 26 subprocess.run(text=True)
call sites across the codebase. The triage review (thanks @alt-glitch) diffed
this PR against #55339 and found that 5 of the 6 originally-touched call sites
are already covered there byte-identically:

- hermes_cli/main.py::_probe_container
- hermes_cli/setup.py SSH probe
- tools/tts_tool.py::_generate_neutts
- tools/transcription_tools.py::_prepare_local_audio
- tools/transcription_tools.py::_transcribe_local_command (both branches)

The one genuinely net-new site — hermes_cli/onepassword_secrets_cli.py::_op_whoami
(the 1Password op CLI whoami probe) — is NOT in #55339 and is fixed here.

Without explicit encoding=, text=True decodes child output with
locale.getpreferredencoding(False) — cp936 on Chinese Windows — which crashes
_readerthread on non-GBK bytes, cascading into pipe buffer fills, event loop
stalls, and TUI freezes (issues #47939, #53428, #57238).

Scope narrowed per triage feedback: the other 5 sites should land via #55339.

Refs #53428 (together with #55339).
2026-07-24 11:45:57 -07:00
Stoltemberg
c89481db5e fix: add explicit UTF-8 encoding to all subprocess text=True calls (#53428)
On Windows with Chinese locale (GBK), subprocess.run(text=True) without
explicit encoding causes UnicodeDecodeError crashes. This fix adds
encoding='utf-8', errors='replace' to all subprocess.run() and
subprocess.Popen() calls that use text=True across 76 non-test Python files.

Fixes #53428 (master tracker for Windows GBK locale crash).

Note: credential_pool.py and electron changes excluded per reviewer request —
those will be submitted as separate focused PRs.
2026-07-24 11:45:57 -07:00
hermes-seaeye[bot]
d460118a1d
fmt(js): npm run fix on merge (#70927)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-24 18:30:37 +00:00
Austin Pickett
0600ac2f77
feat(desktop): add Cron Blueprints to the GUI (#70066)
* feat(desktop): add Cron Blueprints to the GUI

The desktop app had a Cron jobs panel but no Blueprints tab, so the
parameterized automation templates the dashboard offers were unreachable
there (parity matrix: Dashboard=Y, GUI=N).

Adds a Jobs/Blueprints segmented toggle to the cron panel. The Blueprints
tab renders the catalog from the existing GET /api/cron/blueprints endpoint,
one card per template with a typed form (time/enum/weekdays/text slots).
Submitting POSTs to /api/cron/blueprints/instantiate, which fills the slots
and creates a real cron job via the same create_job path as a hand-written
one. The new job is merged into the shared  atom so the Jobs tab
and sidebar reflect it immediately.

Backend already served both endpoints; this is desktop frontend only.
Strings added to all four locales.

* fix(desktop): stop cron blueprint cards clipping and tabs overlapping close X

Blueprint cards were wrapped in PanelBlock (a max-h-48 overflow-auto <pre>
for monospace code), which capped each card and forced an inner scroll,
clipping the copy. Use a plain auto-height card div so rows grow with their
content and the gallery scrolls as one.

PanelHeader actions sat under the overlay's absolutely-positioned close X
(no layout space reserved). Reserve pr-8 clearance when actions are present.

* refactor(desktop): narrow blueprint card i18n dep, document intentional scoping

Address review nits on the Cron Blueprints PR:
- BlueprintCard's submit useCallback depended on the whole t.cron object; it
  only uses the blueprints slice. Bind const b = c.blueprints and use it (plus
  narrow the dep) throughout the card.
- Document the intentional GET-vs-POST profile asymmetry on the blueprint
  endpoints (global catalog vs per-profile instantiate) — the prior comment
  claimed both were profile-scoped.
- Note why the blueprints tab collapses 'all' scope to 'default' (a blueprint
  creates a real per-profile job; 'all' is not a writable target).

* fix(desktop): default blueprint delivery to This desktop, not origin

The blueprint catalog is shared with the dashboard, so its deliver slot
defaults to 'origin' (the chat/home-channel a dashboard or gateway job was
created from). Desktop has no origin chat and no home-channel picker, so the
seeded 'origin' rendered unlabeled and, at delivery, fell through the
home-channel fallback to nowhere when no gateway was configured.

Seed the deliver slot to 'local' (This desktop) when the backend default is
'origin' or empty, drop the origin option from the desktop dropdown, and label
the remaining options with the desktop's own delivery labels — matching the
manual cron editor (local/telegram/discord/slack/email). Also skip the
backend's origin-centric deliver help, which contradicts desktop semantics.

* refactor(desktop): align blueprint cards with the Panel/settings idiom

Address PR review (UI consistency with neighboring surfaces):
- Card container: drop the standalone-card look (bg-foreground/5) for the
  shared in-panel grouping token bg-(--ui-bg-quinary), matching the cron
  editor's in-surface groupings so blueprints sit in the Panel family.
- Form fields: replace the bespoke <label>+<Input> rows with the shared
  ListRow primitive from settings/primitives (label+help on the left, control
  on the right, stacks in a narrow pane) — same idiom as settings/messaging.

No behavior change; blueprint deliver remap and $cronJobs merge untouched.

* fix(desktop): satisfy eslint on the cron blueprint files

CI check:lint failed on import/export ordering and an unused import in the
blueprint changes:
- hermes.ts: sort AutomationBlueprint before AuxiliaryModelsResponse in the
  type import + re-export blocks, and drop the unused AutomationBlueprintField
  import (still re-exported for blueprints.tsx).
- cron/index.tsx: alphabetize the dialog/segmented-control imports and the
  ./blueprints vs ../shell/statusbar-controls group.
- blueprints.tsx: add the required blank lines between statements.

eslint --fix only; no behavior change. typecheck + blueprint tests green.

* refactor(desktop): blueprints reuse the cron editor dialog + shared card

Address review: the blueprint UI was still going its own way on the card and
form. Reuse the app's canonical pieces instead of a bespoke surface.

- CronEditorDialog gains a 'blueprint' mode: EditorState carries the blueprint
  + target profile, the dialog renders the typed slots with the same
  Field/FieldHint/DialogFooter/error-block chrome as manual New cron, and
  submit routes to instantiateAutomationBlueprint. One dialog, one editor state
  machine. Resolves the accordion, the border-t divider, ListRow-vs-Field, the
  ad-hoc buttons, and the plain error <p> in one move.
- Blueprint cards use selectableCardClass({ prominent: true }) (the shared
  theme/pet/gateway card idiom), caller owns padding (p-2), whole card is a
  button that opens the dialog pre-filled. No inline form.
- Gallery renders via PanelDetail, not PanelBody's master/detail row.
- i18n: drop the now-unused blueprints.setUp/cancel, add blueprints.dialogDesc
  across en/ja/zh/zh-hant + types.
- Dropped the stray \u2014 literal comment.

Logic (origin->local deliver, desktopDeliverOptions, merge into $cronJobs) is
unchanged and stays unit-tested. typecheck + eslint + tests green.

* fix(desktop): dropdown no longer closes the cron dialog; unify deliver targets

Two cron-dialog bugs:

1. Dismissing any Select dropdown inside the cron editor dialog closed the whole
   dialog. Radix portals Select/Popover content outside the dialog, so the
   dismiss pointerdown reached the Dialog's DismissableLayer as an
   outside-interaction. Guard DialogContent.onInteractOutside: swallow
   interactions originating from a [data-radix-popper-content-wrapper] (a
   dropdown dismiss inside our own dialog), compose with any caller handler. Fix
   is at the shared Dialog level so every dialog benefits.

2. Blueprint deliver only offered 'This desktop'. The blueprint used the backend
   blueprint field.options (configured gateways only) while the manual editor
   hardcoded local/telegram/discord/slack/email regardless of what's connected.
   Wire the desktop to GET /api/cron/delivery-targets (the documented single
   source of truth, already used by the dashboard) via getCronDeliveryTargets,
   and render both the manual editor and the blueprint deliver slot through one
   shared DeliverSelect. Now all three surfaces agree and only offer connected
   platforms; unconfigured-home-channel targets show a hint.

i18n: add cron.deliverNeedsHomeChannel across en/ja/zh/zh-hant + types.
typecheck + eslint + vitest green.

* fix(desktop): clicking away from an open dropdown no longer closes the dialog

The onInteractOutside guard only caught pointerdowns whose target was inside
the popper wrapper. But dismissing an open Select by clicking elsewhere inside
the dialog also closes the popover, which moves focus — and Radix Dialog reads
that as focusOutside and closes the whole dialog. (Radix Select 2.3.1 has no
modal prop, so that escape hatch isn't available.)

Guard both paths at the shared DialogContent level: onInteractOutside AND
onFocusOutside now swallow the event when it originates from a Radix popper OR
when any [data-radix-popper-content-wrapper] is open at event time (covers the
focus/re-dispatch case where the target is no longer the popper). A genuine
backdrop click with no dropdown open still closes the dialog. Export
isInteractionFromPopper + unit-test the three cases.

typecheck + eslint + vitest green (7 dialog tests).

* fix(desktop): portal popovers into their dialog so dropdowns don't close it

Root cause (affected every dialog, not just cron): Radix Select/Popover/
DropdownMenu portal to document.body — a SIBLING of the dialog, outside its DOM
subtree. Dismissing a dropdown (or clicking another field) moves focus out of
the dialog subtree, which the Dialog's modal FocusScope/DismissableLayer reads
as an outside interaction and closes the whole dialog. Separate body-level
portals also make z-index across the two fragile.

The earlier onInteractOutside/onFocusOutside guards treated symptoms and didn't
hold (and Radix Select 2.3.1 has no modal prop to disable its layer). Real fix
is a layering system: DialogContent publishes its content node via
DialogPortalContainerContext; SelectContent/PopoverContent/DropdownMenuContent
call usePopoverPortalContainer() and portal INTO that node when inside a dialog
(document.body otherwise). The popover is then a true DOM descendant of the
dialog — focus stays in, dismissal no longer closes the dialog, and both share
one stacking context so z-index is deterministic.

Test: with a Dialog open, an open Select's item is a descendant of the dialog
(portalled in), verified in jsdom. typecheck + eslint + component tests green.

* fix(desktop): bump radix-ui so dismissing a dropdown can't close its dialog

Upstream bug, not app-layer: with radix-ui 1.6.0 (dismissable-layer 1.1.13),
an open modal Select sets pointer-events: none on the dialog body, so a click
anywhere inside the dialog hit-tests through to the overlay. The Dialog's
DismissableLayer defers its outside-pointerdown decision to the click, but the
overlay is a registered dismissable surface exempt from the interception check
— so the Select swallowing the press didn't count, the deferred onDismiss
fired, and the dialog closed along with the dropdown.

dismissable-layer 1.1.17 adds shouldHandlePointerDownOutside, which makes the
dialog's layer ignore the press entirely while a higher layer has its pointer
events disabled. Bump radix-ui ^1.4.3 -> ^1.6.5 (dismissable-layer 1.1.17,
dialog 1.1.21, select 2.3.5); lockfile diff is Radix-only.

Repro test fires the pointerdown/up/click sequence on the overlay with a
Select open inside the dialog: red on 1.6.0 (dialog closed), green on 1.6.5.
Counterpart test keeps a genuine overlay click closing the dialog.

typecheck + dialog/select component tests green. Full-suite failures on this
Windows host reproduce identically without the bump (pre-existing env issues).
2026-07-24 14:22:43 -04:00
abundantbeing
4a9447c726 fix(api-server): expose model options inventory
Add authenticated GET /api/model/options to the gateway API server,
sharing the dashboard/TUI picker payload builder so external clients
can sync to the user's configured Hermes provider catalog instead of
scraping the single OpenAI-compatible /v1/models alias.

- new shared hermes_cli.inventory.build_model_options_payload() wraps
  build_models_payload with the stable picker shape and safe
  custom-provider probe policy (probe current only on normal open,
  probe all + cache bust on explicit refresh)
- dashboard web_server and TUI gateway model.options refactored onto
  the shared builder; dashboard build moved off the event loop via
  run_in_threadpool
- capabilities endpoint advertises model_options
- docs for both API server and programmatic integration

Salvaged from PR #54689 by @abundantbeing.
2026-07-24 11:20:07 -07:00
hermes-seaeye[bot]
78a343169c
fmt(js): npm run fix on merge (#70914)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-24 18:14:17 +00:00
brooklyn!
fb8d824bcb
Merge pull request #70907 from NousResearch/bb/desktop-first-run-remote
feat(desktop): add "Connect to existing Hermes" option to first-run onboarding
2026-07-24 13:06:21 -05:00
Austin Pickett
d82dfb6924
feat(desktop): add Webhooks page for subscription CRUD (#69687)
* feat(desktop): add Webhooks page for subscription CRUD

Brings the desktop GUI to parity with the dashboard's Webhooks page.
Adds a /webhooks route that lists webhook subscriptions, enables the
webhook gateway platform, and creates/toggles/deletes subscriptions,
hitting the same /api/webhooks* endpoints the dashboard and CLI use.

- types/hermes.ts: WebhookRoute, WebhooksResponse, WebhookCreatePayload,
  WebhookCreateResponse, WebhookEnableResponse
- hermes.ts: getWebhooks, enableWebhooks, createWebhook, deleteWebhook,
  setWebhookEnabled (profile-scoped) + type re-exports
- app/webhooks/index.tsx: WebhooksView (enable card, restart banner,
  subscription list with copy/toggle/delete, create dialog with
  one-time secret reveal); optimistic toggle, profile re-home
- routing: routes.ts, contrib/surfaces.tsx, chat/route-tile.tsx
- nav: command palette, keybinds (nav.webhooks), sidebar row
- i18n: en + zh full, types interface; ja/zh-hant fall back to English
- test: webhooks-rest.test.ts covers the REST helper contracts

* feat(desktop): surface Webhooks in the status bar instead of nav

Moves the Webhooks entry point from the sidebar nav / command palette /
keybind to a status bar action next to Cron, matching where scheduled
jobs live. The /webhooks route, page, and REST helpers are unchanged.

- use-statusbar-items.tsx: add webhooks action (Globe icon) after cron
- i18n: shell.statusbar.webhooks / openWebhooks (en, zh, types)
- revert nav wiring: sidebar row, command palette entry, nav.webhooks
  keybind action + label, commandCenter.nav webhooks entry

* feat(desktop): add skills field to webhook create form

Exposes the backend's per-subscription skills list in the create dialog
(comma-separated) and shows skill badges on subscription rows, so this
page covers the skill-backed endpoint case as well as general CRUD.

- create form: Skills input; passes skills[] to createWebhook
- rows: render skill badges
- i18n: fieldSkills / fieldSkillsPlaceholder (en, zh, types)

Consolidates the skill-endpoint framing from #42817.

Co-authored-by: LionGateOS <98371158+LionGateOS@users.noreply.github.com>

* feat(desktop): render Webhooks as an overlay instead of a full page

Webhooks took over the whole workspace/chat pane because 'webhooks' was
missing from OVERLAY_VIEWS, so it routed through PageSearchShell while
cron rendered through the Panel overlay. Add it to OVERLAY_VIEWS and wire
it up like cron: mount WebhooksView as a floating overlay in wiring.tsx,
render null for the webhooks route in the workspace table, and convert
WebhooksView from PageSearchShell to the Panel primitive with an onClose.
Drop webhooks from route-tile BUILTIN_PAGES so it can't be tiled as a
page either.

* fix(desktop): place webhook URL copy button next to the URL

The URL span used flex-1, stretching it across the row and pushing the
copy button to the far right. Drop flex-1 so the span sizes to content
and the copy button sits directly after the URL.

* fix(desktop): top-align the deliver-only checkbox with its wrapped label

The label used h-9 items-center, centering the checkbox against the
two-line hint text. Switch to items-start so the checkbox aligns to the
first line.

* feat(desktop): give Webhooks a cron-style master/detail layout

Replace the single-column subscription list with the same Panel
master/detail cron uses: a left PanelList of subscription rows (status
dot + kebab menu) capped by a PanelAddButton, and a right PanelDetail
showing the selected subscription's deliver/events/skills, URL with copy,
description, and prompt. Enable/restart banners sit above the body; the
empty state keeps its own New subscription action.

* fix(desktop): drop header on the Webhooks empty state to match cron

The zero-subscriptions state still rendered the PanelHeader with the
title and the refresh/new buttons on the right. Remove it so the empty
state is just the centered PanelEmpty (icon, message, New subscription),
matching the cron empty state. Also drop the header from the loading
state.

* fix(desktop): top-align deliver-only checkbox and its wrapped label

Drop the min-h-9/pt-1.5 baseline shim that pushed the row down and made
the wrapped hint look misaligned. Use plain items-start with a mt-0.5 on
the checkbox so it sits at the first line, and wrap the hint in a
leading-snug span.

* fix(desktop): drop header refresh/new buttons; + button owns create flow

The populated Webhooks header carried a refresh icon and a New
subscription button. Remove both — the PanelAddButton at the bottom of
the list is the create flow, matching cron. Profile-change reload and the
refresh hotkey still run; the restart banner keeps its own refresh.

* fix(desktop): nudge deliver-only checkbox down 2px

Bump the checkbox top margin from mt-0.5 to mt-[4px] so it sits level
with the first line of the wrapped hint.

* refactor(desktop): reuse shared primitives on the Webhooks page

Address OutThisLife's review — stop reinventing primitives the app
already ships:
- copy: drop the local navigator.clipboard button for the shared
  CopyButton (routes through the Electron clipboard bridge + haptic +
  error state instead of swallowing failures)
- banners: enable/restart callouts now use Alert variant=warning
  (primary color-mix tokens) instead of a hand-rolled amber palette
- toggle: detail Enable/Disable is a Switch (messaging idiom), not a
  text ghost button
- checkbox: create dialog uses the Checkbox primitive, not a raw input

Rows/chips already moved to PanelListRow/PanelDetail/PanelPill in the
earlier cron-layout pass. Left the main-list fetch on manual load and
the local Field helper: cron itself does both, so useQuery here would
diverge from the reference idiom rather than align with it.

* refactor(desktop): move Webhooks fetch to the react-query layer

Replace the manual useState/useEffect load with useQuery keyed by
['webhooks', profileScope] — profile change re-fetches automatically, no
effect. reload() invalidates the query; the optimistic toggle writes the
cache via queryClient.setQueryData then invalidates so backend truth
wins. Load failures surface via an error-watching effect (react-query v5
dropped useQuery onError). Refresh hotkey calls refetch().

Left the create-dialog Field helper as-is: settings ListRow is a
side-by-side settings row, wrong for a stacked dialog form, and cron's
editor dialog (the reference) defines the same local Field.

* refactor(desktop): drop bespoke Field for shared ListRow/ToggleRow

Remove the local Field wrapper entirely. Every create-dialog field now
uses settings ListRow (wide, so label stacks over the full-width
control), the deliver-only pref uses ToggleRow (ListRow + Switch, haptic
baked in) instead of a bare Checkbox, and the created URL/secret reveal
rows use ListRow too. No component in this file is hand-rolled anymore.

* fix(desktop): pair webhook create fields into a 2-column layout

The single-column dialog scrolled awkwardly. Group fields: name +
description side by side, prompt full-width under them, events + skills
side by side, deliver-to + deliver-only side by side. Drop the
deliver-only help text and rename the label to 'Deliver payload only'
(remove the now-unused fieldDeliverOnlyHint i18n key from en/zh/types).

* fix(desktop): align Webhooks page with cron conventions and DESIGN.md

- Delete copy uses deleteDescPrefix + bolded name + deleteDescSuffix (no em-dash)
- Drop the duplicated detail-header Switch + Trash2; enable/disable and delete
  live only in the row kebab, matching CronJobDetail
- Collapse three copyable-value chromes into one flat token-backed CopyValueRow
- Delete success toast gains a w.deleted title
- Replace hand-rolled delete Dialog with shared ConfirmDialog

---------

Co-authored-by: LionGateOS <98371158+LionGateOS@users.noreply.github.com>
2026-07-24 14:06:10 -04:00
cat-that's-fat
7dd2b2dc77 feat(desktop): add "Connect to existing Hermes" option to first-run onboarding
Adds a first-run Desktop choice between installing Hermes locally and
connecting to an existing remote Hermes gateway. The choice appears after
backend resolution but before ensureRuntime(), so selecting remote cannot
accidentally trigger local bootstrap.

New modules:
- first-run-setup-gate: concurrent first-run decision gate and reset semantics
- primary-backend-startup: Electron-free orchestration seam (saved remote
  resolution, gate decision, remote re-resolution, local continuation)
- primary-connection-rehome: prevents dual-owner race where both cold boot()
  and renderer softSwitch() could connect simultaneously
- first-run-remote-form: extracted remote form with stale-result guards

Reuses existing connection-config IPC, encrypted token storage, OAuth
session partition, and primary backend resolution.

Fixes #38602
Fixes #36970
2026-07-24 12:55:06 -05:00
cat-that's-fat
6ab41598a3 chore: fix contributor attribution for desktop PR 2026-07-24 12:54:29 -05:00
kshitijk4poor
6ece52fc73 test: tighten spawn-rewrite assertions and add PTY-path coverage
Follow-up to salvaged PR #70549:
- Replace fragile 'or' assertions with single precise checks that catch
  partial-rewrite regressions (would have masked a missing closing brace)
- Add test_pty_path_uses_rewritten_command covering the PTY spawn path
  that was modified but previously untested
2026-07-24 23:23:19 +05:30
xxxigm
753104e1bd test(fallback): cover xai-oauth → xai same-host same-model failover
Pin that a configured xai API-key fallback still activates when the
primary xai-oauth runtime shares api.x.ai and the same model slug.
2026-07-24 23:22:11 +05:30
xxxigm
a9e7b32162 fix(fallback): allow xai-oauth → xai failover on shared host/model
Base-url+model dedup was meant for custom shim aliases, but it also
skipped first-class providers that share an inference host while using
different credentials. That stranded xai-oauth spending-limit failover
to the xai API-key provider when both used the same model slug.
2026-07-24 23:22:11 +05:30
teknium1
3070de1963 fix(tui_gateway): recover custom provider identity from the session's model name
A session pinned to a named custom provider could silently reroute to the
user's default provider on resume/rebuild. Session rows persist the RESOLVED
provider — bare "custom" for every named providers:/custom_providers: entry —
and when no base_url survived in model_config, the existing heal
(canonical_custom_identity) had only the config.model.provider fallback left.
For users whose global default is a BUILT-IN provider (e.g. nous) that tier
cannot fire, so the bare provider was dropped, resume fell back to the default
provider with the session's custom model name, and the default endpoint 404'd
with "Model '<x>' not found. The requested model does not exist in our
configuration or OpenRouter catalog." Re-selecting via /model fixed it until
the next resume — the reported symptom.

Add a model-name recovery tier between the base_url reverse-lookup and the
config fallback: find_custom_provider_identity_by_model() maps the stored
model back to the entry that serves it (model/default_model/models catalog,
dict and legacy list shapes). The session row always stores the model, so the
entry identity survives even when the row has no base_url AND the global
default points elsewhere.

All five bare-custom heal sites in tui_gateway/server.py now pass the model:
_ensure_session_db_row, _stored_session_runtime_overrides,
_runtime_model_config, _make_agent, and _model_picker_context.
2026-07-24 10:47:32 -07:00
Victor Kyriazakos
45a408f41a fix(gateway): deliver relay-backed homes after restart 2026-07-24 10:45:13 -07:00
kshitij
6de7c0f7a7 fix: use error code 4028 (4025 already taken by session.handoff) 2026-07-24 23:14:57 +05:30
HexLab98
75f61182fc test(tui): cover empty truncate guard on prompt.submit
Refuse ordinal-0 wipes without confirm_empty_truncate; allow the
opt-in path used by first-turn restore/regenerate.
2026-07-24 23:14:57 +05:30
HexLab98
819e01c134 fix(tui): refuse empty prompt.submit truncation without confirm
Stale truncate_before_user_ordinal=0 from a desynced Desktop client
resolved to history[:0] and replace_messages() wiped the durable
transcript. Require confirm_empty_truncate for that edge and have
intentional first-turn restore/regenerate paths send it.
2026-07-24 23:14:57 +05:30
kshitijk4poor
f7e2c0e2e2 chore: add contributor email mapping for agent@hermes.dev -> webtecnica 2026-07-24 23:07:57 +05:30
Hermes Agent
7338309807 fix(telegram): prevent connect hang with retry watchdog and fresh app per attempt (#67498)
The Telegram adapter's connect retry loop could silently stall after
'Connecting to Telegram (attempt 1/8)...' with the event loop permanently
parked in select() — all threads idle, no attempt 2/8 ever scheduled.

Root cause analysis:
- The retry loop reused the same  Application object across all
  8 attempts. After a failed initialize() the app could be in a partially-
  initialized state (closed httpx transports from ,
  or  flag set before the hang) causing subsequent calls
  to silently skip real initialization.
- CancelledError (a BaseException, not an Exception) propagated silently
  through all except handlers with no logging — the task driving the retry
  loop could exit without any trace.
- No total watchdog bound existed for the entire retry loop; only per-attempt
  timeouts via _await_with_thread_deadline. If the loop itself stalled
  between attempts (between-attempt sleep, cleanup, or scheduling), there
  was no timeout to catch it.

Fixes:
1. **Total watchdog deadline**: Compute a total deadline for the entire
   connect loop (8 attempts × init_timeout + 120s margin). Before each
   attempt, check the wall clock; if exceeded, raise OSError immediately
   instead of attempting another initialize().
2. **Fresh Application per retry**: On each failed attempt, rebuild
    via  and re-register all handlers. The old
   app is best-effort shutdown with . This ensures
   each retry starts with a clean slate — no stale transports, no stale
    flag, no leaked state from the previous attempt.
3. **BaseException logging + propagation**: Added
   (placed LAST after all other handlers) to log CancelledError and other
   non-Exception signals before propagating. Previously these exited the
   retry loop silently with no log message.
4. ** block for app rebuild**: The  clause runs after
   every failed attempt that isn't the last, rebuilding the app and
   discarding the old one regardless of which exception class caused the
   failure.
2026-07-24 23:07:57 +05:30
Hermes Agent
d7512c8689 fix: apply _rewrite_compound_background in spawn_local to prevent worker deadlock on server backgrounding
Issue #68915: when the agent runs a compound command with trailing & (e.g.
`cd /app && node server.js &`), bash parses it as `(A && B) &` — a subshell
that holds the stdout pipe open forever when B is a long-running server.
The existing _rewrite_compound_background in terminal_tool.py correctly
rewrites this to `A && { B & }` to avoid the subshell fork, but it was only
applied in the foreground execute() path (tools/environments/base.py).

The background spawn_local() path bypasses base.py entirely and passed the
raw command directly to Popen/PTY, leaving the deadlock unmitigated.

Fix: apply _rewrite_compound_background in spawn_local() before the command
is passed to Popen or PTY spawn. Uses a lazy import to avoid circular
dependency (terminal_tool imports process_registry).

- PTY spawn path: now uses safe_command (rewritten)
- Popen spawn path: now uses safe_command (rewritten)
- Session.command still stores the original (unrewritten) command for display
- Simple `cmd &` is left unchanged (no subshell bug)

Tests: 4 regression tests verifying (1) compound is rewritten, (2) simple bg
is preserved, (3) multi-line compounds are rewritten, (4) session.command
stores original.
2026-07-24 23:07:57 +05:30
teknium1
4a0b84ec09 fix(url_safety): harden proxy DNS delegation — literal IPs stay fail-closed + regression tests
Follow-up on the salvaged #68469 commit:
- Literal-IP hostnames never take the proxy DNS-delegation path (a
  getaddrinfo failure on a literal IP is not a proxy-environment
  symptom, and IPs need no DNS) — keeps the private-IP/metadata floor
  intact under proxy env vars.
- Adds TestProxyEnvironmentDnsDelegation: delegation fires only for
  hostnames, metadata hostname/IP floor holds, DNS-success path
  unchanged, empty proxy var ignored.
- Guards the three pre-existing DNS-failure tests against ambient
  proxy env vars so they don't flake on developer machines.
2026-07-24 10:37:29 -07:00
sg-architect
931ca437ff fix(url_safety): allow DNS failure in proxy/sandbox environments
When the runtime blocks direct DNS (NVIDIA OpenShell, Docker + Squid,
corporate proxy with DNS-only-via-proxy), socket.getaddrinfo() fails
and is_safe_url() blocks *all* requests — including legitimate public
URLs via the configured proxy.

Add _proxy_is_configured() helper that checks HTTPS_PROXY, HTTP_PROXY,
http_proxy, https_proxy, ALL_PROXY, all_proxy.  When DNS fails AND a
proxy is configured, delegate DNS resolution to the proxy rather than
blocking outright.

Blocked hostnames (metadata.google.internal, 169.254.169.254, etc.)
are checked BEFORE DNS resolution, so cloud metadata endpoints remain
blocked regardless of proxy status.

Fixes #32217
2026-07-24 10:37:29 -07:00
kshitij
c769081803 refactor: extract sync_credential_pool_entry_id helper
Replace 3 duplicated entry_id resolution blocks (try/except +
entry_id_for_api_key + fallback to None) in agent_init.py,
chat_completion_helpers.py, and switch_model with a single
sync_credential_pool_entry_id(agent) function in agent_runtime_helpers.

Follow-up to #70323.
2026-07-24 22:58:47 +05:30
Gille
73c4b5a045 fix(auth): stop stale-key credential recovery loops
Track the selected credential by stable pool entry ID so token refreshes and shared cursor movement cannot detach failures from the entry that issued them. Stop unmatched single-entry pools from reporting a no-op rotation as successful recovery.

Co-authored-by: Maxim Esipov <maksesipov@gmail.com>
2026-07-24 22:58:47 +05:30
brooklyn!
d51bd5fdc6
Merge pull request #70870 from NousResearch/bb/ar-rtl-locale
feat(i18n): Arabic (ar) locale with RTL support — desktop, dashboard, agent
2026-07-24 12:20:10 -05:00
Brooklyn Nicholson
a96f7c805b feat(i18n): add Arabic (ar) catalog for agent/CLI messages
Registers `ar` in the supported-language set and alias table and ships
locales/ar.yaml at full key and placeholder parity with en.yaml, covering
approval prompts and gateway slash-command replies. Identifiers, commands,
paths, config keys, model/provider names, and {placeholder} tokens are kept
verbatim.

Co-authored-by: Da7-Tech <286182457+Da7-Tech@users.noreply.github.com>
2026-07-24 12:10:03 -05:00