mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
166 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e8c837c921
|
feat(desktop): surface every provider + models from hermes model in the GUI menus (#40563)
* feat(desktop): surface every provider + models from `hermes model` in the GUI The desktop GUI's model/provider choices were starved relative to the `hermes model` CLI. Onboarding listed ~8 providers, Settings → Model only showed authenticated ones, because the global `/api/model/options` endpoint called build_models_payload() without the full-universe flags the TUI's model.options JSON-RPC already used. - web_server.py: `/api/model/options` now passes include_unconfigured + picker_hints + canonical_order (matching the TUI handler), so every GUI surface fed by it sees all 37 canonical providers with auth hints. - Settings → Model: provider dropdown lists every provider; picking an unconfigured api_key provider shows an inline 'paste key → Activate' flow (auto-selects the recommended default); OAuth/external route to onboarding. - Onboarding: the API-key form is now driven by the full provider catalog (curated five first, then the rest), not a hand-maintained list of five. - types/hermes.ts: ModelOptionProvider gains authenticated/auth_type/key_env. - Tests: model-settings covers the full-universe list + inline activation; fixed a pre-existing stale assertion (nous / hermes-4 was never rendered). * feat(desktop): /model in GUI chat opens the model picker instead of a dead-end notice Typing /model in a desktop chat session printed "/model uses the desktop model picker instead of a slash command" and did nothing — it never opened the picker. (The slash worker can't render the prompt_toolkit modal /model opens in the CLI, so the desktop just showed the unavailable-notice.) - use-prompt-actions.ts: intercept /model client-side. No args → open the desktop model picker overlay (setModelPickerOpen) — the same full provider+model picker as the status-bar button. With args (/model <name> [--provider ...]) → run the switch directly via slash.exec so power users can still type it. - desktop-slash-commands.ts: export isModelPickerCommand() so the hook can detect picker-owned commands without duplicating the PICKER_OWNED_COMMANDS set. - Test: covers isModelPickerCommand for /model (+ args) vs non-picker commands. * fix(desktop): make onboarding provider lists scrollable + clean up card styling The full-catalog onboarding picker could overflow the modal with no way to scroll — the OAuth provider list and the api-key grid both grew past the viewport, hiding the key input and the bottom action row (overflow-hidden card, no scroll container). - Scope a `max-h-[60dvh] overflow-y-auto` region to just the provider list / api-key card grid; the "other providers" disclosure, key input, and action row stay pinned and reachable. - Inner `p-1` so card borders / focus rings aren't clipped by the scroll viewport. - Flatter card styling: drop the persistent border, the redundant selected-state checkmark, and the modal shadow — selection now reads from the ring alone (the muted "already configured" check stays). - Remove the " — set up" suffix from the Settings → Model provider dropdown; the inline setup flow already signals unconfigured providers. * fix(desktop): identify api-key onboarding cards by env var, not id Selecting "Google Gemini" also highlighted "Google AI Studio": the curated catalog and the backend-derived providers can collide on `id` (a provider slug can equal a curated id like `gemini`), so `option.id === o.id` matched two cards at once. Key selection (and the React key + snap-back effect) on `envKey` instead, which the catalog dedups and is therefore unique per card. --------- Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> |
||
|
|
2bf0a6e760
|
feat(dashboard): full tool backend configuration in the GUI (#40418)
Replicate the `hermes tools` configurator in the dashboard Skills →
Toolsets view. Each toolset now opens a config drawer that covers the
full lifecycle the CLI offers: enable/disable, pick a provider/backend,
enter and save API keys, and run a provider's post-setup install hook
with a live log tail.
The toolset view was previously read+toggle only — the provider matrix
and key-status endpoints existed but the page never called them, and
there was no way to save a key or run a backend install (npm/pip/binary)
from the browser.
Backend:
- New CLI subcommand `hermes tools post-setup <KEY>` — non-interactive,
scriptable target that runs a provider's install hook (agent_browser,
camofox, cua_driver, kittentts, piper, ddgs, spotify, langfuse,
xai_grok). Validated against valid_post_setup_keys() so an arbitrary
key can't drive _run_post_setup.
- PUT /api/tools/toolsets/{name}/env — save API keys to ~/.hermes/.env
via save_env_value (same store the CLI writes), validated against the
toolset category's env-var allowlist; blank values skipped.
- POST /api/tools/toolsets/{name}/post-setup — spawn-action that runs
`hermes tools post-setup <key>`; frontend tails the log via the
existing /api/actions/tools-post-setup/status. Registered in
_ACTION_LOG_FILES.
Frontend:
- New ToolsetConfigDrawer component (provider radios, password key
inputs with saved-state, get-a-key links, Run-setup + live install
log). Toolset cards get a Configure button + the drawer also exposes
the enable toggle.
- api.ts: toggleToolset, getToolsetConfig, selectToolsetProvider,
saveToolsetEnv, runToolsetPostSetup + ToolsetConfig/Provider/EnvVar/
EnvResult types.
Validation: 56 admin-endpoint tests pass (10 new: env save w/ CLI
parity + allowlist reject + blank-skip, post-setup spawn validation,
auth gate); 232 web_server tests pass; web npm run build + eslint clean;
HTTP E2E exercises save-key (CLI reads it back) and spawn+poll
post-setup to exit 0.
|
||
|
|
56236b16e3
|
feat(dashboard): rehaul Skills hub browser — connected hubs, featured, preview + security scan (#40384)
The Browse-hub tab was a blank search box with sparse result cards (name + source + one Install button), no way to read a skill before installing, no visual security scan, and no indication it was even connected to any hubs. Backend (web_server.py): - GET /api/skills/hub/sources — lists the configured hubs (label + trust tier + GitHub rate-limit + index availability) and featured skills pulled from the centralized index (zero extra API calls), plus installed-skill provenance so the UI can mark already-installed results. - GET /api/skills/hub/preview — fetches a skill's SKILL.md text + file manifest WITHOUT installing (decodes byte-stored text, masks binaries). - GET /api/skills/hub/scan — runs the SAME quarantine + scan_skill + should_allow_install pipeline the CLI installer uses, then cleans up quarantine, returning verdict / per-finding detail / severity tally / install-policy decision. - search now returns per-source counts + timed-out sources + installed map. Frontend (SkillsPage HubBrowser): - Landing state: connected-hubs strip + featured skill grid (no more blank page). - Rich cards: trust-level color coding, source, tags, identifier, Details + Install (or Installed state). - Detail dialog: read the actual SKILL.md, on-demand visual security scan (verdict pill, severity tally, per-finding list, allow/block policy), GitHub repo link. - Search meta line: result count + timing + per-source breakdown (the 'feels slow / no feedback' complaint). Tests: 4 new endpoint test classes (sources/preview/scan + updated search shape) in test_dashboard_admin_endpoints.py. |
||
|
|
b91aade176
|
feat(desktop): warn when main-model switch leaves auxiliary tasks pinned to another provider (#40286)
Switching the main model never touches auxiliary slot pins (they're independent, sticky per-task overrides). A user who switches main away from a now-unpaid provider keeps paying 402s on every background aux call until they manually reset those pins — silently, with no UI signal. - /api/model/set scope:'main' now returns stale_aux: slots still pinned to a provider different from the new main (additive field). - Desktop Model Settings shows a switch-time notice after Apply AND a persistent banner when any loaded aux slot mismatches the main provider, both wired to the existing 'Reset all to main' action. - Never auto-clears pins — a dedicated cheaper aux model is a legitimate config; surface-and-offer instead of nuking. - Fixes a stale pre-existing assertion in the panel test (main model now renders via selectors, not a standalone label). |
||
|
|
50f9ad70fc
|
fix(dashboard): populate cron delivery dropdown from configured platforms (#40218)
* fix: respect disabled auto-compaction on context overflow Port from anomalyco/opencode#30749. When compression.enabled is false, NO automatic compaction trigger may fire. The proactive token-threshold paths (preflight + post-response should_compress gate) already honoured the setting, but the three provider-overflow recovery paths in the agent loop — long-context-tier 429, 413 payload-too-large, and context-overflow — called _compress_context() unconditionally, silently compressing and rotating the session against the user's explicit choice. Add a single guard at the top of the overflow-recovery dispatch: when compression is disabled and the error is one of those three overflow classes, surface a terminal error (compaction_disabled: True) telling the user to /compress manually, /new, switch to a larger-context model, or reduce attachments. Manual /compress (force=True) is unaffected — it never enters this loop. Tests: new TestOverflowWithCompactionDisabled (413 + 400 overflow don't compress when disabled; control case still compresses when enabled). Existing overflow-recovery tests updated to enable compaction explicitly (they verify the recovery fires); fixture defaults flipped to True to match production (compression.enabled defaults to True). * fix(dashboard): populate cron delivery dropdown from configured platforms The dashboard cron-create/edit dropdown hardcoded five delivery options (local, telegram, discord, slack, email), so users on Matrix — or any other backend-supported platform — had no way to pick their channel even though the cron scheduler delivers to all of them. It also offered Telegram/Discord/etc. to users who never set those up. - cron/scheduler.py: add cron_delivery_targets() — the single source of truth. Intersects gateway-configured platforms with cron-deliverable ones and reports whether each platform's home channel is set. - web_server.py: GET /api/cron/delivery-targets exposes that list (+ the implicit local option) to the dashboard. - CronPage.tsx: both modals render options from the endpoint. Configured platforms missing a home channel still appear, annotated "set a home channel first" (option B), so the user knows what to fix. Edit modal preserves a job's current target even if it's no longer configured. Local-only state shows a "configure a platform under Channels" hint. Validation: scheduler + endpoint E2E'd with a Matrix gateway (home set and unset); 5 new tests; tests/cron + tests/hermes_cli/test_web_server green (366 passed). |
||
|
|
3045d54547 |
fix(desktop): route remote-profile session mutations + fix unified-list pagination
Follow-up to the read-routing fix: make remote-profile sessions fully
first-class, not just resumable.
Mutations (rename/archive/delete) went through the same hermes:api handler but
never carried the owning profile, so they hit the local primary's state.db --
which has no row for a remote session. Deleting/archiving/renaming a remote
session silently no-op'd or 404'd, and the row reappeared on next refresh.
- hermes.ts: setSessionArchived/deleteSession/renameSession take the owning
profile and pass it as request.profile so Electron routes to that profile's
backend (matching the read path). Callers now forward session.profile.
- main.cjs: generalize the intercept (read -> request) to also reroute
DELETE/PATCH on /api/sessions/{id} for remote profiles, stripping the profile
param (the remote serves its own state.db; no cross-profile semantics there).
- web_server.py: DELETE /api/sessions/{id} gains a profile param for parity with
GET/PATCH (local cross-profile delete).
Also fix the unified-list merge: it concatenated each remote's page onto the
primary's without re-windowing, so a limit=N request could return up to
N*(1+remotes) rows and report the primary's (stale) total. Now it over-fetches
limit+offset from each remote (from offset 0), re-sorts by recency, re-windows
to the page, and recomputes total/profile_totals from the remote counts.
Verified live against a remote backend: rename/archive/delete mutate the remote
db; page 1 windows to limit, profile_totals reflect remote counts, page 2 has no
overlap with page 1. tsc -b clean; connection-config tests pass.
|
||
|
|
72eb42d9ec
|
feat(update): stash/restore by default + settable discard for non-interactive updates (reverts #38542, #39568) (#39645)
* Revert "fix(update): require managed marker before destructive clean" This reverts commit |
||
|
|
89baf02919 |
Merge origin/main into bb/desktop-profile-support
Resolve conflicts in desktop settings/cron/messaging/sidebar: adopt main's ListRow + actions-menu refactors for credential rows; keep our profileColor import on the sidebar. Drop the now-orphaned Tip-based helpers. |
||
|
|
2f0c8e90e6 | Add Telegram QR onboarding to dashboard | ||
|
|
cf9dc366dd |
refactor(desktop): drop per-session icons, read-only cross-profile reads
The per-session icon picker added more noise than value — rip it out end to end (sessions.icon column, set_session_icon, the PATCH field, the picker UI, and the SessionInfo.icon type). The cross-profile session aggregator now opens each profile's state.db read-only (mode=ro, no schema init), so listing other profiles on every sidebar refresh never DDLs or takes a write lock on their live DBs. The single-profile hot path stays on par with /api/sessions. |
||
|
|
b94b3622b5 |
feat(desktop): per-session profile switching + cross-profile sessions
Add first-class profile support to the desktop app without app reloads. - Swap the single live gateway onto a session's profile lazily (spawned on demand by the Electron backend pool), so one backend serves the active profile and others stay cold — no OOM with many profiles. - Aggregate sessions across profiles by reading each profile's state.db read-only; unified "All profiles" view groups sessions per profile with per-profile pagination, while the default view stays scoped to one profile. - Add an Arc-style profile rail at the sidebar foot: a default<->all toggle pinned left, colored named-profile squares scrolling between, Manage pinned right. Profile identity is a deterministic per-name color. - Route profile-scoped REST (config/env/skills/tools/model) to the active gateway profile and invalidate React Query caches on swap. Single-profile users never trigger a swap, so their path is unchanged. Backend: - web_server: profile-aware active/list endpoints + per-profile session totals; hermes_state: session_count(exclude_children); main.py: honor --profile over HERMES_HOME env for pooled backends. UI primitives: - Add a position-aware Tip tooltip (instant, themed) as a drop-in for native title=, and strip redundant tooltips from self-descriptive chrome. |
||
|
|
acce1a2452
|
feat(desktop): polish credentials settings and messaging env routing (#39217)
* feat(desktop): polish credentials settings and messaging env routing Align Provider API Keys and Tools & Keys with Advanced ListRow inputs, add Tools & Keys sidebar subnav, move platform env vars to Messaging via channel_managed discovery, strip toolset emojis, and condense cron actions. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): align Messaging credential inputs with settings ListRow style Remove monospace inputs and use CREDENTIAL_CONTROL_CLASS + ListRow layout to match Provider API Keys and Tools & Keys. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
580d924097 |
perf(desktop): make session-id search SQL-bounded, not O(n)
search_sessions_by_id previously fetched up to 10k sessions via list_sessions_rich and filtered them in Python — O(n) per keystroke. Push the id match into SQL instead. - list_sessions_rich gains an optional id_query param: a case-insensitive LIKE pushed into the outer WHERE, matched against each surfaced row's id AND every id in its forward compression chain (via the existing chain CTE). Searching a compression root id or a tip id both resolve to the same projected conversation. LIKE wildcards in the needle are escaped. - search_sessions_by_id now fetches only matching rows (limit*4) and ranks exact > prefix > substring in Python over that small set. - web_server /api/sessions/search: route ID matches and content matches through one lineage-keyed dedup helper so an id-hit and a content-hit on the same conversation collapse to a single result (the contributor's version keyed ID hits by raw sid and content hits by root, which could double-list a compression tip). - command-center haystack also matches _lineage_root_id for parity. E2E verified against a real DB: exact match over 3000+ sessions materializes 1 row in Python (was ~3000), 5ms; root-id resolves to tip; LIKE-wildcard escaping holds. Follow-up to @0xharryriddle's feat(desktop): search sessions by id. |
||
|
|
9ecc331be8 | feat(desktop): search sessions by id | ||
|
|
cae6b5486f |
feat(dashboard): always enable embedded chat; remove dashboard --tui flag
The dashboard's embedded Chat surface (/chat, /api/ws, /api/pty) was gated behind `hermes dashboard --tui` / HERMES_DASHBOARD_TUI=1. The desktop app and the dashboard's own Chat tab both drive the agent over the /api/ws + /api/pty WebSockets, so a dashboard started without the flag would pass the /api/status health check but slam the chat WebSocket shut with WS code 4403 — the app connects, reports "ready", and chat stays dead. This was the root cause behind multiple user reports of the desktop app failing to connect to a self-hosted gateway/dashboard, and it bit Docker and host installs alike. Make the embedded chat unconditional: - web_server.py: _DASHBOARD_EMBEDDED_CHAT_ENABLED defaults to True; drop the embedded_chat parameter and the runtime reassignment from start_server(). The WS gates still read the constant (now always true) so the seam — and its "rejects when disabled" contract test — stays meaningful. - main.py: remove the `--tui` argument from the dashboard subparser and the `embedded_chat = args.tui or HERMES_DASHBOARD_TUI==1` derivation. - web/: isDashboardEmbeddedChatEnabled() returns true unconditionally; drop the deprecated __HERMES_DASHBOARD_TUI__ alias and the dead LEGACY_TUI_RE scrape in the vite dev-token plugin. - apps/desktop/electron/main.cjs: drop `--tui` from the spawned dashboardArgs (it would now error with "unrecognized arguments: --tui") and the redundant HERMES_DASHBOARD_TUI env injection. - Docker: no s6 run-script change needed — the script never passed --tui; the HERMES_DASHBOARD_TUI env var is now simply a no-op, so the image works out of the box with no extra var. - Docs: remove every dashboard --tui / HERMES_DASHBOARD_TUI reference across the CLI reference, env-var reference, docker/desktop/web-dashboard guides, in-app tips, and the zh-Hans translations. The terminal `hermes --tui` / HERMES_TUI references are intentionally left untouched. Tests: 270 passing across web_server, dashboard lifecycle, host-header, auth-gate, and docker-override-scripts suites. |
||
|
|
6717914e0a
|
fix(dashboard): explain WHY a chat WS connection was refused (#38743)
* Port from google-gemini/gemini-cli#21541: back up corrupted config.yaml When config.yaml fails to parse, load_config() silently falls back to DEFAULT_CONFIG and leaves the broken file on disk. If the user then re-runs the setup wizard or hermes config set (both rewrite config.yaml), their broken-but-recoverable overrides are lost for good. Adapts the policy-file recovery from gemini-cli#21541: on the first parse warning for a given broken file, snapshot it to config.yaml.corrupt.<ts>.bak (best-effort, symlink-guarded, size-deduped) and tell the user where it landed. Unlike Gemini's version we deliberately do NOT reset config.yaml to a clean state — hermes never silently mutates user config, and leaving it means a hand-fixed file is re-read on the next load. Tests: 3 new cases (backup created + content preserved + original untouched; same-size backup dedup; symlink not copied). E2E verified with isolated HERMES_HOME and a real tab-indented broken config. * fix(dashboard): explain WHY a chat WS connection was refused The embedded-chat PTY WebSocket (/api/pty) collapsed every rejection into a bare close code: 4401 for any auth failure, 4403 for three unrelated failures (host mismatch, origin mismatch, peer-IP). Neither the server log nor the browser said which gate fired or why, so a "chat won't connect" report was undiagnosable without a repro. Server (web_server.py): - _ws_auth_reason / _ws_host_origin_reason / _ws_client_reason return a short machine-parseable reason; old bool wrappers kept for callers/tests. - pty_ws splits the overloaded 4403 into 4401 (auth), 4403 (host/origin), 4408 (peer not allowed), 4404 (chat disabled), and sends the reason on the close frame (clamped to the 123-byte RFC6455 limit). - Each path logs one line: 'pty auth rejected reason=.. mode=.. cred=.. peer=..' / 'pty refused: <reason> ..'. Accepted path logs 'pty accepted peer=.. mode=.. cred=..' so an audit shows HOW a peer authed, not just that it did. tui_gateway/ws.py: - 'ws send/write failed' now logs error_type=<ExcName> so an exception whose str() is empty (closed-transport sends) no longer logs 'error='. web/src/pages/ChatPage.tsx: - console.warn the real close code + server reason on every close. - Map 4404/4408 to specific banners; 4401/4403 banners echo the server reason; [session ended] prints the close code. E2E verified all five reject paths + accepted path produce matching close code, wire reason, and server log line. |
||
|
|
e45dd2b0e7
|
refactor(web): unify main-slot model assignment base_url/context handling (#38593)
Both POST /api/model/set and the profile-model writer hand-rolled the same provider/default/base_url/context_length reconciliation. Extract it into _apply_main_model_assignment so the custom-vs-hosted base_url logic lives in one place — removing the future-drift risk where one site learns about custom base_url persistence and the other forgets. Behavior unchanged; pinned with a direct helper unit test. |
||
|
|
e3313c50a7
|
feat(dashboard): add Debug Share to the System page (#38600)
* Port from google-gemini/gemini-cli#21541: back up corrupted config.yaml When config.yaml fails to parse, load_config() silently falls back to DEFAULT_CONFIG and leaves the broken file on disk. If the user then re-runs the setup wizard or hermes config set (both rewrite config.yaml), their broken-but-recoverable overrides are lost for good. Adapts the policy-file recovery from gemini-cli#21541: on the first parse warning for a given broken file, snapshot it to config.yaml.corrupt.<ts>.bak (best-effort, symlink-guarded, size-deduped) and tell the user where it landed. Unlike Gemini's version we deliberately do NOT reset config.yaml to a clean state — hermes never silently mutates user config, and leaving it means a hand-fixed file is re-read on the next load. Tests: 3 new cases (backup created + content preserved + original untouched; same-size backup dedup; symlink not copied). E2E verified with isolated HERMES_HOME and a real tab-indented broken config. * feat(dashboard): add Debug Share to the System page Surface `hermes debug share` in the dashboard. The System > Operations section gets a dedicated card that uploads a redacted report + full logs and returns the paste URLs as real, copyable links instead of a log tail. - debug.py: factor a pure build_debug_share() returning structured {urls, failures, redacted, auto_delete_seconds}; run_debug_share now calls it (CLI output unchanged). - web_server.py: POST /api/ops/debug-share runs the share core in a worker thread and returns the structured payload synchronously (the URLs are the whole point — not a backgrounded action). - api.ts: runDebugShare() + DebugShareResponse. - SystemPage.tsx: share card with a redaction toggle (on by default), per-link + copy-all buttons, and the 6h auto-delete countdown. - tests: build_debug_share core + endpoint (redact toggle, failure 502, token gate). |
||
|
|
ca06715721 |
feat(web): wire local/custom endpoints into model assignment
The runtime resolver reads model.base_url from config and ignores the OPENAI_BASE_URL env var, so a self-hosted endpoint could not be configured from the GUI. Two changes enable it: - POST /api/model/set accepts an optional base_url and persists it as model.base_url when provider=custom (still clearing stale base_url for hosted providers). - POST /api/providers/validate now returns the model ids a custom endpoint advertises at /v1/models, so the GUI can auto-pick a default without asking the user to type a model name. Refs desktop onboarding "Local / custom endpoint" bug. |
||
|
|
d50741af90
|
fix(onboarding): clarify Anthropic API vs OAuth provider entries and reorder (#38577)
The setup-flow provider list showed two Anthropic/Claude entries with
ambiguous labels ('Anthropic (Claude API)' and 'Claude Code (subscription)')
in no deliberate order. Relabel and reorder so the distinction and the
subscription caveat are explicit:
- 'Anthropic API Key' (PKCE, API path)
- 'Anthropic OAuth: Required Extra Usage Credits to Use Subscription' (external)
- Both Anthropic entries moved to the bottom of the list.
- 'OpenAI Codex (ChatGPT)' -> 'OpenAI OAuth (ChatGPT)', now first after Nous.
Applied consistently to the backend OAuth catalog (web_server.py) and the
desktop onboarding overlay's PROVIDER_DISPLAY title/order map; test
assertions updated to the new titles.
|
||
|
|
1b89715e15 |
fix(desktop): guard reconnect sockets and keep branch search precise
Avoid stale WebSocket events from an old reconnect attempt flipping the gateway state after a newer socket opens. Also limit session-search dedupe to compression edges so branch-specific hits still open the branch instead of collapsing to the parent. |
||
|
|
93228d5299 |
fix(desktop): persist pins, reconnect after sleep, dedupe session search
Four related desktop session-management bugs: - Pins lost until refresh: pinned sessions are joined against the paginated in-memory session list, so a pinned chat that aged off the most-recent page got evicted on the next refresh (every message.complete triggers one) and the Pinned section went empty. mergeWorkingSessions -> mergeSessionPage now also preserves pinned rows (matched by live id or lineage root). Pin id checks in the chat header, command center, and delete/archive are normalized to the durable sessionPinId so pins survive auto-compression. - Stuck on "Starting Hermes" after sleep: macOS sleep drops the renderer WebSocket; nothing reconnected on wake so the composer stayed disabled. The gateway boot hook now auto-reconnects with backoff on close/error and on wake signals (powerMonitor resume/unlock-screen IPC, window online, visibilitychange). connect() gains an open timeout so a hung reconnect can't deadlock in 'connecting'. Composer placeholder distinguishes "Reconnecting to Hermes" from a cold start. - Loses chats from itself: the same hard-replace that dropped pins also dropped loaded sessions; mergeSessionPage keeps them. - Multiple copies/branches in search: /api/sessions/search deduped only by raw session_id, so compression segments and branches surfaced as separate hits. It now dedupes by lineage root and returns the live compression tip, matching the session_search tool's behavior. |
||
|
|
7fb8a6b5c5
|
feat(dashboard): enrich profiles dashboard and de-dupe channel env vars (#37872)
* feat(desktop): enrich profiles dashboard and de-dupe channel env vars Add active-profile switching, role descriptions (manual + auto-generate via the auxiliary LLM), per-profile model selection, and gateway-running / distribution badges to the GUI Profiles page. New profile creation gains clone-all, optional description and model assignment. Hide messaging-platform credentials (channel_managed) from the Keys/Env page since the Channels page is the canonical surface for them, and relabel the trimmed "messaging" category as "Gateway". Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review feedback on profiles/env changes - ProfilesPage: scope the action-menu outside-click handler to the menu's own container via a ref so opening one card's menu no longer leaves others open. - EnvPage: route the "Gateway" label and hint through i18n (t.common.gateway / gatewayHint) instead of hard-coded English, with an English fallback for untranslated locales. - web_server: only report description_auto=true when auto-generation actually succeeded. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address second-round review on profiles - ProfilesPage: treat describe-auto success by null-checking the description and trust the response's description_auto flag instead of assuming true; disable the model-editor Save button unless the selected choice resolves to a real /api/model/options entry (avoids silent no-op saves). - tests: cover the new profile endpoints (active get/set + 404, description round-trip + 404, model round-trip + 400 validation, and describe-auto success/failure contracts). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): more profiles review fixes (toggles, races, tests) - ProfilesPage: use the canonical `active` returned by setActiveProfile; make the SOUL/description/model action-menu items toggle their editor closed when already open; guard description save/auto-describe against stale responses via an activeDescRequest ref so a late reply can't clobber a different open editor. - tests: assert /api/env channel_managed classification matches _channel_managed_env_keys(). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c5d199eada
|
feat(dashboard): check-before-update flow on the System page (#38205)
The dashboard's update button ran 'hermes update' immediately with no preview. Now the System page shows whether an update is available and asks the user to confirm before applying it. - New GET /api/hermes/update/check: reports install method, current version, and commits-behind (via banner.check_for_updates, 6h-cached; ?force=1 busts the cache). Soft-fails to behind=null on network error; marks docker/nix/homebrew as can_apply=false with the out-of-band cmd. - System page: update-status badge on the Hermes version row (latest / N behind), a Check-for-updates button, and an Update-now button that opens a ConfirmDialog showing the commit count before POST /api/hermes/ update fires. Cached status loads with the rest of the page. - Docs + 5 endpoint tests (git/up-to-date/docker/soft-failure + auth gate). |
||
|
|
fd1ec8033d |
fix(dashboard): authenticate server-spawned PTY child WS with a process-internal credential
The embedded-TUI PTY child attaches to two server-internal WebSockets: /api/ws (its primary JSON-RPC gateway backend) and /api/pub (the event sidecar). Both URLs are built server-side in web_server.py and handed to the child via its environment. In OAuth-gated mode (auth_required=true, every hosted Fly agent), _ws_auth_ok unconditionally rejects the legacy ?token=<_SESSION_TOKEN> path — a leaked session token must not grant WS access once the gate is engaged. But _build_gateway_ws_url() still only emitted ?token=, with no gated-mode branch (its sibling _build_sidecar_url had been given a ticket branch; the gateway-url builder was missed). So the TUI child's /api/ws upgrade was rejected 4401 -> 'gateway websocket connection failed' -> 'gateway startup timeout', leaving the embedded chat unusable on every gated deployment. A single-use 30s browser ticket is the wrong shape for this link: the child reads its attach URL once at startup and reuses it on every reconnect, and on a slow cold boot it may not dial within the TTL. (_build_sidecar_url's own docstring already flagged this fragility.) Fix: add a process-lifetime, multi-use internal credential to dashboard_auth.ws_tickets (internal_ws_credential / consume_internal_credential), minted once per process and NEVER injected into the SPA — it only leaves the process via a spawned child's env, so browser-side XSS can't read it, and a leak grants no more than a ticket already does. _ws_auth_ok accepts it via ?internal= in gated mode only. Both _build_gateway_ws_url and _build_sidecar_url now use it, so the child can reconnect both sockets. Loopback / --insecure behavior is unchanged (still ?token=). Needs review: touches _ws_auth_ok + dashboard_auth (core auth surface). |
||
|
|
dd28f2ac9c
|
fix(dashboard): trust non-web WS origins on OAuth-gated binds after ticket auth (#37870)
Generalises #37747. The WS Origin guard (_ws_host_origin_is_allowed) only trusted the packaged Electron app's non-web origin (file:// / null / app://) when the bind was NOT OAuth-gated. The packaged Hermes Desktop renderer loads over file://, so when it drives a remote OAuth-gated gateway its /api/ws upgrade was rejected with HTTP 403 even though _ws_auth_ok had already validated the single-use ?ticket= one line earlier. This guard runs only AFTER _ws_auth_ok has accepted the WS credential, which is the real auth boundary in every mode: * loopback bind -> legacy dashboard session token * non-loopback --insecure -> legacy session token (Tailscale / LAN, #37747) * OAuth-gated public bind -> single-use, 30s-TTL, identity-bound ?ticket= A non-web origin can only come from a native client; a DNS-rebinding attack always arrives from an http(s) origin and is still match-checked against the bound host. So once the upstream credential check has passed, the Origin guard adds nothing for a non-web origin. Collapsed the loopback/non-gated special cases to 'return True' for non-web origins. http(s) origins keep the strict same-host check, so browser DNS-rebinding defence is unchanged. Tests: gated file:///null/app:// now asserted ALLOWED; cross-site http(s) still rejected on gated and loopback binds; #37747's loopback and non-loopback-insecure cases retained. 37/37 test_dashboard_auth_ws_auth + test_web_server_host_header pass. |
||
|
|
6ed9a2de8f | fix(dashboard): allow desktop websocket origins on remote binds | ||
|
|
54343bcade
|
Merge pull request #37738 from NousResearch/bb/statusbar-model-menu
feat(desktop): inline model picker in the status bar |
||
|
|
ea4fe15631 |
feat(desktop): inline model picker in the status bar
Replace the status-bar model chip's modal with a Cursor-style dropdown: - providers grouped by name in a stable order (no recency reshuffle on select) - per-model hover-Edit submenu for reasoning effort + fast, gated by per-model capabilities now surfaced in the model.options payload - unified Fast toggle: flips the speed=fast param where supported, else swaps to the model's `-fast` variant (base and variant collapse into one row) - localStorage-backed "Edit Models" dialog to choose which models appear Adds reusable dropdown primitives (DropdownMenuSearch, shared row/label tokens, portaled + collision-aware submenus) and reads session state from nanostores rather than prop-drilling, so editing options doesn't rebuild and close the menu. |
||
|
|
123b945731 | Merge remote-tracking branch 'origin/main' into bb/grok-provider-desktop | ||
|
|
cbc82511ea
|
fix(web-server): move event channel state from module globals to app.state (#37683)
Module-level asyncio.Lock() binds to whatever event loop was active at
import time. When the same web_server module is reused across multiple
TestClient instances (or across uvicorn reloads), the old lock still
references a defunct loop, causing 'attached to a different loop' errors
and flaky subscriber-registration races in CI.
Replace the module-level _event_channels dict + _event_lock with:
- _lifespan() async context manager that creates both on the running
event loop during FastAPI startup (guaranteed correct loop binding)
- _get_event_state() lazy accessor that initialises on app.state when
TestClient is used without a `with` block (preserves backward compat)
All call sites (_broadcast_event, /api/pub, /api/events) now receive the
app reference and read state via _get_event_state(app) instead of the
module globals. The test polling loop is updated to check
app.state.event_channels rather than the removed module attribute.
|
||
|
|
a13db76eaa |
fix(desktop): signal loopback worker to stop on cancel
Shutting down the callback server stopped the serve thread but left the worker spinning in _xai_wait_for_callback (which polls callback_result) until the timeout. Flag callback_result as cancelled on DELETE so the wait returns promptly and the daemon thread exits — avoids thread buildup on repeated cancel/retry. |
||
|
|
33807e2b14 |
fix(desktop): use auth-store path as xAI OAuth source_label
source_label is meant to be a human-readable origin (file path / source),
not the internal auth_mode string ("oauth_pkce"). Surface the auth-store
path, then the source slug, then a generic label.
|
||
|
|
d963ad56c1 |
fix(desktop): address second Copilot pass on xAI loopback flow
- onboarding: openSignInUrl now falls back to window.open when the desktop bridge's openExternal throws/rejects (OS handler missing, user denied), not just when the bridge is absent - web_server: cancelling a loopback session shuts down the 127.0.0.1 callback server + joins its thread immediately, freeing the port instead of holding it until the wait times out (+ regression test) - web_server: document the new "loopback" flow in the /api/providers/oauth enum, the poll-endpoint docstring, and the Phase 2 flow comment block |
||
|
|
3be9fb7317 |
fix(desktop): address Copilot review on xAI loopback flow
- web_server: join the callback-server thread in the start error path so a failed discovery/URL build doesn't leave a daemon thread running - web_server: loopback worker now bails if the session was cancelled while waiting for the callback or exchanging the code, instead of persisting tokens the user no longer wants (+ regression test) - onboarding: fall back to window.open when the desktop bridge's openExternal is unavailable, so the flow never silently stalls |
||
|
|
63e824831c | fix(desktop): order xAI Grok after MiniMax in the OAuth catalog | ||
|
|
dd5e97bd7f |
feat(desktop): make xAI Grok a first-class OAuth provider in the launcher
xAI Grok was only reachable via the "I have an API key" form. xAI's OAuth (SuperGrok / Premium+) flow already exists in the backend (`hermes auth add xai-oauth`) but was never surfaced in the desktop onboarding launcher. Add a loopback PKCE flow: the local backend binds the 127.0.0.1 callback listener, the client opens the browser, and the redirect lands back automatically — no code to copy/paste. Reuses the existing xAI OAuth helpers (discovery, callback server, token exchange, persist) rather than duplicating them. - web_server: catalog entry (flow: loopback) + status dispatch + _start_xai_loopback_flow + background worker + route branch - desktop: 'loopback' flow type, awaiting_browser status, xAI Grok card (PROVIDER_DISPLAY / FLOW_SUBTITLES / FlowPanel waiting render) - tests: catalog listing, start authorize-url, worker persist, state mismatch rejection |
||
|
|
bb0619dbce
|
fix(auth): align Codex OAuth persistence paths (#37517)
* fix(desktop): codex OAuth onboarding now resolves on fresh install The desktop codex device-code worker persisted tokens with a hand-rolled pool.add_entry(), writing only credential_pool.openai-codex. It never set active_provider, so on a fresh install the onboarding setup.runtime_check resolved provider "auto", couldn't detect the Codex OAuth session, and raised "No inference provider configured" — while setup.status (which sniffs the pool) reported configured. The disagreement surfaced as the onboarding banner "Connected, but Hermes still cannot resolve a usable provider." Use the canonical _save_codex_tokens() instead, matching the CLI's `hermes auth add openai-codex` path and the Nous/MiniMax dashboard workers. It writes the providers.openai-codex singleton (setting active_provider) and syncs the pool. * fix(auth): align Codex OAuth persistence paths Ensure desktop and CLI Codex OAuth logins both write the canonical provider state so fresh installs resolve a usable runtime provider. --------- Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com> |
||
|
|
6d14a24b79
|
feat(dashboard): nous-blue theme, bulk sessions, schedule picker (#37383)
* feat(dashboard): nous-blue theme, bulk sessions, schedule picker
Batch of related dashboard improvements gathered on
austin/fix/dashboard-changes:
* Nous Blue theme — faithful port of the LENS_5I overlay system onto
the existing DashboardTheme. Lifts the foreground inversion layer to
z-index 200 to fix the long-standing hover / loading visual artifact,
adds an explicit swatchColors slot so the theme picker shows the
post-inversion preview, and migrates the legacy "lens-5i" theme key
from localStorage / API to "nous-blue" on first read.
* Theme-aware series colors: new --series-input-token /
--series-output-token CSS vars consumed by Analytics + Models
charts; ToolCall + ModelInfoCard switched to semantic
--color-success for diff lines and the Tools capability badge.
* Analytics + Models headers: consolidate period selector + refresh
next to the page title and drop the redundant period badge.
* Bulk session management — "Delete empty (N)" button + per-row
checkboxes with shift-click range select and a bulk-delete action
bar. Backed by SessionDB.delete_sessions() /
delete_empty_sessions() plus POST /api/sessions/bulk-delete and
DELETE /api/sessions/empty (registered before the templated
/api/sessions/{session_id} family so they don't get shadowed).
Hard cap of 500 IDs per bulk request. Full pytest coverage.
* Cron page — human-readable schedule picker (every-interval / daily
/ weekly / monthly / once / custom) replaces the raw cron
expression input; the job list now renders "Weekly on Mon, Wed,
Fri at 14:30" instead of "30 14 * * 1,3,5". English-only ordinals
for monthly schedules so non-English locales don't get incorrect
suffixes.
* example-dashboard plugin moved from plugins/ to tests/fixtures/ so
stock installs no longer ship the demo. Tests install it
dynamically via a pytest fixture that also reorders the FastAPI
routes.
* i18n: 40+ new keys for the bulk-select UI and schedule
picker/describer translated across all 16 locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(dashboard): dedupe memory provider picker
The memory provider <Select> lived on both /system and /plugins,
writing the same config.yaml field through two different endpoints
with no cross-page refresh. Remove the picker from /system in favor
of a read-only status row + link to /plugins, where it pairs with
the context-engine picker under "Plugin providers".
/system retains the destructive admin controls (file sizes, Reset
MEMORY.md / USER.md / all). The api.setMemoryProvider client and
PUT /api/memory/provider backend endpoint are left in place for
CLI / script callers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(dashboard): address Copilot review on PR #37383
- Backdrop layer-stack comment claimed LENS_5I-style themes override
--component-backdrop-bg-blend-mode to multiply, but our only
LENS_5I-style theme (nous-blue) keeps the default difference.
Reword to describe what the code actually does and present the
var as a forward-looking extension hook.
- /api/sessions/bulk-delete docstring promised the response would
echo back the list of deleted IDs, but the implementation only
returns {ok, deleted}. Tighten the docstring to match the wire
format; the client already knows what it asked to delete, so the
IDs aren't needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dashboard): address copilot review on cron describe + bulk-select checkbox
- schedule.ts: restrict `describeCronExpression` to strictly 5-field cron
expressions. The backend `parse_schedule` also accepts the 6-field
`min hour dom month dow year` form, and humanising those by
destructuring only the first five fields would silently drop the year
(e.g. ``0 9 * * * 2099`` rendered as "Daily at 09:00"). 6+ field
expressions now fall through to the raw-string fallback so the user
sees what's actually scheduled.
- SessionsPage.tsx (SessionRow): wire the bulk-select Checkbox's
``onClick`` directly instead of attaching it to a parent ``<span>``
with a no-op ``onCheckedChange``. Radix forwards onClick to the
underlying ``<button role=checkbox>``, so the same handler now drives
both mouse clicks (preserving shift-key state for range select) and
keyboard activation (Space on the focused checkbox, which the browser
synthesises as a click on the <button>). Improves a11y / keyboard UX
without changing the controlled-selection model.
- SessionsPage.tsx: also extend ``SessionRowProps`` with the new
``onRename`` / ``onExport`` props introduced on main so the row's
destructured prop types resolve after the merge.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
de8bdf529d |
fix(desktop): keep pinned + recent sessions visible across compression
Long-running sessions auto-compress: the gateway ends the original session and surfaces the live continuation under a new id (list_sessions_rich projects the root forward to its tip). Two symptoms fell out of the id rotation: - A pinned session "vanished" — the pin is stored as the pre-compression root id, but the sidebar only matched on the live id, so it was filtered out. Pins now resolve on the durable lineage-root id (`_lineage_root_id`, already surfaced by the projection): the sidebar indexes sessions by both ids, pin/ unpin and reorder operate on the durable id, and `sessionPinId()` is shared with the Cmd+P toggle. Existing pins keep working with no migration. - A freshly-continued session was missing from the list until you ungrouped + "load 50 more" — the list paginated by original start time, so an old-but- active conversation sat past the first page. The desktop now requests `order=recent` (GET /api/sessions gains an `order` param backed by the existing recency CTE), surfacing live continuations on the first page. |
||
|
|
89db6c8534
|
Merge pull request #37283 from NousResearch/fix-toolset-provider-selection-display
fix(desktop): reflect active toolset provider in config panel |
||
|
|
134643a2fa |
fix(desktop): reflect active toolset provider in config panel
The toolset config panel highlighted the first keyless provider (e.g.
Nous Portal) on load instead of the provider actually written to config.
The /api/tools/toolsets/{name}/config endpoint never reported which
provider was active, so the GUI's default-expand logic fell back to
"first configured" — and keyless providers are always "configured".
Backend now annotates each provider with is_active (via the same
_is_provider_active helper the CLI 'hermes tools' picker uses) plus a
top-level active_provider summary. The panel prefers that signal before
falling back to first-configured/first.
Adds a frontend regression test (active provider is expanded on load)
and backend coverage (config reports is_active/active_provider; selecting
a provider round-trips into the next config read).
|
||
|
|
bd8e2ec1a6
|
feat(dashboard): complete admin panel — MCP catalog, enable/disable toggles, hook creation, system stats (#36736)
* feat(dashboard): MCP catalog + enable/disable, webhook toggle, hook create/delete, system stats
Backend for the comprehensive admin pass:
- MCP: GET /api/mcp/catalog (browse Nous-approved optional-mcps), POST
/api/mcp/catalog/install, PUT /api/mcp/servers/{name}/enabled
- Webhooks: PUT /api/webhooks/{name}/enabled; gateway rejects disabled routes
with 403 (hot-reloaded, no restart)
- Hooks: POST/DELETE /api/ops/hooks — create (with consent approval) + remove;
list now reports accurate allowlist status + valid events
- System: GET /api/system/stats — OS/arch/python/cpu + psutil memory/disk/
uptime/process, stdlib fallback
All gated by dashboard auth; secrets never returned.
* feat(dashboard): MCP catalog UI, enable/disable toggles, hook create, system stats
- McpPage: catalog section (browse Nous-approved MCPs, one-click install with
env prompts) + per-server enable/disable toggle with gateway-restart note
- WebhooksPage: per-subscription enable/disable toggle (muted + badge when off)
- SystemPage: new Host stats section (OS/arch/python/cpu/mem/disk/uptime/load),
shell-hook create modal + delete, 'Create backup' label
- api.ts: client methods + types for catalog, toggles, hook CRUD, system stats
* test(dashboard): cover catalog, toggles, hook CRUD, system stats, webhook toggle
Adds tests for the comprehensive pass: MCP enable/disable + catalog list +
catalog-install-unknown, hook create/delete with consent, system stats shape,
and webhook enable/disable. 26 tests total, all green.
* docs(dashboard): document the comprehensive admin pass + fresh screenshots
Updates the MCP/Webhooks/Pairing/System sections for catalog browse+install,
enable/disable toggles, hook creation, and host system stats; adds the new
endpoints to the API table; replaces the screenshots with live captures of
the rebuilt pages (real data, no dummies) including the hook-create modal.
* feat(dashboard): curator, portal status, and prompt-size/dump/migrate ops
Closes the last in-scope CLI gaps from the coverage audit:
- Curator: GET /api/curator (status), PUT /api/curator/paused, POST
/api/curator/run (background)
- Portal: GET /api/portal (Nous auth + Tool Gateway routing, read-only)
- Diagnostics: POST /api/ops/prompt-size, /api/ops/dump, /api/ops/config-migrate
(backgrounded, tailed via action status)
Host-bound commands (secrets/proxy/lsp/acp/computer-use/desktop/completion/
postinstall/uninstall/claw) remain CLI-only by design.
* feat(dashboard): curator + portal + diagnostics UI, tests
- SystemPage: Nous Portal status section (auth + Tool Gateway routing),
Skill curator card (status + pause/resume + run now), and three new
Operations buttons (prompt size, support dump, migrate config)
- api.ts: client methods + CuratorStatus/PortalStatus types
- tests: curator pause/resume, portal shape, system-stats shape, + auth-gate
coverage for the new GET endpoints (31 tests total)
* docs(dashboard): document curator, portal, and diagnostics + refresh System screenshots
Updates the System section for the Nous Portal status, Skill curator
controls, and the new prompt-size/dump/migrate operations; adds them to the
API table; refreshes the System screenshots (now showing Portal + Curator)
and adds a dedicated curator/gateway/memory capture.
* feat(dashboard): session stats/export/prune + skills hub search endpoints
Completes the existing tabs' backend depth (audit vs CLI):
- Sessions: GET /api/sessions/stats (store stats), GET /api/sessions/{id}/export,
POST /api/sessions/prune. /stats is registered before /{session_id} so the
literal path isn't captured by the parameterized route.
- Skills: GET /api/skills/hub/search — parallel multi-source hub search (threaded),
returns installable identifiers
- (rename via PATCH and cron-edit via PUT already existed; now surfaced in UI)
* feat(dashboard): complete existing tabs — sessions mgmt, skills hub browse, cron edit
Audited every existing tab against its CLI command and filled the gaps:
- Sessions: store stats bar, per-row rename + export (JSON download), and a
prune-old-sessions control (mirrors hermes sessions rename/export/prune/stats)
- Skills: new 'Browse hub' view — search the skill hub across all sources,
install by identifier with a live install log, and 'Update all' (mirrors
hermes skills search/install/update)
- Cron: per-job Edit modal (pre-filled) calling updateCronJob (hermes cron edit)
- api.ts: renameSession/getSessionStats/exportSessionUrl/pruneSessions,
updateCronJob, searchSkillsHub + types
Models tab was already comprehensive (provider+model picker, dynamic per-provider
lists, main + all 11 aux-task assignments, reset) — verified, no change needed.
* test(dashboard): cover session stats/rename/export/prune + skills hub search
Adds the route-shadowing guard for /api/sessions/stats (must not be captured
by /api/sessions/{session_id}), rename/export/prune, and the empty-query
short-circuit for hub search. 36 tests total, all green.
* docs(dashboard): document enhanced Sessions, Skills hub, and Cron edit
Sessions: stats bar, rename, export, prune (+ screenshot). Skills: new Browse
hub view for search/install/update (+ screenshot). Cron: edit action. API
table updated with the new endpoints.
|
||
|
|
d4b533de4e |
fix: batch of small robustness/correctness fixes from @kyssta-exe
Salvages 8 distinct fixes from a batch of PRs by @kyssta-exe, reapplied
onto current main (original branches were stale) with a few refinements.
- cron(jobs.py): load_jobs() validates top-level JSON shape — a bare
list auto-repairs into the {"jobs": [...]} dict; scalars/null raise a
clear RuntimeError instead of an uncaught AttributeError that took
down the whole cron subsystem (#37065, closes #36867).
- web(web_server.py): close the per-action log file handle after Popen
so the parent stops leaking one fd per spawned action (#36843).
- web(web_server.py): DELETE /api/env returns 400 for invalid key names
instead of a misleading 500, mirroring PUT /api/env (#36840).
- gateway(gateway.py): read /proc/<pid>/cmdline inside a with-block so
the fd is released immediately instead of relying on GC (#36804).
- web-tools(web_tools.py): include "xai" in check_web_api_key() so a
configured X.AI web backend reports as available (#36802).
- compression(conversation_compression.py): mark the feasibility check
done only after it completes, and default the gate to "not checked"
if the attribute is missing (#36803).
- completion(completion.py): replace `ls` with directory globbing in the
generated bash/zsh/fish profile listers — handles names with spaces
and skips non-directory entries (#36806).
- terminal-tool(terminal_tool.py): drop a duplicate `import threading`
(#36808).
- claw(claw.py): the migrate recommendation now points at the real
`hermes gateway stop` command instead of the non-existent
`hermes stop` (#36795, #36796, closes #36771).
- tests: guard against a leaked HERMES_CRON_SESSION breaking gateway
approval tests — add it to the hermetic conftest unset list (root
cause, protects every test) and pop it in the affected test's
setup_method (#36796).
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
|
||
|
|
85b65e29f0
|
feat(desktop): session hygiene, archive, media streaming + connecting overlay (#37099)
* feat(desktop): session hygiene, archive, media streaming + connecting overlay
Address a batch of desktop feedback:
- Stop leaking empty "Untitled" sessions: the TUI gateway pre-created a DB
row on every session.create (i.e. every launch/draft). Persist the row
lazily on first prompt instead, and hide message-less rows in the sidebar.
- Archive/hide sessions: new `archived` column + set_session_archived, web
API (`?archived=` + PATCH archived), Ctrl/⌘-click and a context-menu item
in the sidebar, and an "Archived Chats" settings panel to restore/delete.
- Videos load via a streaming `hermes-media://` protocol instead of capped,
in-memory data URLs (16 MB limit) — bypasses the cap and supports seeking.
- Background-process completions route to the session that launched them:
the completion event now carries session_key and each poller only consumes
its own.
- Sidebar: "Group by workspace" toggle is always visible; each workspace
group gets a "+" to start a session in that directory; "New agent"/"Agents"
relabeled to "New session"/"Sessions".
- New gateway connecting overlay (ascii decode → fade out) replacing the bare
skeleton/"starting gateway" state.
* fix(desktop): bail connecting overlay on boot error
The shownRef latch kept the connecting overlay mounted behind
BootFailureOverlay after a hard boot failure. Return null on boot.error
so the failure recovery surface fully owns the screen.
* fix(desktop): address Copilot review
- /api/sessions: validate `archived` (400 on unknown) and return `archived`
as a JSON boolean instead of SQLite's 0/1.
- PATCH /api/sessions/{id}: 400 (not a misleading 404) when the body has no
updatable fields; stop conflating a no-op with "not found".
- hermes-media protocol: drop `bypassCSP` — streaming only needs
secure/standard/stream/supportFetchAPI.
- Sidebar workspace header: split the toggle and the "+" into sibling buttons
so we no longer nest interactive elements inside a <button>.
* fix(desktop): address Copilot re-review
- hermes-media protocol: restrict streaming to an audio/video extension
allowlist (415 otherwise) so it can't be used to read arbitrary local files.
- Connecting overlay: use z-[1200] instead of the non-standard z-1200 utility.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
||
|
|
b571ec298d
|
feat(dashboard): full administration panel — MCP, pairing, webhooks, credentials, memory, gateway, ops (#36704)
* feat(dashboard): backend API for MCP, pairing, webhooks, credential pool, memory, gateway lifecycle Adds REST endpoints so a remote admin can manage these without CLI access: - MCP servers: list/add/remove/test (config.yaml parity with hermes mcp) - Pairing: list/approve/revoke/clear-pending messaging codes - Webhooks: list/subscribe/remove (hot-reloaded JSON store) - Credential pool: list/add/remove rotation keys (via CredentialPool API) - Memory provider: status/select/disable/reset - Gateway lifecycle: start/stop (restart+update already existed) Secrets redacted on read; usable values only reach the agent at session start. All endpoints sit behind the existing dashboard auth gate. * feat(dashboard): backend API for ops + skills hub - Ops actions (spawned, log-tailed via /api/actions): doctor, security audit, backup, import, checkpoints prune - Ops reads (structured JSON): hooks list + allowlist status, checkpoints list with per-session size - Skills hub actions (spawned): install / uninstall / update - Registers new action log files for all spawn-based endpoints All gated by the existing dashboard auth middleware. * feat(dashboard): admin pages for MCP, pairing, webhooks, and system ops Adds four new dashboard pages + nav entries so a remote admin can manage Hermes without CLI access: - MCP: list/add/remove/test MCP servers - Webhooks: list/create/delete subscriptions (one-time secret reveal) - Pairing: approve/revoke/clear messaging pairing codes - System: gateway start/stop/restart, memory provider + reset, credential pool add/remove, ops (doctor/audit/backup/import/skills update) with a live action-log viewer, checkpoints prune, shell-hooks status api.ts: client methods + types for all new endpoints. App.tsx: routes + sidebar nav (plain labels, no i18n key required). Verified: tsc -b clean, production build succeeds, new pages lint clean, zero new eslint errors in App.tsx. * test(dashboard): cover admin API endpoints 20 tests across MCP, credential pool, memory, pairing, webhooks, ops, plus an auth-gate parametrize that asserts every admin endpoint requires the session token. Asserts request contract + CLI-config parity, not catalog values (per the no-change-detector-tests rule). * docs(dashboard): document MCP, Webhooks, Pairing, and System admin pages Adds Pages sections for the four new admin tabs and an Admin-endpoints table to the REST API reference. Updates the page description to reflect the dashboard's expanded role as a full administration panel. |
||
|
|
c1a531d063
|
fix(dashboard): guard update endpoint in Docker with structured guidance (salvage #34831) (#36263)
* fix: guard dashboard update in Docker * fix(dashboard): align action response type --------- Co-authored-by: Donovan Yohan <donovan-yohan@users.noreply.github.com> Co-authored-by: Donovan Yohan <34756395+donovan-yohan@users.noreply.github.com> |
||
|
|
7fbe9b79ab
|
fix(desktop): add missing PATCH /api/sessions/{id} so rename works (#36249)
The desktop rename dialog sent PATCH /api/sessions/{id}, but the backend
only defined GET and DELETE for that path — FastAPI returned 405 Method
Not Allowed, surfaced to the user as "Rename failed". Add the PATCH route
backed by SessionDB.set_session_title (handles sanitization, uniqueness,
and clearing the title when empty).
Also fix a misleading notification: any 405 was summarized as an unrelated
"does not support that audio endpoint" message. Make it a generic 405 hint.
|
||
|
|
51c68d4ab1
|
Add Hermes desktop app (#20059)
* feat: better composer etc * docs: add desktop and dashboard run instructions * fix(desktop): address security scan findings * fix(dashboard): resolve @nous-research/ui path under npm workspaces The sync-assets prebuild step shelled out to 'cp -r node_modules/@nous-research/ui/dist/fonts ...' with a path relative to apps/dashboard/. That works only when the dep is installed locally in the dashboard workspace, but 'npm install' at the repo root (the documented setup — see apps/desktop/README.md) hoists shared deps to the root node_modules under npm workspaces. The relative cp then fails with 'No such file or directory', sync-assets exits 1, the Vite build aborts, and 'hermes dashboard' surfaces a generic 'Web UI build failed' message. Replace the shell one-liner with scripts/sync-assets.cjs, which walks up from the dashboard directory looking for node_modules/ @nous-research/ui — working in both the hoisted (workspaces) and co-located (standalone) layouts. Also guards against a missing dist/fonts or dist/assets with a clearer error pointing at a rebuild of the UI package rather than silently copying nothing. * feat(desktop): support connecting to a remote Hermes backend Add HERMES_DESKTOP_REMOTE_URL and HERMES_DESKTOP_REMOTE_TOKEN env vars that, when set, short-circuit the local-child spawn in startHermes() and connect the Electron renderer to an already- running 'hermes dashboard' server reachable over the network. Motivating use case: WSL2 users who want to run the Hermes core (agent loop, tools, filesystem access) inside their WSL distribution while rendering the Electron GUI on native Windows. Before this change, the desktop app always spawned a local Python child on the same host as the renderer, which doesn't cross the WSL/Windows boundary. The remote path reuses waitForHermes() as a liveness probe (/api/status is in the backend's public endpoint allowlist), so the connection is only returned once the backend is actually ready. WebSocket URL derivation picks ws:// or wss:// based on the input scheme. URL validation rejects non-http(s) schemes and requires both env vars together to avoid a half-configured connection that would silently fall through to the spawn path. No behaviour change when the env vars are unset — the default local-spawn flow is untouched. Typical usage: # in WSL2 hermes dashboard --tui --no-open --host 0.0.0.0 --port 9119 --insecure # on Windows set HERMES_DESKTOP_REMOTE_URL=http://localhost:9119 set HERMES_DESKTOP_REMOTE_TOKEN=<session token> set HERMES_DESKTOP_IGNORE_EXISTING=1 (launch Hermes desktop) * ci(desktop): automate desktop releases Add GitHub Actions release channels for signed desktop installers and document the stable/nightly download paths. * feat: file tabs * refactor(desktop): tighten right-rail tab close API Promote closeRightRailTab/closeActiveRightRailTab as the single public entry point. Drops the activeTabRef + handleCloseDocument indirection in ChatPreviewRail, the unused $rightRailHasContent atom, and the legacy dismissFilePreviewTarget alias. -70 LOC. * feat(desktop): polish composer pill toward reference look Solid foreground-on-background send/voice-conversation circle (black-on-white in light, white-on-black in dark) anchors the right edge as the primary CTA instead of the orange theme primary. Bumps the primary control to 2.125rem so it visually outranks the ghost mic/plus controls. Opens up the surface padding (0.625rem x / 0.5rem y) so the input row breathes around its controls, and nudges the corner radius from 20 to 24px for a slightly pill-ier silhouette. LiquidGlass distortion is preserved. * feat(desktop): add startup and onboarding flow Add phase-based desktop boot progress, fresh-install sandbox testing, and first-run provider credential onboarding so packaged installs can start cleanly without manual settings detours. * fix(desktop): gate prompts on provider setup Show the desktop provider onboarding flow before prompt submission when no inference provider is configured, preventing fresh installs from falling through to backend credential errors. * fix(desktop): surface provider onboarding from session warnings Propagate credential warnings through session runtime info and open desktop onboarding whenever a session reports no usable provider, so unconfigured installs cannot fall through to prompt errors. * fix(desktop): route gateway provider errors to onboarding The "No inference provider configured" auth error reaches the renderer through gateway error events, not the prompt.submit promise; the previous patch only caught the latter, so the error toast still surfaced and onboarding never opened. Also strip credential-shaped env vars from the test:desktop:fresh sandbox so the packaged backend can't see provider keys leaking from the launching shell. * fix(desktop): use strict runtime check to drive onboarding setup.status returned True whenever any provider auth state was discoverable, including indirect fallbacks like a gh-CLI Copilot token. That made desktop think the user was set up while the agent's actual resolve_runtime_provider call still raised AuthError, leaving the user with a useless toast and no onboarding. Add a setup.runtime_check gateway method that runs the same resolver the agent uses on session creation, and switch the desktop onboarding overlay and prompt precheck to use it. * feat(desktop): OAuth-first onboarding using existing dashboard provider API Replace the engineer-flavored API key form with a Sign-in-first onboarding overlay that uses the dashboard's existing /api/providers/oauth catalog and PKCE/device-code endpoints (Anthropic, Nous, OpenAI Codex, etc.). API key entry is now a fallback tab with friendly provider names instead of env var prefixes, and the loud raw resolver error is gone in favor of a one-line welcome message. * fix(desktop): polish onboarding provider list Reorder OAuth providers so Nous Portal is first, give the segmented Sign in / API key control equal column widths, and replace the engineer-flavored backend names like "Anthropic (Claude API)" / "MiniMax (OAuth)" with friendlier in-app titles. External-CLI providers now show a softer subtitle and an external-link icon instead of a chevron. * refactor(desktop): split onboarding overlay into store + view Move the OAuth state machine, runtime check, copy-to-clipboard, and api-key save into store/onboarding.ts (matching the boot.ts pattern), leaving the overlay as a presentation layer that subscribes via useStore. Tabs are now table-driven, child panels read flow from the store instead of prop-drilling, and the polling/PKCE/error/success branches share a small Status atom. * fix(desktop): external CLI providers + center mode tabs External-CLI providers (Claude Code, Qwen Code) now open an in-overlay panel with the CLI command, copy button, and an "I've signed in" recheck instead of firing an invisible toast. Center the Sign in / API key tab control so it sits under the heading instead of hugging the left edge. * fix(desktop): drop onboarding tabs for an inline link, group device-code waiting state Replace the Sign in / API key tab pair with an "I have an API key" footer link under the OAuth provider list, with a "Back to sign in" affordance inside the API key form. Group the device-code "Waiting for you to authorize..." status next to the Cancel button so the alignment matches the action. * refactor(desktop): tighten onboarding store + overlay Drop the dead isOnboardingBusy/BUSY set, factor the catch-fallback dance into safeReq, and share a single reloadAndConnect helper between PKCE submit, device-code success, external recheck, and api-key save. In the overlay, extract Step / CodeBlock / FlowFooter / CancelBtn / DocsLink atoms so the four sign-in panels share the same chrome instead of repeating it inline. Net effect: fewer literal divs, one place to touch the spacing, and the code-block + footer rows are reusable across future flows. * fix(desktop): mount onboarding from frame 1 to kill the FOUT Default onboarding.configured to null (unknown until the runtime check resolves) and have the onboarding overlay render whenever it's not yet confirmed true. The boot overlay now yields to it, so the very first paint is the Welcome card with a "While we get you set up..." progress strip instead of a flash of the chat shell between boot dismiss and onboarding mount. The picker swaps in cleanly once the gateway opens and the runtime check confirms the user is not configured. Already-configured users see the same prep card briefly while their existing runtime warms up, then the overlay dismisses without touching the chat shell. * fix(desktop): top-align empty sessions placeholder The "Start a chat to build your history." empty state used a min-h-35 grid place-items-center container, which floated the text in a tall dead zone. Render it as a flat paragraph that sits right under the section header like the empty pinned state does. * refactor(desktop): drop dead boot overlay Onboarding overlay subsumes the boot card now that it mounts from frame 1 and renders boot progress inline. The standalone DesktopBootOverlay is unreachable in every flow (yields whenever onboarding has not confirmed configured, dismisses once it has). * fix(desktop): hide pinned/recents sections until first session A fresh sidebar showed the Pinned and Recent chats headers with floating empty-state copy underneath. Drop both sections (and the now-orphan SidebarEmptySessionState) when there are no sessions yet — they reappear after the first chat. Skeletons during initial load are unchanged. * feat(gui): route embedded TUI through dashboard gateway (#21979) Inject HERMES_TUI_GATEWAY_URL into dashboard PTY sessions so embedded ui-tui instances attach to the in-process websocket gateway, with coverage for the new env wiring. * Add desktop remote gateway settings Make the desktop gateway connection configurable from settings so local remains the default while remote backends can be saved, tested, and applied without environment variables. * feat(gui): first-class Messaging page + gateway menu redesign - Add Messaging page to the desktop app with per-platform setup, status, and inline guidance. Catalog derives from gateway.config Platform enum + plugin registry, so every messaging adapter the CLI supports (Telegram, Discord, Slack, Mattermost, Matrix, WhatsApp, Signal, BlueBubbles, Home Assistant, Email, SMS, DingTalk, Feishu, WeCom, Weixin, QQ, Yuanbao, API server, Webhooks, plugins) shows up without per-platform code. - New REST endpoints: GET /api/messaging/platforms, PUT and POST /test on the same path. Secrets go through the existing .env pipeline; enable/disable writes config.yaml. - Replace gateway statusbar dropdown with a richer panel: status row, icon-only restart + system-panel actions, recent activity (with timestamps trimmed in display, full text on hover), platform list. - Auto-poll the messaging page every 6s (paused when hidden) so status updates without a manual check. - Drop Settings / Command Center from the sidebar nav (still reachable via shortcuts and the titlebar cog). - Flatten top corners on Messaging/Skills/Artifacts/Chat panes. - Share new StatusDot component across messaging + gateway menu. - Fix gateway/config.py so an explicit platforms.<name>.enabled=false in config.yaml is honored when env tokens are present. - pb-9 on the chat content area for breathing room above the composer. * Potential fix for pull request finding 'CodeQL / Clear-text logging of sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * pin electron version * hide application menu on non-mac systems * interpret compactPreview for non-string vlaues as JSON or an empty string * fix(desktop): keep composer contenteditable mounted across stacked toggle The composer rendered {input} inside two different parent fragments depending on `stacked`. When auto-expand flipped `stacked` (e.g. the moment typed text wrapped past two lines), React reconciled the two branches as different positions and unmounted/remounted the contenteditable. The fresh mount started empty, so any in-flight characters — most reliably reproduced by holding a key — were lost. Replace the conditional with a single CSS Grid whose template-areas swap on `stacked`. The three children (menu, input, controls) keep stable identities across the toggle; only their grid placement changes, which the browser handles without React tearing down the editor. * refactor(desktop): align install layout with install.ps1 / install.sh Make the desktop app's runtime layout match what scripts/install.ps1 and scripts/install.sh produce, so a desktop-only user and a CLI-only user end up with the same files in the same places and can share one install. Layout - ACTIVE_HERMES_ROOT = HERMES_HOME/hermes-agent (was: process.resourcesPath/hermes-agent, read-only) - VENV_ROOT = HERMES_HOME/hermes-agent/venv (was: userData/hermes-runtime) - desktop.log = HERMES_HOME/logs/desktop.log (was: userData/desktop.log) - HERMES_HOME default: %LOCALAPPDATA%\hermes on Windows, ~/.hermes elsewhere The packaged .app/.exe still ships a read-only payload at process.resourcesPath/hermes-agent (FACTORY_HERMES_ROOT). On first launch or after an installer-driven upgrade we sync factory -> active, then provision the venv and run pip install -e . against the active root. Key behaviors - Pin HERMES_HOME in the spawned Python's env so get_hermes_home() resolves to the same path resolveHermesHome() picked. Without this, Python falls back to ~/.hermes on every platform - fine on mac/linux, a split-state bug on Windows where our default is %LOCALAPPDATA%\hermes. - Detect developer installs by .git presence at ACTIVE; never overwrite a user's checkout via factory sync. - Marker at ACTIVE/.hermes-desktop-runtime.json (schema v4) tracks pyproject hash + factory version + runtime schema version. depsFresh fast-paths when nothing changed. - Dev (npm run dev) prefers SOURCE_REPO_ROOT over ACTIVE so devs run their local edits, not whatever's under HERMES_HOME. - Better error messages distinguish "no payload" from "no Python". - Preserve a legacy ~/.hermes on Windows when no %LOCALAPPDATA%\hermes exists, so users with prior pip/manual installs aren't orphaned. pyproject.toml - Promote fastapi, uvicorn[standard], ptyprocess (non-Windows), and pywinpty (Windows) to main dependencies. The dashboard backend (hermes dashboard) needs them at runtime; the previous lazy-import fallback was a footgun for fresh installs. - Empty the [pty] optional-extra; kept as a no-op back-compat alias for any existing pip install hermes-agent[pty] invocations. Drops the hardcoded BUNDLED_RUNTIME_REQUIREMENTS list in main.cjs - the desktop now installs whatever pyproject.toml says, single source of truth. Files - apps/desktop/electron/main.cjs: runtime layout, HERMES_HOME pin, factory->active sync, marker v4 - apps/desktop/scripts/test-desktop.mjs: track new venv location - apps/desktop/README.md: new Setup, Runtime Bootstrap, and Debugging sections - pyproject.toml: fastapi/uvicorn/pty backends in main dependencies; [pty] extra emptied Tested locally on Windows: npm run dev boots cleanly, sessions land at the new location, type-check + lint + test:desktop:platforms all pass. Verified end-to-end on a fresh Win11 VM via dist:win installer. Known gaps (filed as follow-ups, not in this PR): - Skills not seeded on packaged installs (sync_skills only runs in cmd_chat, not cmd_dashboard). Need to move to shared pre-dispatch. - Git Bash not bundled or detected; agent's terminal tool errors out with a useful message but desktop bootstrapper should pre-flight it. - install.ps1 / install.sh should be decomposed into composable phase libraries so the desktop bootstrapper can reuse them as a single source of truth across all install surfaces. * feat(desktop): theme polish, prose chat typography, composer chrome - DS tokens/midground, Backdrop, scoped scrollbars, typography plugin + prose - Composer liquid/radius utilities, thread font parity, tool/thinking cues - File tree label scale, preview flex, thread retry loading + streaming tests * feat(desktop): NSIS prereq detection page + auto-install via winget The packaged Windows installer now detects Python 3.11+ and Git for Windows at install time and offers to install missing prereqs via winget. Mirrors the prereq logic scripts/install.ps1 already runs for CLI installs, so desktop installer users get the same out-of-the-box experience as install.ps1 users. Why - Hermes' terminal tool calls bash.exe directly (tools/environments/ local.py); on Windows that's Git Bash from Git for Windows. Without it, the agent fails on the first terminal() call. - Hermes' Python runtime needs 3.11+. Without it, the desktop bootstrapper errors out at venv creation. - Both gaps surfaced on a fresh Windows 11 VM smoke test: VM had Python pre-installed but no Git, so the agent's first terminal call failed with "Git Bash isn't installed." - install.ps1 has had Install-Git + Install-Uv functions for ages. The desktop installer was the asymmetric outlier. How — NSIS prereq page - New file: apps/desktop/installer/prereq-check.nsh (plugged into electron-builder via build.nsis.include) - Real Wizard page using nsDialogs, inserted via customPageAfterChangeDir hook (between the Directory page and InstFiles). - Group boxes for Python and Git, each showing detection status. - Pre-checked install checkboxes when winget is available. - Auto-skips silently if both prereqs are already installed. - Falls back to manual download URLs when winget itself is missing. - Detection: - Python: probes `py -3.11`/`-3.12`/`-3.13`/`-3.14` via the Python launcher. Microsoft Store "Python stub" (no py.exe) is correctly classified as not-installed. - Git: `where git`. - winget: `where winget` (Win10 1809+ / Win11 with App Installer). - Install execution (in customInstall macro): - Python: nsExec::ExecToLog with `--scope user --silent`. Per-user install, no UAC prompt, output streams to install log. - Git: ExecShellWait via Windows ShellExecute. Critical because Git always installs per-machine and triggers UAC; ShellExecute preserves the foreground focus chain across non-elevated → elevated process spawns, so UAC actually comes to the foreground. nsExec::ExecToLog breaks the chain because winget runs hidden. - Both pass `--disable-interactivity --accept-package-agreements --accept-source-agreements` to suppress winget's own dialogs. - Verification: probes Git's standard install locations via FileExists rather than `where git`. NSIS's process inherits PATH at startup, so a freshly-installed Git won't be visible to `where` until restart. - Silent installs (/S) skip the prompts; managed deploys handle prereqs out-of-band via Group Policy / Intune. How — Electron-side safety net - New findGitBash() in main.cjs, parallel to findSystemPython(). Probes the same locations as tools/environments/local.py:_find_bash() so a positive result here means the agent's terminal tool will work. - ensureRuntime now throws a clear, actionable error on Windows when Git Bash isn't found, matching the existing "Python 3.11+ is required" error path. - Catches users the NSIS page doesn't: .msi installer users (NSIS prereq page doesn't run for MSI), `npm run dev` users, manual installers, anyone who unchecked the install boxes on the NSIS prereq page. - All gated on `IS_WINDOWS`; macOS / Linux unaffected. NSIS build issue (resolved) - electron-builder defaults to `-WX` (warnings as errors). NSIS optimizer emits "warning 6010: function not referenced" for our page functions because Page custom directives don't count as references in its static-analysis pass. The functions ARE called at runtime when NSIS invokes the page; the optimizer just can't see it statically. - Set `build.nsis.warningsAsErrors=false` in package.json so this spurious warning doesn't fail the build. (Documented option from electron-builder's nsisOptions.) Out of scope (filed for future work) - MSI prereq detection: Windows Installer custom actions are a different mechanism. Enterprise deploys typically handle prereqs via GP/Intune. - Bundle PortableGit + python-build-standalone in extraResources for zero-network installs. ~80MB increase. - Mac / Linux GUI prereq flows (different installer formats; Xcode CLT covers most macOS prereqs already; Linux is per-distro hard). Files - apps/desktop/installer/prereq-check.nsh (new, ~290 lines NSIS) - apps/desktop/package.json (build.nsis.include + warningsAsErrors) - apps/desktop/electron/main.cjs (findGitBash + preflight) - apps/desktop/README.md (Runtime prerequisites section) Cross-platform impact - macOS / Linux builds (dist:mac, dist:mac:dmg, dist:mac:zip): nsis config is ignored entirely; .nsh is dormant. - npm run dev: .nsh dormant; main.cjs preflight gated on IS_WINDOWS. - scripts/install.ps1, scripts/install.sh: no reference to any new files; CLI install paths untouched. - Hermes CLI / dashboard / gateway: no reference; runtime untouched. - All checks: node --check on main.cjs and test-desktop.mjs pass; npm run test:desktop:platforms 4/4 passing; node --test green. Tested - npm run dist:win produces signed .exe and .msi without errors. - Fresh Win11 VM (Python pre-installed, no Git): prereq page renders, Python check shows detected, Git checkbox pre-checked. Click Next → Git installs via winget with UAC prompt in foreground. - After install completes, Hermes launches and the agent's terminal tool can run bash commands. Verified Git Bash is detected at `C:\Program Files\Git\bin\bash.exe` by ensureRuntime's preflight. * feat: theme changes, composer tweaks, in app update ux, finesse * fix(cli): seed bundled skills on dashboard + gateway entrypoints `sync_skills(quiet=True)` was only being called from inside `cmd_chat`, which meant `hermes dashboard` (the desktop GUI's backend) and `hermes gateway` (Telegram/Discord/Slack/etc daemons) never seeded the bundled skill library into ~/.hermes/skills/. This surfaced as "No skills found" in the desktop GUI's skills panel on fresh installs, despite the agent having access to the full bundled library when invoked via `hermes chat`. scripts/install.ps1 worked around it by running skills_sync.py as part of Copy-ConfigTemplates, but that's not part of the desktop installer's bootstrap chain. Fix - Extract the skills-sync block from cmd_chat into a module-level `_sync_bundled_skills_quietly()` helper. - Call the helper from cmd_chat (preserving existing behavior), cmd_dashboard (after the --status/--stop early-return paths and fastapi import check, so we don't run skills_sync on management commands or when deps aren't installed), and cmd_gateway. Why these three entrypoints - cmd_chat: the user's primary CLI entrypoint - cmd_dashboard: the desktop GUI's backend; this is what `hermes dashboard --tui` invokes when the desktop bootstrapper spawns Hermes - cmd_gateway: long-running daemons where the user expects the agent to have full skill access Other entrypoints (cmd_config, cmd_doctor, cmd_login, cmd_status, etc.) are management commands that don't need skill discovery and were never running skills_sync in the first place — leaving them alone. Idempotence - tools/skills_sync.py is manifest-based: skipped skills cost milliseconds. Calling it from multiple entrypoints adds no real cost, and users running `hermes chat` then `hermes dashboard` get two fast no-ops on the second call. Failure handling - Helper wraps skills_sync in try/except. Skills are an enhancement, not a hard dependency — Hermes runs fine with an empty skills/ dir. Files - hermes_cli/main.py: + new helper `_sync_bundled_skills_quietly()` at module level + cmd_chat: replace inline block with helper call + cmd_dashboard: add helper call after fastapi import succeeds + cmd_gateway: add helper call before delegating to gateway_command * feat(desktop): hoisted todo widget, JSON tool summaries, history grouping & timer fixes - Hoist todo to first-class widget (shadcn checkboxes, brand colors, no tool-accordion). Header derives label from active task; non-active rows fade. - Replace raw JSON dumps with structured key/value summaries via formatToolResultSummary; nested error extraction for clearer failures. - Fix loaded-session grouping: stitch interleaved assistant/tool iterations into one bubble instead of orphaned synthetic messages. - Stable tool/thinking timers via keyed registry so unmount/scroll doesn't reset elapsed counts; gate "running" on real live thread state. - Reorganize chat-only assistant-ui components under components/chat/. * fix(desktop): address CodeQL alerts on PR #20059 - settings/helpers.ts: harden setNested against prototype pollution. POLLUTING_PATH_PARTS check is now applied at every assignment site (loop + leaf) and uses Object.defineProperty so CodeQL can see the guard inline rather than via a helper function call. - lib/markdown-preprocess.ts: rebuild the dangling-fence close regex from a fence-char + length instead of marker.replace(...). The marker is captured by `(`{3,}|~{3,})` so it can only be backticks or tildes, but CodeQL was tracing tainted input text into the RegExp source and flagging hostname dots from input as part of the pattern (false positive js/incomplete-hostname-regexp on the test fixture URLs). Reconstructing from a literal char breaks the dataflow. - scripts/notarize-artifact.cjs: drop args from the run() rejection message. Args carry --key-id / --issuer / key file path; the existing outer catch already squashes errors to a generic line, but CodeQL was flagging the args.join(' ') as clear-text logging of APPLE_API_KEY_ID. Composer DOM-text-as-HTML alerts (composer/index.tsx:379, :547) are already addressed in |
||
|
|
234ac00937
|
fix(dashboard): allow insecure WS peers on explicit non-loopback binds (#35386)
The merged 0.0.0.0/:: insecure-bind fix (#35141) did not cover binding directly to a specific non-loopback address (e.g. a Tailscale/LAN IP via --host 100.64.0.10 --insecure). In that mode the dashboard HTML loaded but every WebSocket upgrade was rejected by the loopback-only peer guard, so /chat connected then silently received no data. Generalize _ws_client_is_allowed to lift the loopback-only peer gate for any explicit non-loopback bound host, not just the 0.0.0.0/:: wildcard. DNS-rebinding stays blocked: _ws_host_origin_is_allowed already requires the Host header to exactly match the bound interface for explicit binds, mirroring _is_accepted_host on the HTTP layer. Co-authored-by: pxdsgnco <14163800+pxdsgnco@users.noreply.github.com> |