Commit Graph

1575 Commits

Author SHA1 Message Date
Teknium
3861f1da62 fix(web): pick own key or Nous Tool Gateway per web capability in Desktop
Desktop's web picker had no way to send one capability through the Nous
Tool Gateway while the other used your own key. "Use for Search/Extract"
on the Nous Subscription row wrote the row's vendor string (firecrawl)
into web.<cap>_backend, which the runtime reads as BYOK Firecrawl: the
gateway was never used, and the managed and BYOK rows lit up together.

- The managed per-capability pick now stores `nous`; search/extract
  dispatch, the registry and the Firecrawl client resolve the route per
  capability (search: managed Perplexity, extract: managed Firecrawl).
- A whole-toolset Nous pick clears stale per-capability pins (they
  outranked the new selection).
- /api/tools/toolsets/web/config reports search_via_nous /
  extract_via_nous and each row's managed_nous_feature; the panel lights
  the right row, labels the route, and refetches after a pick instead of
  mirroring the vendor string.

Fixes #100513. Builds on the approach in #100539 by @liuhao1024 and the
self-healing pin clear from #122676 by @praggybuilds.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Murphy (praggybuilds) <praggybuilds@users.noreply.github.com>
2026-10-06 02:53:56 -07:00
Teknium
f9798bcf9b fix(tts): validate a plugin's stream rate once; keep nous off plugin routing
Independent-review findings on the plugin PCM streaming path:

- stream_sample_rate was checked, then re-read and int()-ed in the
  adapter: NaN/inf raised out of resolve_streaming_provider (the speaker
  then played nothing) and 0.5 became sample_rate=0. Validate once
  (finite, >= 1 Hz) and hand that int to _PluginPCMStreamer.
- `nous` is an alias of the openai path in _get_provider but not a
  reserved name, so a pinned `tts.streaming.provider: nous` could stream
  a plugin registered as `nous` while the sync path speaks OpenAI.
- Document that stream() must be thread-safe: the speaker prefetches up
  to 3 sentences, unlike the serialized synthesize() path.
2026-10-06 00:52:41 -07:00
Teknium
10f5530967 fix(tts): plugin PCM streamers keep the sync dispatcher's voice and routing
Follow-up to the salvaged plugin-streaming commit. The adapter read
voice/model from the plugin's own tts.<name> section, so a streamed reply
spoke the plugin default while synthesize() got tts.voice: the voice
changed with the surface. It also skipped the sync dispatcher's routing:
a same-named `type: command` provider (which wins for synthesize) lost
to the plugin on the streaming path, and an is_available() == False
plugin was streamed anyway.

- tools/tts_tool_plugins: one routing guard (_plugin_route_key) and one
  voice/model/speed builder (_plugin_voice_kwargs), shared by
  synthesize() dispatch and the new _plugin_pcm_streaming_provider
  (streams_pcm + positive stream_sample_rate + is_available(), read at
  resolve time)
- tools/tts_streaming: _PluginPCMStreamer stays a thin adapter;
  stream_channels/stream_sample_width dropped (the contract is int16
  mono, which every sink assumes)
- agent/tts_provider: streams_pcm / stream_sample_rate declared on the
  ABC with the contract
- tests: two invariant tests next to the plugin dispatch tests (the
  streaming test module skips without numpy); docs for plugin authors
2026-10-06 00:52:41 -07:00
John Paul Soliva
fbe93e7112 docs(voice): voice-channel access follows DISCORD_ALLOWED_ROLES too
(cherry picked from commit 7976b4aae1826a4e234f3f2a7cf796cb5b7ae98e)
2026-10-05 20:48:50 -04:00
Teknium
63f5e0ea45 feat(computer-use): pluggable single-select computer-use providers
Third-party drivers (e.g. a Codex-style computer-use reimplementation) could
only replace cua-driver by patching core. computer_use now follows the
single-select provider model Hermes uses for memory providers, context
engines and cron schedulers:

- plugins/computer_use/__init__.py discovers bundled
  plugins/computer_use/<name>/ then user $HERMES_HOME/plugins/<name>/
  (marker: register_computer_use_provider / ComputerUseProvider) and loads
  ONLY the provider computer_use.backend names (default "cua"), read per
  call so each profile gets its own. Unselected providers are never
  imported; listing reads plugin.yaml only. An unknown/unloadable name
  returns an error naming it and the installed providers; no fallback.
- The built-in cua-driver is an ordinary bundled provider at
  plugins/computer_use/cua/ (driver implementation stays in
  tools/computer_use). tools/computer_use/backend.py gains
  ComputerUseProvider (create_backend(permission_mode=...), is_available(),
  optional doctor()).
- The tool's check_fn and backend construction go through the selected
  provider; the model-facing schema is unchanged for every provider
  (prompt-cache invariant). The test-only HERMES_COMPUTER_USE_BACKEND env
  switch is gone (tests patch _new_backend).
- General plugin discovery treats computer-use providers as exclusive
  (not eagerly imported, not listed as toggleable) and skips the bundled
  computer_use/ category, same as memory/, context_engine/, cron_providers/.
- hermes tools and the Desktop toolset panel list installed providers next
  to cua; computer-use doctor/status/permissions and the Desktop status
  payload skip cua-driver/TCC probes when another provider is selected.
2026-10-05 17:20:30 -07:00
Teknium
7b2ff7a7d4 feat(plugins): inject_message(origin=...) starts a gateway session in the plugin's own profile
`ctx.inject_message` could only reach an existing gateway session (`session_key`), so an
orchestrator plugin that opens a chat/thread itself (a bot-authored Discord forum post never
comes back as an inbound message) had no way to get an agent turn there.

`origin` takes the SessionSource.to_dict() shape. The gateway creates (or continues) that chat's
session through the adapter's normal message path, in the profile whose plugin manager loaded the
plugin, and the reply goes back to the origin chat:

- Profile: resolved from the manager's immutable home, never an ambient one. The origin may not
  name another profile, the gateway's own routing for that chat through that profile's adapter
  must land in the plugin's profile, and the profile needs its own connected adapter for the
  platform (another profile's bot is never borrowed). Refusals are synchronous False + one log line.
- Authorization: same current-allowlist check as the session_key path (no bypass for host code).
- Multiplex: handle_message runs inside the profile's runtime scope so the spawned turn AND the
  reply send after it inherit that profile's home and secrets (applies to the session_key path too,
  which used to send from the default loop context).
- allow_gateway_injection is read from the plugin's own profile config.
- Plugin/system turns (event.internal) skip the human first-contact notes (onboarding intro and
  the "no home channel" nudge), which were posted into the plugin's fresh thread.

Requested by canonicle-desk in #132609.

Closes #132609
2026-10-05 17:11:30 -07:00
unsupportedpastels
932210ec2a fix(discord): a /voice join to another text channel drops speech the receiver still holds for the old one
The receiver buffers PCM per speaker with no conversation identity, and a same-voice-channel /voice join keeps that receiver. The poll and the leave flush then read the binding after draining, so audio captured for channel A was stamped with B and passed the post-STT check. The gateway join handler now discards the receiver's unemitted audio, under its lock, before moving the binding to a different text channel; a first join or same-channel rejoin keeps it. Nothing awaits between the discard and the binding write.

Reported by andrexibiza (F1) on #133471 and #130312.
2026-10-05 20:06:54 -04:00
John Paul Soliva
fb49ef18f5 fix(discord): capture the voice binding when a poll batch is collected
The listen loop transcribes a check_silence() batch serially, so a later
utterance read its binding after a /voice join landed during an earlier
utterance's STT and was posted to the new channel. Capture the binding
once per batch (and per leave flush) and pass it in.

(cherry picked from commit e4b020a0fd21833a89fc1fa7f904db489d797d9c)
2026-10-05 20:06:54 -04:00
John Paul Soliva
118a008f9b fix(discord): an utterance transcribed across a /voice join rebind is not posted to the new channel
_process_voice_input awaits WAV conversion and STT, then calls the
gateway callback with only guild, user and transcript, and the callback
routes by the guild's binding as it is at that moment. A `/voice join`
from another text channel while the bot is already connected rewrites
_voice_text_channels and _voice_sources, so an utterance captured for
the old conversation became a turn in the new one, with its channel
prompt, bound skills, transcript echo and replies.

The bound text channel is captured when the utterance is accepted and
compared after STT; if the binding moved, the stale work is dropped and
logged. Rejoining from the same channel keeps the conversation and is
dispatched as before. Nothing awaits between that check and the
callback's own read of the binding.

Reported by andrexibiza (F6) on #128788.

(cherry picked from commit 47d515eb4e1633945212e42d6cf77aba41906ea8)
2026-10-05 20:06:54 -04:00
Teknium
03f545ebcf fix(memory): catalog memory-provider installs stop failing on every start and every Install click
Two install routes refused a catalog memory provider that config already
names (memory.provider), so the install could never succeed and was
retried on every agent start / every click — the hindsight (91%) and
mnemosyne (87%) failed rows in hermes.extension.install.count.

1. Agent init (`recover_at_startup`) installs the configured provider
   through the consent gate. Under the classic CLI that gate's input()
   runs on the agent-build thread behind prompt_toolkit, so the turn
   hangs on "Initializing agent..." forever; with no terminal (Desktop,
   gateway, `chat -q`) it is refused — "Install declined: dependency
   install skipped (non-interactive)" — and recorded failed on every
   process start. Agent init now never asks: a provider that shipped in
   core installs with its built-in's consent (security.allow_lazy_installs
   still gates it), any other catalog provider is not attempted and the
   user is told the one command that installs it.

2. The dashboard/Desktop Install click (`dashboard_install_plugin`) on a
   plugin memory.provider already selects hits the publication veto that
   is meant for replacing an active plugin, so it is refused every time
   while the same plugin unselected installs and enables. The click now
   carries the consent the fresh-install enable already acts on; a forced
   reinstall keeps the veto.
2026-10-05 16:06:32 -07:00
Teknium
59224a20f0 fix(plugins): only a real memory-provider tree selects memory.provider on install
Catalog category "memory" also holds hook plugins, context engines and
skill packages. Deciding from the category made `--enable` overwrite the
user's memory.provider with an unloadable name and never enable the plugin.
Decide from the installed tree (_is_memory_provider_dir), and save
memory.provider only when find_provider_dir resolves the name.
2026-10-05 15:38:19 -07:00
Teknium
dc81e17f0a docs(memory): memory providers activate via memory.provider, not plugins enable
The install prompt for a memory provider now sets memory.provider; say so on
the Hindsight setup and plugin install pages and drop the claim that the
plugin is enabled under plugins.enabled.
2026-10-05 15:38:19 -07:00
Teknium
8079fd01f4 fix(memory): stop false drift refusals; refused writes name the exact fix
Fleet telemetry (14d, v0.21.5) shows the builtin store returning
success=False on 34-47% of add/replace/remove calls. Live `hermes chat -q`
runs against origin/main reproduced the dominant causes:

- Drift guard refused every replace/remove/batch on a file whose only
  difference from the tool's own layout was whitespace (blank lines
  around the delimiter, an empty entry). Parsing only strips whitespace,
  so flushing such a file loses nothing; the round-trip signal could not
  protect any content and turned the store read-only until hand-fixed
  (#107270). The guard now fires only on the signal that can actually
  lose foreign text: one entry larger than the whole store's limit.
- An over-budget add/replace/batch said "would exceed the limit" but not
  by how much, so models retried with consolidations that were still too
  big. Every over-budget message now states the exact chars to free and
  points at the one-batch fix.
- An old_text that matched nothing (paraphrased, stale after a concurrent
  write, or several entries glued with the delimiter) returned no hint on
  the batch path. Zero-match errors now carry `closest_entries` (top word
  overlap) on both paths, and flag a delimiter-spanning old_text.
- A batch replace missing its own `content` now says to set it on the
  operation object.

Fixes #107270. Same diagnosis as #107278 (@KoNit-K) and #61612 (@webtecnica),
which normalized the round-trip comparison instead of dropping it.
2026-10-05 15:36:45 -07:00
Brooklyn Nicholson
4f91c5a462 fix(stt): opt-in 16 kHz mono normalization for command STT providers
Command STT providers received the raw audio file with no format
normalization: _transcribe_command_stt only checked .exists() before
substituting {input_path}. Desktop voice recordings arrive as WebM/Opus
48 kHz and providers with container/sample-rate contracts (e.g. Tencent
16k_zh) reject the raw webm, so desktop voice chat failed on every
message. _transcode_audio_for_stt (ffmpeg -> 16 kHz mono m4a) existed
but was wired only into the OpenAI path's rejection retry.

Add stt.providers.<name>.normalize (default off, so whisper-style CLIs
that accept any container keep the original file): when truthy, the
input is transcoded into the per-call temp dir and {input_path} points
at the normalized file. Transcode failure returns an error envelope
instead of silently feeding the raw container to a provider the user
declared needs normalization.

Fixes #81811

Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
2026-10-05 16:40:15 -05:00
unsupportedpastels
c83fe1b2c4 docs(dashboard): name what an untrusted proxy means for login limits 2026-10-05 12:23:21 -04:00
Teknium
be5481690d feat(gateway): reshape post_gateway_admission into a fail-open consume hook
Teknium ruled the post-admission consumer hook (#129958) must fail OPEN, so
reshape the salvaged implementation into one small seam:

- `{"action": "handled", "reply": str|None}` is the only decisive result;
  None, any other value, a raising callback or a timeout runs the ordinary
  agent turn. One buggy plugin can no longer blackhole every inbound
  message on a profile (the cherry-picked version failed closed on any
  exception or malformed return from ANY registered callback).
- Fire under the message's ROUTED profile scope. The receiving bot's
  handler binds the transport home so auth can read its .env; when that
  bot serves a chat for another profile, the ambient scope at the hook was
  the wrong profile and that profile's consumers never ran.
- Snapshot-only payload (session_key, platform, source dict, message_id,
  text): no live gateway runner / session store handles, so the plugin
  isolation host needs no HOST_DEGRADED_HOOKS entry.
- Bounded by plugins.hook_callback_timeout (fail-open) instead of the
  fail-closed policy-hook set; the required-but-discarded receipt, alias
  table, destination snapshot/restore machinery and hard-coded English
  failure notice are gone.
- Tests trimmed to three invariants through the real _handle_message and
  real plugin dispatch: consume path, raise path proceeds, routed profile
  scope bound.
2026-10-05 01:26:23 -07:00
JerryLiu369
b63305f379 feat(gateway): profile-scoped post-admission consuming gateway plugin extension (#129958)
Closes #129958
2026-10-05 01:26:23 -07:00
Teknium
c958e1a7b9 feat(plugins): on_human_input_request / on_human_input_resolved hooks
One observer pair fires wherever the agent blocks waiting on a person, so a
notifier plugin no longer needs a hook per prompt type or a `sudo` regex over
terminal commands (#132333, consumer: pushover-hermes-plugin).

Fire sites are the existing choke points every surface already routes through:
- sudo: tools/terminal_tool_sudo.py::_prompt_for_sudo_password (CLI callback,
  Ink TUI/Desktop sudo.request card, /dev/tty fallback)
- clarify: tools/clarify_tool.py::clarify_tool (CLI, TUI/Desktop, gateway)
- approval: tools/approval_prompt.py::prompt_dangerous_approval (CLI/TUI/ACP
  callback and input() paths), ::_present_with_selected_transport (plugin
  transports), tools/approval_gateway_wait.py::_await_gateway_decision
  (gateway platforms)

Payload: kind, request_id (shared by the pair), session_id, session_key,
platform, prompt; resolved adds outcome. prompt is always force-redacted and
the typed password / clarify answers are never passed. Smart (aux-LLM)
approvals and coalesced gateway followers do not fire it: no new human prompt.

Closes #132333
2026-10-05 01:15:16 -07:00
liuhao1024
0df1837e81 fix(approval): fire observer hooks for the two hookless classic-CLI prompts
The protected agent-instruction write prompt and the MCP/vault consent
prompt called prompt_dangerous_approval directly on the classic CLI
without firing pre_approval_request / post_approval_response, so
observer plugins (notifiers, audit loggers) could not see those human
waits in the terminal even though their gateway twins were visible.

Fire both hooks around the CLI prompt with the same payload shape the
gateway branch builds (pattern_key protected_instruction_file /
mcp_elicitation, surface="cli" or the caller's consent surface); the
post hook fires in a finally so the wait always settles for observers
even when the panel itself raises.

Also document the consent surface values (mcp-elicitation/<server>,
mcp-trust/<server>, vault-payment) and that the Ink TUI/Desktop report
surface="gateway" today.

Fixes #131876
2026-10-05 00:23:41 -07:00
teknium1
639eee3058 docs(plugin-catalog): README and docs pages describe the same delisting, removal and entry fields 2026-10-04 04:35:58 -07:00
teknium1
0f7a95f16d docs: Home Assistant moves to the plugin catalog 2026-10-04 00:08:20 -07:00
teknium1
64ad33e32d refactor: remove Skill Sync (hermes sync) and the org skill mirror from core
Skill Sync only worked against a Nous Portal sync plane that was never made
publicly accessible, so no install has ever used it. We don't want it in core
right now, so this deletes it outright instead of moving it behind a plugin
hook surface nobody needs yet:

- `hermes sync` command, `tools/skills_sync_client*.py` and their tests
- the personal pull ticks (gateway housekeeping, classic CLI startup,
  `hermes serve` maintenance) and the debounced post-write push
- the per-skill `sync` usage flag (`set_sync` / `is_sync_enabled`)
- the `skills/_org/<org_id>/` mirror: index gating on `.active_org`, the
  `[org-shared]` / name-collision labels, the org provenance header and
  `org_provenance` field on skill_view, the org delete guard and the
  auto-propose `org_sharing` note on skill_manage

The code is preserved in the private NousResearch/hermes-skill-sync repo if
the feature comes back.
2026-10-03 23:05:48 -07:00
Lester Liang
c225c4a04e fix(cli): finalize one-shot Relay sessions (#79471)
hermes -z ends its turn but leaves the Relay root conversation open:
run_conversation keeps the root resumable and the one-shot exit is
os._exit, which skips the atexit finalizer every other CLI path relies
on. Exported Relay telemetry showed session start + turn start/end with
no session end, and on_session_finalize never fired for one-shot runs.

_run_agent now keeps the session id at turn entry (compression can
rotate agent.session_id without opening a second root) and _close_agent
calls finalize_session for it after the background-completion linger and
before memory/agent teardown, on failure as well as success.

Co-authored-by: Soheil Fakour <soheil.fakour@gmail.com>
2026-10-03 19:01:42 -07:00
John Paul Soliva
ddde067010 fix(plugins): a forced reinstall of the same source keeps the user's files
install --force --ref is the documented way to move a pinned plugin, but it replaced the whole plugin directory: untracked and git-ignored files (plugin state, local config) were deleted and tracked edits were lost with no copy. A reinstall from the source the plugin was installed from now carries user files like update does and copies tracked edits to plugins-backup/. A different source still starts clean.
2026-10-03 21:40:42 -04:00
kshitijk4poor
8e54341152 docs(skills): state one precedence for duplicate skill names
The skills page said local skills win over external_dirs in one place and
listed project > local > external_dirs (no create_dir) in another, while
skill_view refused both. Document the single order every surface now uses
(project > local > skills.create_dir > external_dirs), how same-directory
duplicates are listed and loaded, and that plugin:skill names are unaffected.
2026-10-04 02:54:56 +05:30
kshitijk4poor
343500b354 refactor(web): drop the gate's leftovers and keep the tier rules tested
Review and simplify pass on the guest fast-search restore.

- tools/tool_backend_helpers.py: _account_info was split out so two gates
  could share one account read. Only managed_nous_tools_enabled is left, so
  fold it back into its pre-salvage shape. Keep the branched force_fresh
  call, because tests stub the reader with zero-arg lambdas.
- plugins/web/perplexity/provider.py: the managed= parameter on
  _managed_gateway only fed the removed refusal helper. search() now calls
  resolve_free_search_gateway() directly once it has the route answer. The
  module docstring now names guests.
- hermes_cli/nous_account.py: the _error_info docstring described the
  removed gate. The tier carried on a failed lookup still picks the "needs
  a Nous account" copy over billing or re-login guidance.
- Tests: the case-insensitive tier match and the tier on error snapshots
  lost their only coverage when the gate tests went, so cover them at
  their consumer, format_nous_portal_entitlement_message. The fast-search
  premise now asserts that the guest case really reads as the anonymous
  tier. Remove the dead exp_offset parameter.
- Docs: the Perplexity section said there is no anonymous tier, which is
  true for direct keys only. It now names the managed guest route.
2026-10-04 00:10:54 +05:30
kshitijk4poor
96ac3d349f fix(web): restore free fast search for anonymous guest users
The free managed Perplexity fast-search route exists for anonymous guest
Portal users. The salvage of #130121 (cee3000c24, via #130660) added a
registered-account gate, fast_search_entitled(), that excluded the
anonymous tier. That defeated the original change: guests fell back to
the keyless ring and never sent a gateway request.

This removes the gate, so resolve_free_search_gateway() decides on
identity alone again, as rewbs wrote it. The guest-disabled, expiry
refresh and dead-guest rules still apply through the token reader. Every
other vendor, and the billed managed-Firecrawl fallback, keep the paid
entitlement gate.

Kept from the salvage: the case-insensitive is_anonymous_tier, the
account tier carried on error snapshots, and the single route walk per
search dispatch. Tests: the anonymous identity is back in the autodetect
and rescue cases. The tests that pinned the guest exclusion are removed.
2026-10-04 00:10:54 +05:30
teknium1
33eedf29c9 docs(plugins): pin plugins.isolation in the managed scope for shared deployments
plugins.isolation is profile config, so a profile (or its agent) can set it
back to in_process. The managed scope already beats profile config and makes
`hermes config set` refuse, and the isolation reader goes through that
overlay; document it as the operator lock and pin the contract with a test
(red when isolation_mode() reads the raw profile file).
2026-10-03 10:15:11 -07:00
teknium1
17a7a9cb6b docs(plugins): plugin host isolation mode and its invariants 2026-10-03 10:15:11 -07:00
kshitijk4poor
b90c5c71cb docs(goal): document the /goal -- <text> escape for control-word goals
Control verbs with trailing words now run the verb instead of replacing
the goal, so goal text that starts with one of those words needs the
`--` escape. Without a docs line the change is only discoverable from the
in-chat hint.
2026-10-03 20:44:07 +05:30
FroRaut
c84ef16384 fix(cron): unreachable-model re-runs no longer use up repeat slots
A recurring job with a finite repeat count (for example repeat=3, every
24h) lost one repeat slot for each automatic re-run after a fire that
could not reach the model. The failed occurrence counted, and then each
5/15/30-minute re-run of that occurrence counted again, so a half-hour
outage finished a repeat=3 job with zero model calls. With
cron.retry_unreachable off, the same outage costs only the one failed
occurrence.

Cause: cron/jobs.py:_advance_after_run bumps repeat.completed on every
mark_job_run, and the re-runs parked by
cron/unreachable_retry.py:plan_retry go through it like scheduled
occurrences. cron/unreachable_retry.py:will_retry also treated a re-run
on the last slot as the job's final run.

Fix: cron/scheduler.py:_finish_completed_run passes ladder_rung=True
when the dispatched instant is the parked re-run instant (new
is_retry_run, which compares the dispatch snapshot's _scheduled_instant
with the ladder state). mark_job_run then calls _advance_after_run with
count_run=False, and will_retry leaves that run out of its final-run
check the same way. The first failed occurrence still counts, and manual
runs still count.

(cherry picked from commit 27038f7064bb611811efee8c527dddecd4fcb57b)
2026-10-03 16:31:54 +05:30
John Paul Soliva
001a3d0203 fix(cron): a bare custom base_url takes a stored key only at a configured origin
The bare 'custom' arm of _validate_cron_base_url returned early as pure BYOK, but the resolver attaches the host-gated env keys (OPENAI, OPENROUTER, OLLAMA, <VENDOR>_API_KEY) to such a job by hostname. A job could pair one with another scheme, port or subdomain. When a stored key would go with the base_url, require the origin of a configured endpoint or a registry provider; a base_url no stored key matches stays unrestricted.

(cherry picked from commit bb7c37c8d7b88077acd45de74f1efb1501adca1a)
2026-10-03 16:31:09 +05:30
John Paul Soliva
1903b2b6e3 fix(cron): bind a stored provider key to its configured origin, not its hostname
_validate_cron_base_url accepted a job base_url whose hostname matched
the provider's endpoint or was a subdomain of it, so a cronjob tool call
could pair the stored key with plain HTTP, another port, or a subdomain.
Compare the full origin (scheme, host, effective port) via
utils.base_url_origin, the rule the other secret-to-new-URL checks use.
The scheduler backstop calls the same validator, so every surface that
stores a job is checked before the key is sent.

(cherry picked from commit a281efeedac02f87e168743b389b78f6feaf5472)
2026-10-03 16:31:09 +05:30
kshitijk4poor
7b6a6c595f docs(codex): note that Ultra reaches the app-server as codex's own mode 2026-10-03 16:29:34 +05:30
kshitijk4poor
c1541ff4ca docs(codex): reasoning effort and /fast reach the app-server runtime 2026-10-03 16:29:34 +05:30
John Paul Soliva
c30ec25ec7 fix(proxy): judge Sec-Fetch-Site and compare Origin by host and port
Browsers send no Origin on a GET, so a cross-site page (or a DNS-rebound
page on a wildcard bind) could still reach /v1/models with the operator's
credential. Sec-Fetch-Site, which a page cannot forge, now refuses those.
The Origin check compares the parsed host and port instead of the raw
string, so an Origin that omits the default :80 or spells an IPv6
address differently is matched the way a browser means it.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
(cherry picked from commit 72bf4e9c34f6c6200658e42345f775291462697c)
2026-10-03 16:29:17 +05:30
John Paul Soliva
b7ea7a7759 fix(proxy): refuse every browser Origin on a wildcard bind
A wildcard bind accepts any Host name, so a DNS-rebound page reaches it
with Origin equal to its own Host and the same-origin rule let it through
to the operator's credential. A wildcard listener has no origin of its
own, so an Origin-bearing request is refused there; API clients without
Origin keep working. Also treat the absolute form localhost. as localhost
and detect expanded IPv6 unspecified binds as wildcard.

(cherry picked from commit 5ce7aac4d7e4d160b2a6447f0442e747f489d72f)
2026-10-03 16:29:17 +05:30
John Paul Soliva
4b1ff38e51 fix(proxy): accept only local Host names and no foreign browser Origin
hermes proxy attaches the operator's subscription credential to every
forwarded request but accepted any Host header and any Origin. A
loopback or specific-IP bind now serves only its own Host names, and a
request whose Origin is not the proxy's own is refused with 403.
Non-browser clients send no Origin and are unaffected; a wildcard bind
keeps accepting any Host.

(cherry picked from commit 3cf3870cfc2a795e439b1a8c0c53424f79c24505)
2026-10-03 16:29:17 +05:30
John Paul Soliva
0ef09aed76 fix(dashboard): learning-node and skill editor writes honour ?profile=
DELETE/PUT /api/learning/node, POST /api/skills and PUT /api/skills/content
read the target profile from the JSON body only. A shared-backend Desktop
(global remote, registry connections) scopes these calls with ?profile= and
sends a body without one, so the star map's delete archived, and its edit
rewrote, the same-named skill of the backend's launch profile while the
selected profile's skill stayed as it was.

Take the body profile first and fall back to the query, as
PUT /api/skills/toggle and the tools routes already do.
2026-10-02 22:34:58 -05:00
fangliquanflq
942dd4fb0f test(hooks): cover crashed fail-closed process 2026-10-02 20:18:26 -05:00
teknium1
f6f6befbcf fix(fallback): a bare llama.cpp alias resolves only to the live managed server
Completes the managed-endpoint rung for every caller that resolves a bare
llamacpp / llama.cpp / llama-cpp alias outside the main ladder:

- The rung moves from _resolve_custom_branch to the resolve_provider_client
  entry and to _get_cached_client, via agent/auxiliary_local_runtime.py.
  The `main_runtime is None` gate is gone: the cached auxiliary path always
  passes a normalized dict, so the rung never ran there, and a task pinned to
  llamacpp inherited a foreign main runtime's URL instead.
- No running server now means (None, None) — the same answer the main ladder's
  _resolve_llamacpp_runtime gives — instead of continuing into
  _try_custom_endpoint / _resolve_api_key_provider, which posted the local GGUF
  slug (and, for compression, the conversation) to whichever cloud provider held
  a key (#119227) or to the primary's own endpoint. OPENAI_BASE_URL is not
  consulted either: the alias table entry (b7e0d715be) exists for explicit
  base_url lanes, and the main ladder never reads it for a bare alias.
- The cached client keys on the live endpoint (base_url + key), read at call
  time, so a server restarted on a new port/key gets a fresh client and a
  stopped one resolves to nothing instead of a stale client.
- The endpoint is resolved with the calling profile's config (load_config() at
  call time), matching the main ladder; a configured provider entry named like
  the alias still wins.
- Vision keeps the raw provider name for its final resolve so the alias is not
  flattened to bare `custom` first.
- The aux "unavailable" message names the stopped local server instead of an
  invented LLAMACPP_API_KEY.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
2026-10-02 16:44:56 -07:00
teknium1
83097ba008 fix(desktop): keep the hermes://skill/install deep link through the catalog revert
The plugin deep link (hermes://plugin/install?catalog=) and the skill
deep link are separate features: one per kind. Reverting #119381's card
catalog should not leave skills without a link, so this restores the
skill route on its own:

- Desktop: skill-deeplink-install store, the skill route in
  deeplink-routes, its use-desktop-integrations wiring, and the
  confirm-store onConfirm/details/busy/done phases its dialog uses
  (restored from main with their tests). Its nine strings move from the
  deleted catalog.* namespace to skillDeepLink.* (en + the six locales
  that translated them).
- Website: Skills Hub "Install in Hermes" button, plus the
  source-qualified installIdentifier from extract-skills.py so the link,
  the embedded picker and the CLI command all name the exact skill
  (apps/shared/src/catalog-install.ts keeps only the skill helpers).
- Docs: skills.md "Install from the website" section, desktop.md pointer.

The card catalog UI stays reverted.
2026-10-02 10:29:28 -07:00
teknium1
6cb7a64e59 revert: Skills and Plugins card catalog browser, third landing (reverts #119381)
Reverts all seven commits of #119381 (59c54c78..476ff4dd), which
re-landed the native card-based Skills/Plugins catalog browser after it
was reverted twice (#113037 reverting #112669, #115787 reverting
#113128). Capabilities -> Skills and Plugins return to the list/detail
tabs with the embedded catalog picker they had before #119381.

Removed with it:
- Desktop catalog/ module (browser, cards, detail dialog, filters,
  reels/masonry, install switch) and skill deep-link install
  (hermes://skill/install); skills/embedded-hub-picker.tsx comes back.
- apps/shared catalog-browse.ts and catalog-install.ts.
- The `catalog` i18n namespace in every locale (+ regenerated
  locales/_keys.desktop.json); the two i18n pack tests and the keys
  test now exercise connectorsPage.searchPlaceholder instead.
- Website /skills "Open in Hermes Desktop" skill links and the
  extract-skills install identifiers; docs return to the pre-#119381 text.
- Later fixes that only patched the reverted catalog code go with it:
  5832a6eb93 + 7aba2cbbe9 (catalog download deadline),
  9e63cdc4b8 (catalog lookalike rows), c756986db2 (catalog counts),
  the catalog half of c13232e90c / f265273347.

Kept on purpose (later work that depends on code #119381 introduced):
- DialogContent `chrome` / `overlayClassName` / nested-dialog layering
  (the image lightbox uses them).
- vitest.setup.ts / src/test/jsdom.ts stubs (other suites rely on them).
- Kanban toolset wiring on the Plugins Desktop switch (cd30fdadf6,
  7ca2f5665f) and its tests, the `hermes_not_connected` server tone,
  the SearchField flex-1 fix, skill provenance helpers. SkillDetail shows
  a provenance pill only for agent/hub skills, since the pre-#119381
  labels have no `external` entry.
2026-10-02 10:29:28 -07:00
teknium1
0a374d1674 docs: Supermemory installs from the plugin catalog
memory-providers.md gets a "Plugin catalog" note on the Supermemory section,
`hermes plugins install supermemory` in its setup, and a "Migrating from
bundled Supermemory" section that mirrors the Hindsight one. memory.md,
overview.md, integrations/index.md, cli-commands.md and the developer guide
no longer list Supermemory as bundled.
2026-10-02 05:10:06 -07:00
teknium1
7e53b3ef82 chore(memory): remove bundled honcho provider; install from the plugin catalog 2026-10-02 04:39:28 -07:00
teknium1
a5144c71e9 fix(memory): show every memory-provider migration outcome, ask once per update
Agent-start recovery (recover_at_startup) reported installed / refused / failed /
not-in-catalog only to agent.log, and so did a selected provider that reports unavailable,
so a user with security.allow_lazy_installs: false, offline, or who declined the dependency
prompt silently ran without external memory (#119769).

- Startup outcomes go through _emit_startup_warning: printed in the CLI; other drivers get
  a sticky warn AgentNotice (Desktop toasts notices but drops warn-kind status.update), and
  the messaging gateway, which wires notice_callback per turn, receives it on turn 1.
- hermes update labels each line with the profile it is about and gives the exact
  `hermes -p <profile> plugins install <name>` command for non-active homes.
- Profiles sharing one environment get one set of dependency answers per provider
  (shared_dependency_answers): the first install asks the normal Python/Node questions,
  later homes reuse them; a decline or failure names the remaining profiles once instead of
  failing (or re-prompting) one by one; Ctrl-C ends the migration with a message (#125794).
- The success line no longer names memory.<name> (#124038); docs follow.

Co-authored-by: chelsealong <chelsealong@126.com>
2026-10-02 04:09:57 -07:00
salch-cred
9d794107b3 fix(memory): migration note must not imply config.yaml provider keys still apply
Refs #124038.
2026-10-02 04:09:57 -07:00
teknium1
084a84b544 fix(memory): migrate deps-bearing providers without a terminal under allow_lazy_installs
Desktop, the gateway and a scripted `hermes update` have no TTY, so the
dependency consent gate refused every catalog provider that declares Python
deps: both migration hooks failed with 'Reinstall declined: dependency install
skipped (non-interactive)' and the provider stayed missing. The agent-start
hook exists precisely for those surfaces.

The configured memory.provider plus security.allow_lazy_installs (read for the
home being migrated) is the consent that let the bundled provider install its
deps on demand; carry it through dashboard_install_plugin ->
install_catalog_entry(assume_deps_consent=...). A terminal still prompts, and
allow_lazy_installs: false still refuses with the install one-liner.
2026-10-02 03:40:17 -07:00
teknium1
9de7853a05 docs(tool-search): describe local tool_call batch splitting
The page still said multi-local and mixed tool_call batches are rejected.
The agent now splits a batch containing local tools into one call per
entry; document that and the connector-only dispatch.
2026-10-01 23:50:31 -07:00
teknium1
a184f85924 docs: explain Desktop's default vs Classic Hermes and the reserved classic name
On Desktop `default` and /skin default mean Nous (a CLI/TUI or config switch
to `default` moves a Desktop pick back to Nous), the gold look is the
separate `classic` theme reached by /skin classic, gold or hermes, and
`classic` joins the Desktop built-in names a user skin file cannot recolor.
2026-10-01 22:27:00 -07:00