Commit Graph

33 Commits

Author SHA1 Message Date
Teknium a1c25d393a feat(desktop): built-in optional-skills catalog in Capabilities → Skills with one-click install
The Skills tab now lists the entire official optional-skills catalog
(optional-skills/ shipped with the repo) below the installed skills.
Each catalog row has an Install button that routes through the standard
hub action pipeline; once the install finishes the row flips into the
installed list with the normal enabled/disabled toggle.

- backend: GET /api/skills/hub/official — OptionalSkillSource.list_local()
  scan (no network) + per-profile installed flags from the hub lock
- desktop: catalog section in SkillsView with search/scope integration,
  install-state spinners off $hubActions, and an OfficialSkillDetail pane
  (hub preview: frontmatter + full SKILL.md + Install)
- CapRow gains an optional action slot (button instead of the Switch)
- electron: route the new endpoint with the skills family (primary backend)
- i18n: officialCatalog/officialPill keys across en/ja/zh/zh-hant
2026-09-01 01:39:59 -07:00
Brooklyn Nicholson 0e7eebc266 fix(desktop): scope remote model catalog and primary-label REST to the focused profile
A shared dashboard's launch HERMES_HOME is not the selected profile. model.options now runs under @_profile_scoped, and global-remote REST keeps ?profile= even for the primary label.

Co-authored-by: fangliquanflq <fangliquan@qq.com>
2026-08-31 20:40:09 -05:00
Brooklyn Nicholson 90ee4460cb fix(desktop): translate sidebar recents_profile through SSH aliases
Managed SSH maps a Desktop profile label onto a different remote name.
The sidebar filter lives in recents_profile, so rewriting only ?profile=
left those reads on the remote default and the Sessions list came back empty.

Co-authored-by: noah <loahnisk@gmail.com>
2026-08-25 12:07:00 -05:00
Michael Nguyen 6eb77df1aa fix(desktop): route SSH media through active connection 2026-08-23 21:59:22 -07:00
hermes-seaeye[bot] f303c695d7 fmt(js): npm run fix on merge (#93300)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 23:14:28 +00:00
Andrew Fiebert f2c204d689 fix(desktop): stop transient remote ticket blips locking the chat
Post-boot WebSocket ticket mint failures and prolonged reconnects were
promoting into the full-screen "Hermes couldn't start" overlay, locking
users out of reading/drafting during brief 1–3 minute remote flaps.

- Ignore non-reauth boot-progress errors after a healthy cold boot
- Escalate prolonged transport reconnects with a non-blocking toast
- Soft-reset remote liveness rebuilds (no boot UI reset)
- Retry transient ws-ticket mints; auth rejections still fail fast
2026-08-23 16:09:23 -07:00
Axl Ibiza, MBA d0ea5f1722 fix(desktop): surface Nous Cloud 503 at the OAuth ticket-mint boundary
The original implementation classified 502/503/504 only inside the readiness
loop, but for OAuth-backed Cloud connections the WebSocket-ticket mint runs
before waitForHermesReady. A server fault there was wrapped by
gatewayTicketFailure into a generic message and the Cloud-down classifier was
never reached. This closes that boundary and fixes a latent regex defect.

- isServerSideHttpError: structured-first (err.statusCode for 502/503/504),
  legacy 'NNN:' prefix as fallback, non-Error inputs rejected. Also fixes the
  committed '\d' (double-escaped, matched a literal backslash) that made the
  function never detect a status prefix.
- makeNousCloudBackendDownError: single factory for the actionable Cloud-down
  error (isCloudBackendDown/statusCode/detail/cause), shared by both the
  ticket-mint boundary and readiness exhaustion.
- main.ts: run the Cloud classifier at mintGatewayWsTicket before the
  gatewayTicketFailure wrap; 401/403 still route to reauth.
- connection-config.ts: gatewayTicketFailure preserves an integer statusCode
  from the source error; auth semantics unchanged.
- boot-progress/IPC: carry isCloudBackendDown and statusCode through
  DesktopBootProgress so the renderer overlay (a PR-body promise) can key on
  the structured result rather than re-classifying the message string.

Tests: backend-health (structured detection, non-Error rejection, factory
shape/cause/guards, legacy fallback), connection-config (statusCode preserve,
401/403 reauth, integer-only copy), and an OAuth ticket-mint integration
regression (Cloud 503 -> actionable Cloud-down; 401 -> reauth). Connection-
config suite 80/80 green; backend-health sync tests green; the async readiness
loop tests cannot run on this host (pre-existing local-run limitation) and are
the CI gate. PR #85373 (#85335).
2026-08-21 19:09:40 -07:00
Teknium ac3d7ddae7 fix(desktop): skill hub installs on non-default profiles no longer 404, and failed installs surface
Three fixes for the "Install on this agent" pipeline, covering the whole
split-brain class between action-spawning endpoints and their status polls:

1. electron/connection-config.ts — the /api/actions/{name}/status poll family
   now routes to the same backend as every action-spawning route. Before,
   POST /api/skills/hub/install ran on the PRIMARY backend (scoped route)
   while the follow-up status poll for a non-default profile routed to the
   profile's POOLED backend, which never registered the dynamic action name
   (skills-install-<slug>-<hash> lives only in the spawning process's
   memory) -> 404 "Unknown action" toast even though the install succeeded.
   POST /api/mcp/catalog/install joins the scoped table for the same reason.

2. src/store/hub-actions.ts — a non-zero subprocess exit now rejects with the
   action log tail so the caller's catch toasts it. Before, a failed install
   (scan gate, network, bad identifier) stopped silently: no toast, no row
   flip, and the unchanged skills list read as "install did nothing".

3. src/contrib/runtime-loader.ts — a disk plugin copy shadowed by a bundled
   twin now publishes a visible "(stale disk copy)" inventory row carrying
   the folder path, instead of a console.info nobody sees. Stale
   desktop-plugins/ leftovers from dev deploys are the same folders that
   actively break the feature on shells without the bundled twin.
2026-08-19 02:42:34 -07:00
David Dudok de Wit 7245b022db feat(desktop): route REST through registered sources 2026-08-18 22:03:25 -07:00
embwl0x 55e34fb7d0 fix(desktop): avoid local profile REST backend spawns 2026-08-17 15:51:02 -07:00
Al Zilla 30299efa38 fix(desktop): open SSH default bots on the remote root profile
Clicking Mac Mini / Spark (the device default row) passed the desktop
pool key as the remote Hermes profile. That profile does not exist, so
the chat never opened. Named profiles (bob, dixie) already sent a real
name and worked.

Also refuse to fall back to this-device's default chat pin when the
remote source did not actually become active.
2026-08-17 14:32:52 -07:00
Ayush Nangia 15dd3bf586 fix(desktop): route remote sub-profiles through the primary's remote gateway
A URL-remote desktop whose PRIMARY profile has a per-profile remote
override lists the gateway's sub-profiles in the Bots pane, but
clicking one fell through the routing table's last case and spawned a
fresh local backend that shared nothing but the name (#88296).

resolveProfileBackendRoute now consults primaryRemoteActive: when the
primary's own backend is remote and the sub-profile has no stored
entry of its own, it routes through the primary gateway with profile
scoping (the same shared-primary flow global remote uses). Profiles
with their own local entries still pool locally.
2026-08-17 10:58:48 -07:00
Teknium 6cca9f3e71 fix(desktop): validate SSH host input and redact credentials in ssh target logging
A user typed their root password into the Desktop SSH host field
(root@IP:PASSWORD form). Three failures compounded:

1. validateSshTarget() only checked for option injection (leading dash),
   control chars, and port range — commas in an IP, whitespace ("ssh "
   prefix pastes), and non-numeric ":<segment>" leftovers all dialed ssh
   with garbage and failed silently five times.
2. normalizeSshConfig() only strips a ":<segment>" when it is numeric, so
   a pasted password stayed glued to the hostname all the way into ssh
   argv and the desktop.log connect line.
3. redactSecrets() had no pattern for ssh targets, so the password landed
   verbatim in desktop.log and then in a PUBLIC debug-share paste.

Changes:
- validateSshTarget(): reject whitespace, commas, non-numeric colon
  segments (with a "never put a password in the host field" hint that
  does NOT echo the credential), and garbage hostnames; still accepts
  bare IPv6 (::1, fe80::1%eth0). Reject whitespace/@ in user.
- redactSecrets(): new pattern masks any non-numeric segment where a
  port belongs in user@host:... strings — defense in depth so future
  parse gaps can't leak credentials into logs or debug shares.
- normalizeSshConfig(): strip a pasted leading "ssh " prefix.
- Tests for all three, including the exact incident shapes.
2026-08-17 01:54:41 -07:00
hermes-seaeye[bot] cecb3a6ed7 fmt(js): npm run fix on merge (#88206)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-17 06:37:33 +00:00
hermes-seaeye[bot] 1826310f49 fmt(js): npm run fix on merge (#88128)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-17 04:19:24 +00:00
Teknium b711fd0513 feat(desktop): carry remote gateway headers through the connections registry, test probes, and Settings UI
Completes PR #74468 (remote gateway headers for Cloudflare Access, #74466)
against the v2 multi-connection registry that landed after the PR was
authored, and closes the review blockers:

- connection-registry: additive optional `headers` field on remote/cloud
  entries (normalized through the same forbidden-name filter, secret
  envelopes like `token`); inherited on edit, treated as dial material by
  connectionDialFieldsChanged, preserved by normalizeRegistry, and carried
  through migrateV1ToRegistry. v2 registries without the field load
  unchanged — no version bump.
- main.ts registry paths: connectRegistryBackend dials with the entry's
  headers (readiness probe, ticket mint, descriptor REST via
  getJsonForBackend/fetchJsonForBackend, registry ws-url minting with
  rememberRemoteWsHeaders so renderer upgrades get them injected).
- saveRegistryConnection encrypts incoming plaintext header values with the
  same safeStorage/allowPlainText seam as tokens; sanitizeRegistryConnection
  exposes only header NAMES to the renderer — values never cross IPC.
- Connection tests exercise the leg they validate: both
  hermes:connection-config:test and hermes:connections:test now send the
  configured headers on the HTTP status call, the ws-ticket mint, AND the
  live WebSocket probe (probeGatewayWebSocket grew an injectable `headers`
  option passed as the undici WebSocket constructor's second argument).
- Settings → Connections gains an "Extra gateway headers" editor for
  remote/cloud entries (name + secret value rows, stored values shown as
  saved-but-hidden, clearable), with i18n keys (en + zh; other locales fall
  back through defineLocale).
2026-08-16 19:58:49 -07:00
tigercraft4 fcef62ef72 feat(desktop): support remote gateway headers 2026-08-16 19:58:49 -07:00
Jesse Panganiban 307d46c207 fix(desktop): map SSH profile aliases in REST paths 2026-08-16 19:28:56 -07:00
Teknium 4ee87f76ee fix(desktop): read cron run-history from the owning gateway
When Hermes Desktop works against a REGISTERED gateway connection, cron
jobs execute on that gateway and persist their run sessions in the
gateway's state.db. But every REST call in the app — the cron surface
included — carried only `profile`, so `hermes:api` routed it through the
local profile pool and `_list_cron_job_runs_sync` read a local state.db
with zero `source='cron'` rows. Every job showed "No runs yet" while the
same endpoint on the gateway returned the real runs (#87882).

Fix at the routing seam:

- HermesApiRequest gains an optional `connectionId`. The renderer's cron
  helpers (list/get/runs/delivery-targets/create/update/pause/resume/
  trigger/delete/blueprints) now tag the active registry connection via a
  new connectionScoped() twin of profileScoped(), fed from the same
  setApiRequestConnection seam store/gateway already maintains for the
  plugin socket.
- The hermes:api main-process handler resolves a tagged request through
  ensureRegistryBackend — the SAME pool the job list and WS traffic use —
  instead of the legacy profile route. Shared remote/cloud hosts (one
  gateway, many profiles) get the path scoped with ?profile= via the new
  pathWithProfileScope helper, factored out of pathWithGlobalRemoteProfile.
- '' / 'local' / absent connectionId keep the byte-identical v1 route, so
  single-source and connection-config-remote users are unaffected.

This covers the run-history panel, the sidebar cron peek, and every other
cron surface in one place, since they all funnel through the same helpers.

Fixes #87882
2026-08-16 16:27:16 -07:00
Teknium fceab28602 fix(desktop): cold start no longer hides Cloud agents until Portal re-login
Fixes #73495. Two cold-start defects made the configured Hermes Cloud
agent vanish after a Desktop restart even though the persisted Portal
session was still renewable:

1. hasLivePortalSession() trusted the FIRST cookies.get() on the lazy
   `persist:` partition. It now reuses the warmOauthCookieStore()
   warm-up + bounded reread that hasLiveOauthSession() gained in
   PR #67769, so a single hydration false-negative no longer clears the
   agent list and flips the panel to signed-out.

2. Discovery required the short-lived `privy-token` access cookie but
   treated its absence as a full interactive re-login, even when the
   30-day `privy-session` / `privy-refresh-token` renewal cookies
   survived the process exit. New cookiesHavePrivyAccessToken() splits
   "signed in (renewable)" from "discovery can succeed right now";
   discoverCloudAgents() and cloudAgentSilentSignIn() now mint a fresh
   access token via one bounded, hidden, deadline-capped portal load
   (renewPortalAccessSilently) before or after a 401, and only surface
   needsCloudLogin when renewal genuinely cannot complete.

PRIVY_SESSION_COOKIE_VARIANTS also learns `privy-refresh-token` so a
renewal-only jar still counts as signed in rather than demanding an
interactive login while usable refresh material sits in the partition.

Tests: connection-config.test.ts covers the access/session split,
including the exact renewal-only cold-start jar from the issue repro.
2026-08-14 20:33:39 -07:00
Cad from Arca 6d3cb23d24 fix(desktop): reject reserved remote profile names 2026-08-01 14:30:16 -07:00
Cad from Arca d6be88fbc8 fix(desktop): map SSH profiles to remote profiles 2026-08-01 14:30:16 -07:00
teknium1 087b2230c4 fix(desktop): accept scheme-less host:port in the remote gateway URL field
Users pasting a Tailscale IP or LAN host as 'host:port' (no http://) hit
either a hard 'URL is not valid' error in the main process or, worse, a
silent dead probe in the renderer: the ^https?:// gates in the settings
and first-run forms never fired, so the field sat idle with no feedback.

- normalizeRemoteBaseUrl() (electron/connection-config.ts) now prepends
  http:// when the input has no scheme:// prefix; explicit non-http
  schemes (ws://, ftp://) still reach the protocol check and get a clear
  rejection.
- New renderer twin coerceRemoteUrlScheme() (src/lib/remote-url.ts),
  wired into both probe gates (gateway-settings.tsx and
  first-run-remote-form.tsx) so the debounced /api/status probe, sign-in,
  test, and save all see the coerced URL.
- Tests for both sides (electron/connection-config.test.ts,
  src/lib/remote-url.test.ts).
2026-07-31 22:28:43 -07:00
Brooklyn Nicholson 97a8034dfd refactor(desktop): resolve profile backend routing from one table
Three helpers each re-derived part of the same decision: which backend
serves profile P, and does its REST path need a `?profile=` scope.
profileUsesPrimaryBackend answered the first half, pathWithGlobalRemoteProfile
answered the second, and ensureBackend re-checked globalRemoteActive() around
both. Splitting one table across three predicates is how the global-remote
case ended up registering reapable pool entries for a backend it never owned.

resolveProfileBackendRoute() states the four routes in one place and returns
the backend, the descriptor scope, and whether the path needs a query
parameter. The call sites read the answer instead of recomputing it.

One behavior change falls out: `hermes:api` now passes the primary profile
through, so the primary no longer sends itself a redundant `?profile=<self>`
on a global remote that already serves it.
2026-07-27 13:44:11 -05:00
Gille 3884e0eea0 fix(desktop): reuse global remote backend across profiles
Keep non-primary profiles that inherit the app-global remote on the primary connection descriptor instead of creating processless pool entries that the idle reaper repeatedly removes.

Preserve per-profile remote overrides and local pooled backends, and cover the routing policy with behavioral tests.

Co-authored-by: Rodrigo Fernandez <rodrigo@nxtlevelsaas.com>
2026-07-27 13:41:59 -05:00
yoniebans 25f4ba7b3c Merge remote-tracking branch 'origin/main' into feat/desktop-remote-ssh-current
# Conflicts:
#	apps/desktop/electron/connection-config.test.ts
#	apps/desktop/electron/connection-config.ts
#	apps/desktop/electron/main.ts
2026-07-21 10:16:01 +02:00
Gille 272bbaf792 fix(desktop): avoid false remote gateway reauthentication (#68250)
* fix(desktop): avoid false remote gateway reauthentication

Co-authored-by: Rod-fernandez <rodrigo@nxtlevelsaas.com>
Co-authored-by: David Andrews (LexGenius.ai) <david@lexgenius.ai>

* fix(desktop): harden remote revalidation state

---------

Co-authored-by: Rod-fernandez <rodrigo@nxtlevelsaas.com>
Co-authored-by: David Andrews (LexGenius.ai) <david@lexgenius.ai>
2026-07-20 20:54:36 -04:00
yoniebans faa3bce6dc style(desktop): satisfy merged eslint/prettier config
The SSH modules predate the stricter lint config that landed on main (curly, no-empty, perfectionist sorting, prettier). Mechanical lint:fix + fmt pass, empty catch blocks filled with the codebase's void-0 convention, and inline no-control-regex disables on the three deliberate control-char patterns (same pattern as lib/ansi.ts).
2026-07-20 23:01:49 +02:00
yoniebans f003d888e1 feat(desktop): integrate SSH with soft gateway switching
Wire Cloud-aware SSH persistence, authenticated backend reuse, scoped transport identity, deterministic apply serialization, Files cache isolation, terminal routing, recovery classification, and orderly soft-apply/quit teardown.
2026-07-16 14:43:14 +02:00
yoniebans a6113b4229 feat(desktop): add SSH to the Cloud-aware connection model
Add SSH as a separate saved connection shape while preserving Cloud URL/OAuth semantics, inactive SSH drafts, strict host/port normalization, and profile-specific precedence.
2026-07-16 14:43:14 +02:00
Ben c101207b99 feat(desktop): Hermes Cloud connection mode — one sign-in, agent discovery, silent connect
Adds a third "Hermes Cloud" gateway mode to the desktop app: one portal
sign-in auto-discovers the agents on your account and connects to any of
them with no second interactive prompt.

- Electron: widen connection mode to 'local' | 'remote' | 'cloud', routed
  through a centralized modeIsRemoteLike() so every resolution site treats
  cloud exactly like remote; portal discovery (GET /api/agents over the
  OAuth partition), Privy-cookie liveness, multi-org picker (NAS 409), and a
  silent per-agent /oauth cascade (load protected root, not /login).
- Persist a cloudOrg on the cloud block; unselect cloud on mode switch.
- Renderer: Hermes Cloud ModeCard + agent picker (signed-out/loading/empty/
  list), org picker, Change-org, connected-highlight + Connected pill.
- i18n (en + zh full; ja/zh-hant inherit via defineLocale), Cloud icon.
- IPC: hermes☁️{status,login,logout,discover,agent-sign-in}.

Salvage of #55402 onto current main: the original branch predates the
desktop electron .cjs -> .ts migration (39d09453f), so the electron half
was re-authored against the .ts files. Authorship preserved.

cloud-auto-discovery Phases 3 + 4.
2026-07-10 01:37:43 -05:00
ethernet 56a8e81d33 cleanup(desktop): npm run fix for fmtting
we should run this as part of merges at some point :)
2026-07-08 16:24:16 -07:00
ethernet 39d09453f9 feat(desktop): ts-ify everything 2026-07-08 16:24:16 -07:00