Commit Graph

4405 Commits

Author SHA1 Message Date
teknium1
7e19f4138b fix(telemetry): record auxiliary model calls with their real outcome
Every failed auxiliary model_route row read error_class=unknown because the
aux logical scope only carried outcome/model/provider and the projection
hard-coded unknown for any non-success. Every exception escaping call_llm,
including Hermes' own aborts (Ctrl+C / /stop mid-compression, interrupted
aux streams, task cancellation), was recorded as failed.

- classify the error that ended an aux call (and the last one a successful
  call recovered from) with agent.error_classifier; HTTP-200 bodies that
  carry a provider error object are classified from that object
- Hermes aborts (non-Exception BaseExceptions, InterruptedError) close the
  call as cancelled/none, matching the contract
- MoA aggregator streams report the same way
- aux calls that finish under the turn's own live scopes (title generation
  runs beside the turn) were never counted: the scope could not be popped
  and the turn-close drain closed it as an orphan without a route. The
  drain now closes it with the stored real result.
2026-10-03 13:38:29 -07:00
teknium1
4b2c2ae999 fix(telemetry): one-shot CLI runs report entrypoint one_shot, not interactive
Fleet telemetry labelled every `hermes -z` / `chat -q` / `-Q` run as
entrypoint=interactive because the cli surface mapped straight to
interactive and no caller declared anything else, so 95% of CLI sessions
read as one-turn humans and scripts could not be told apart.

- New closed entrypoint `one_shot` (contract + v3 schema enum). The task
  scope start declares it for cli-surface runs carrying the
  HERMES_SINGLE_QUERY_SESSION marker the one-shot paths already set (the
  signal session source and cache_ttl: auto read). Children Hermes
  dispatchers spawn (kanban worker source=kanban, A2A forward source=a2a)
  read the existing `background`. Delegated children, other surfaces and
  the REPL are unchanged.
- Engagement keeps treating one_shot like interactive (its prior label).
- `hermes -z` exits via os._exit, skipping the atexit hook that closes the
  metrics session: its pre-exit cleanup now runs the runtime shutdown, so
  task_run.finished and session.count are actually written (before, only
  task_run.started survived and the store kept a hot journal).
- Smoke expectations move to one_shot (it drives `chat --query --quiet`).
- relay-shared-metrics.md documents the entrypoint vocabulary.
2026-10-03 13:25:25 -07:00
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
kshitijk4poor
b564815995 fix(tui_gateway): TUI/Desktop /save json exports the stored session and obeys the export cap
session.save wrote the in-memory history, so turns in-place compaction archived were missing from the
snapshot and sessions.max_export_messages never applied, unlike CLI and messaging /save json since the
#130339 fix. The two existing call sites' cap check + export + undone-row filter move into
hermes_cli.session_export.load_save_snapshot; the CLI, the messaging gateway (one off-loop hop) and the
tui_gateway handler all call it. The TUI handler reads the session's own profile db under that profile's
config, refuses an oversized session with rpc error 4131 (the same message as the CLI), and keeps the
in-memory export as the fallback when no stored row exists. Turn-isolated sessions run the same handler on
the compute host; the host now forwards the handler's error code so the refusal arrives as 4131, not 5011.

Refs #130339
2026-10-03 23:28:02 +05:30
alt-glitch
f1c79255fe feat(plugins): requires.gpu gates a plugin server on the host's GPU vendor
A portable plugin can now declare `requires: {gpu: nvidia}` for an MCP
server. On a host without an NVIDIA GPU, Hermes then refuses the
install, hides the server's tools, and the catalog card says "needs an
NVIDIA GPU". The check is the existing
`hermes_platform.host.facts.gpu_class()` read.

The requirement is independent of `app:`. The RTX Remix Toolkit is the
first consumer: it is a package the user unpacks anywhere, so there is
no install location to check, but it needs an NVIDIA GPU.

An unreadable GPU (`unknown`) passes, so a failed registry or sysfs
read does not lock out a machine that has the GPU. Only `nvidia` is
accepted: gpu_class() reports the highest-priority vendor, which
answers the question exactly for NVIDIA and not for AMD or Intel.

A registered server on such a host reports the new `unsupported_gpu`
state, carried through the gateway contract and the Plugins tab, with
a sentence that names the GPU instead of "Install <app>".
2026-10-03 10:26:12 -07:00
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
c776b6691d docs(email): tell operators with per-message receiver hosts to unset authserv_id
The authserv_id pin now matches the topmost Authentication-Results host
exactly; main also accepted a subdomain of the pin. Operators who pinned a
parent domain for a provider that stamps from changing MX hostnames would
see every message dropped with no guidance (gate 2c Medium finding).
2026-10-03 20:54:04 +05:30
kshitijk4poor
2811f7eaaa docs(email): document topmost-only exact authserv_id match
The adapter now trusts only the topmost Authentication-Results field and
requires a set authserv_id to match it exactly (no subdomain alignment),
so operators whose relay stamps a different top authserv-id see the
untrusted-authserv warning. Say so where the warning is documented.
2026-10-03 20:54:04 +05:30
kshitijk4poor
0b3c03d62e docs(telegram): document max_concurrent_updates and per-chat ordering
Concurrent cross-chat dispatch is now the default; document the knob, its
default, and that same-chat updates stay ordered.
2026-10-03 20:50:15 +05:30
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
kshitijk4poor
c5ff25a50a fix(sessions): cap /save json by sessions.max_export_messages
The E4 2c review (convergent with quality) found /save json now loads the
transfer projection -- every stored row, archived included -- with no
sessions.max_export_messages guard, while `hermes sessions export` and the
console export refuse oversized sessions before materializing that same
projection. A heavily compacted session could load its whole archive into
memory from one chat command. Both /save surfaces now call the existing
assert_export_safe() before a JSON export and reply with the shared
SessionExportTooLargeError text; md/html transcripts are unaffected.
2026-10-03 19:21:54 +05:30
kshitijk4poor
cf00b855c4 docs(sessions): scope the /save json backup claim to CLI and messaging
The E4 2c review found the new docs say a `/save json` snapshot holds the
same rows as the transfer backup, but the TUI/desktop /save
(tui_gateway session.save) still dumps in-memory history. Narrow the
English and zh-Hans sentences to the CLI and messaging /save surfaces;
the TUI path is left as a follow-up.
2026-10-03 19:21:54 +05:30
John Paul Soliva
9cb2fddee4 fix(sessions): /save json snapshots keep compaction-archived turns too
The CLI and gateway /save json wrote the live rows only, the projection the
removed comment justified by import_sessions replaying archived turns as live
context. Import now restores the active/compacted flags, and the dashboard
import accepts a /save json file, so a snapshot of a compacted session lost
every turn the summary replaced. One export_projection() rule now picks the
display history for transcripts and every flagged row for JSON, shared by
`hermes sessions export` and both /save surfaces.

(cherry picked from commit 12f98c891cd70083d35dda9bcabe17459e4c7bb0)
2026-10-03 19:21:54 +05:30
kshitijk4poor
2b52acc2d8 docs(profiles): name the OS credential stores in the distributions credential bullet too 2026-10-03 19:02:12 +05:30
kshitijk4poor
94aedfb84b docs(profiles): list the OS credential stores among export exclusions
Gate finding (2c, Low): the "Always excluded" list in the export-contents
section did not mention the .ssh/.aws/.gnupg/.kube/.envrc exclusion this stack
adds, nor that nested copies under a skill are dropped too. Add them to the
English list and to the zh-Hans credential-store list (that translation has no
export-contents section; its credential list notes exports strip them too).
2026-10-03 19:02:12 +05:30
John Paul Soliva
3acd949217 feat(desktop): keep-awake follows the turn — a "while working" mode alongside off/always
"Keep computer awake" was a boolean: off, or a machine-global power-save
blocker held for as long as Hermes Desktop is open. That is the wrong shape
for its own stated purpose. An unattended overnight run dies when the
machine sleeps unless the user predicted, before starting, that they would
need the toggle — and a user who did flip it leaves a laptop pinned awake
all week, including the idle hours after the run finished (#101991).

Main already knows, live, whether any turn is in flight:
`mergeActiveWork(activeWorkByWebContents.values()).count > 0` is the exact
expression that drives stream throttling. This change wires the blocker to
that same signal behind a three-way mode:

  off            never hold the blocker (unchanged default)
  while-working  hold it only while a turn is in flight; release at idle
  always         hold it whenever the app is open (what `on` used to mean)

Both persisted copies migrate in place. The renderer's localStorage boolean
(`hermes.desktop.keepAwake.v1`) and main's `keep-awake.json` `{ on }` are
read once and mapped `true -> always`, `false -> off`, so an existing user
sees exactly the behaviour they had. The IPC channel keeps its name and
accepts either the mode or the old boolean. An unrecognised saved value is
refused rather than trusted and falls back to `off`.

No new listener, timer, or IPC channel: `updateStreamThrottleFromActiveWork`
computes the merged picture once and hands it to both consumers, and the
cold-launch apply reads the mode instead of the flag. No trailing delay
between back-to-back turns — a sub-second release cannot let a machine
sleep, and `powerSaveBlocker` start/stop are cheap.

The policy is three pure functions in power-save.ts (parse, read, wanted)
so it is testable without the main process; main only wires them.

(cherry picked from commit c6dab931a2891ea7026eea2c07c271bcf48446b9)
2026-10-03 17:11:39 +05:30
John Paul Soliva
618a4d5dbd docs(sessions): a restored export keeps its pinned, hidden and archive flags
(cherry picked from commit c04df9dbe560c1de862d020f69073c535e021995)
2026-10-03 16:41:37 +05:30
EloquentBrush0x
675bad762a fix(discord): resolve allowlisted usernames by username only, not display name or nickname
_resolve_allowed_usernames resolved a DISCORD_ALLOWED_USERS name through member.name,
member.display_name or member.global_name, and the first match took the slot. Any member of a
shared server could set a nickname or display name to an allowlisted username, get the
resolved ID (DMs, tools, approval buttons) and lock the real user out.

Match the unique account username only, as #125034 did for SimpleX display names. An entry
that matches only a display name stays unresolved and gets a connect-time hint. Tests cover
a copied nickname and a copied display name seen before the real user, and the hint; the
Discord docs now say display names and nicknames are never matched.

(cherry picked from commit 252f0bccab39afe94f0f71254e47a8f785ec1fb0)
2026-10-03 16:37:12 +05:30
itsflownium
5b33f3128a fix(sessions): include protected rows in retention backups
(cherry picked from commit 891575265e657acf5d0b5e6179d11013407a7f8c)
2026-10-03 16:36:00 +05:30
kshitijk4poor
2358519860 refactor(sessions): one export-guard helper and refusal text for CLI and console
`hermes sessions export` and the console's `sessions export` each
re-implemented the per-session max_export_messages loop and carried a
near-identical refusal string, so a wording or limit-resolution fix had to
land twice. SessionDB.assert_exports_safe resolves the limit once and
asserts each id; export_too_large_message is the single refusal text.

The CLI now resolves the limit before the bare-export search_sessions
scan, so `max_export_messages: 0` skips that scan entirely instead of
listing every session only to check nothing. The round-trip test now
asserts the refusal names the config key, and the export-guard docs say
the cap counts every stored message, not just active ones.
2026-10-03 16:32:10 +05:30
Froraut
0f548a9cf9 fix(sessions): address review — guard the JSONL backup with max_export_messages; document it
The backup now loads every stored row, archived generations included,
but `_cmd_export` skipped the per-session `assert_export_safe` guard
that the console export applies. A heavily compacted session with a
small live tail could therefore load an unbounded archive into memory.

All three selection paths (`--session-id`, filters, bare export) now
run the `sessions.max_export_messages` guard over the stored row count
before loading anything, and print the refused session id. Transcript
views keep their display projection and are not guarded, as before.

website/docs/user-guide/sessions.md no longer says JSON/JSONL exports
are live-only. It now says they carry every stored row, including
compaction-archived and rewound or edited-away messages, with their
flags. It also says an import restores those rows as archived, and
that the guard applies to the backup.

(cherry picked from commit 36cdc89e342b7bda4e362294c1bd0ba5d44fed6d)
2026-10-03 16:32:10 +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
Froraut
d584836ffa fix(matrix): keep every outbound event under the homeserver's byte cap
The homeserver rejects any event whose canonical JSON is over 65,536 bytes
(M_TOO_LARGE), but Matrix chunked outbound text on characters, 16,000 by
default with a 65,535 ceiling. Each message carries its text twice (body plus
formatted_body HTML), and an edit carries it four times (the "* " fallback
plus m.new_content).

On the default config this caused three failures:

- A formatted chunk of CJK text came to about 95 KB and was rejected, so
  _send_with_retry fell back to a plain 3,500-char truncation.
- A streamed edit near the limit came to about 80 KB even for English, so
  edits failed mid-stream.
- The standalone sender (cron without a running gateway) posted each
  16,000-char chunk as one event, and hit the same failure on non-Latin text.

The fix measures Matrix text in UTF-8 bytes, the unit of the cap:

- The adapter's message_len_fn now returns UTF-8 bytes, so send() and the
  stream consumer's split and progress-edit budgets count the same unit.
- A chunk gets a third of a 45,000-byte content budget, which leaves room for
  the event envelope and E2EE's 4/3 base64 inflation. That makes 15,000 the
  default and the ceiling for max_message_length, so English chunks stay
  about the same size.
- _build_text_message_content drops the HTML when escape-heavy text would
  push the content past the budget.
- An edit that would still be too large keeps its full m.new_content and
  shortens only the top-level fallback, which edit-aware clients never
  render.
- The standalone sender re-chunks on UTF-8 bytes and applies the same HTML
  guard.

(cherry picked from commit cab00c94175955a68ecd4d38c5748d63148ca6be)
2026-10-03 16:31:35 +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
John Paul Soliva
978fe337bf docs(env): an OPENAI_BASE_URL-bound OPENAI_API_KEY goes only to that origin
(cherry picked from commit e2a64b83239b22e97a02ed4d7898dbc68c11ee3d)
2026-10-03 16:30:27 +05:30
John Paul Soliva
e6ec4aa9ed fix(auxiliary): the inherited main key comes from the same source as the anchor endpoint
_read_main_api_key_if_same_host read base_url and api_key through the
per-field readers, which fall back to config.yaml field by field. A live
main runtime that is keyless or uses key_cmd therefore paired its own
base_url with config.yaml's key, and an auxiliary task pointed at the live
endpoint received the key issued for config.yaml's endpoint.

The anchor and the key now come from one source: the runtime bound for the
turn, else config.yaml. Documents the origin rule (EN + zh-Hans).

(cherry picked from commit ad3de0cf6fd35ee783c5d999ed98e913d23ffcab)
2026-10-03 16:30:27 +05:30
kshitijk4poor
c4343445c4 fix(email): warn when a listed sender's mail fails authentication
r2 review: moving explicit failing verdicts to debug hid a listed contact whose
own domain breaks DKIM or whose forwarder breaks alignment. Those drops warn
again, with the verdict and no opt-out hint; forged stranger mail under open
access stays at debug. The two operator hints now live in one reason->hint
table behind a single warning call.
2026-10-03 16:30:02 +05:30
kshitijk4poor
a4dadad7cd fix(email): warn when a mis-pinned authserv_id drops an admitted sender
The previous commit moved every non-missing-header auth failure to debug,
which made a mis-pinned authserv_id (the only stamp comes from another
authserv-id) drop allowlisted mail silently at default log levels. That is
an operator misconfiguration, not routine forgery, so it warns again, with
a hint pointing at platforms.email.authserv_id instead of the opt-out.
2026-10-03 16:30:02 +05:30
kshitijk4poor
4c982e9ca4 fix(email): warn with the opt-out hint only when Authentication-Results is missing
Under open access every stranger is granted, so each forged or DMARC-failing
message logged a WARNING telling the operator to disable sender
authentication. Keep that warning (and hint) for the one verdict that points
at a server that never stamps the header; log every other unauthenticated
drop at debug, including an explicit failing verdict for an allowlisted
address. The reason string is lifted to _NO_AUTH_RESULTS_REASON so the
parser and the check share it.
2026-10-03 16:30:02 +05:30
kshitijk4poor
fc8307f35b docs(email): open access no longer skips the authenticated-From check 2026-10-03 16:30:02 +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
b932c53264 fix(profiles): the Teams pipeline store and mem0.json are credential stores
teams_pipeline_store.json persists each Graph subscription's client_state, the shared secret
MSGraphWebhookAdapter._verify_client_state checks on every notification. mem0.json is read for
api_key (the documented self-hosted layout keeps the server key there). A named export shipped
both and a distribution could install both; they are on the shared inventory now.

The two invariant tests name both stores, the Teams one through the writer's own filename
constant, so they fail without the inventory entries at both boundaries.

(cherry picked from commit 6e44670c62c077521c90cb270c1ace3dacef6651)
2026-10-03 16:29:29 +05:30
John Paul Soliva
64a073ad88 fix(profiles): every store in a profile home, and Hermes' copies of them, stay out of both boundaries
A named export still shipped credential stores the list did not name, most with the secret intact:
home/ (the subprocess HOME, where git, ssh, gh, npm and skill CLIs such as xurl keep credentials;
.git-credentials and private keys have no suffix the scrub edits), weixin/accounts/ (WeChat bot and
context tokens), proxy/ (iron-proxy CA key and proxy tokens), chrome-debug/ (the /browser connect
Chrome profile), .copilot_jwt.json and runtime/photon-sidecar.json. They are on the list now.

The recovery copies (backups/, state-snapshots/, auth.json.*, .env.bak*, config.yaml writer copies)
move into profile_path_is_private, so a distribution can no longer plant a state snapshot or an
auth.json.corrupt either; before, only the export dropped them. config.yaml copies match the writers'
formats, so a hand-named config.yaml.bak-my-note (user-owned per config_backups) is exported again,
and the export scrub now covers config.yaml.* names so its secrets are redacted like config.yaml's.

The three invariant tests become two: the export test runs the real recovery writers and checks
case-folded names; the distribution test covers plant and replace for the legacy and
distribution_owned layouts.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
(cherry picked from commit 2bd63066f48798a849394df131860ed3140d1597)
2026-10-03 16:29:29 +05:30
John Paul Soliva
69613ebc5f fix(profiles): honcho.json is a credential store, and a file cannot replace a store's directory
honcho.json holds the Honcho apiKey and the OAuth grant (oauth.refreshToken,
a key name the export scrub does not mask), so it joins the profile credential
stores: never exported, never installed or overwritten by a distribution.

A distribution payload that ships a regular file where the profile has a
directory holding credential stores (a file named `platforms`, or
`platforms/whatsapp`) was a whole-directory replace that deleted
platforms/pairing and the platform sessions. The update is now refused in the
pre-write guard, before any file is replaced.

(cherry picked from commit 10a3b017c56862b73ed4affffc2786ef65e2dcbe)
2026-10-03 16:29:29 +05:30
John Paul Soliva
8c1adb7d6d fix(profiles): a distribution can neither plant nor replace a nested credential store
PROFILE_CREDENTIAL_PATHS was honoured by the export, but a distribution
install only checked top-level names. A distribution that owns platforms/
(the legacy whole-payload layout, or an explicit distribution_owned entry)
planted its own platforms/pairing approvals and platforms/whatsapp/session
into the installer's profile, and replaced the installer's on update.

profile_path_is_private() / profile_path_contains_private_store() match
root-relative paths case-folded. The install skips a private path, refuses
it in distribution_owned, and merges any ancestor of one instead of
replacing it. The export uses the same predicate.

Also lists the legacy single-user Google Chat token file (it carries the
OAuth client_secret, which the text scrub does not redact), the legacy
Google Chat PKCE directory, and the Google Meet node RPC token.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
(cherry picked from commit 395959f4ae4ca2c92a45199709f54ccfd9fd1724)
2026-10-03 16:29:29 +05:30
John Paul Soliva
fc33275f22 fix(profiles): keep Hermes' recovery copies of credential stores out of named exports
A named-profile export dropped the stores themselves but shipped the copies Hermes writes of them: backups/ (pre-update zips of the whole home, config.yaml copies), state-snapshots/ (per-profile update snapshots with pairing stores), auth.json.corrupt, and the config migration's .env.bak-<stamp> / config.yaml.bak-<stamp>. None of their suffixes is one the text scrub edits. The default export's root allow-list already omits all of them.

Also lists feishu_comment_pairing.json beside the other pairing stores, and mirrors the credential-store wording in the zh-Hans distributions page.

(cherry picked from commit 0a24aeaaf55279bfa9af4cc9f5e80873fb611020)
2026-10-03 16:29:29 +05:30
John Paul Soliva
9c18f8c28c fix(profiles): keep every credential store out of profile exports
Named-profile exports dropped only auth.json, .env and bot-desktop, so
other credential stores Hermes reads from a profile home were archived
as-is: .op.env (1Password service-account token), npmrc, OAuth token
files, mcp-tokens/, vault/, browser profiles and platform sessions.
.op.env and npmrc also escape the text scrub pass.

Add PROFILE_CREDENTIAL_PATHS (root-relative) as the one list, exclude it
from named exports, and fold its root entries into
DEFAULT_EXPORT_EXCLUDE_ROOT so distribution installs treat them as
user-owned.

(cherry picked from commit fb8ce7a9999049c1bc11aaf7a3d9b2190d8ed1aa)
2026-10-03 16:29:29 +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
fe4432ecdd docs(proxy): mirror the Host and Origin checks in the zh-Hans subscription proxy page
(cherry picked from commit 4ffa98773fd30d1b7c5a590169f794a517d146c1)
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
teknium1
c8301ea6c9 fix(local-runtime): Vulkan/HIP launches subtract what other programs hold on the card
launch_budget read free memory only from nvidia-smi, so a discrete AMD/Intel card launched at its
capacity window regardless of other programs' VRAM, the case that silently pages on Windows. It
now asks the engine's device probe afresh at launch (the polled view stays cached); the driver's
free count includes other processes (live: 16.2 GB held elsewhere read as 8.2 GB free of 26.0).

Docs: auto is a preference with the platform's pinned fallbacks, not an unconditional CUDA pick.
2026-10-03 02:25:26 -07:00
teknium1
2e6924bd10 fix(local-runtime): size AMD/Intel GPUs by the engine's own device type
The previous commit budgets any Vulkan/HIP device that `llama-server
--list-devices` prints as a discrete card. That output carries no
integrated-vs-discrete verdict, so an iGPU (Intel ADL-N reports 11.8 GB of
a 16 GB machine's shared memory) would be counted twice: once as VRAM and
again as system-RAM spill.

Ask the installed engine's ggml library instead, in a bounded child
process: ggml_backend_dev_type() is GPU for a discrete card and IGPU for
an integrated one (Vulkan reads it from VkPhysicalDeviceType). llama.cpp
places layers on discrete devices whenever one exists and falls back to an
integrated GPU only otherwise, so the budget follows the same rule:

- discrete device -> its own memory, system RAM as spill (uma=False)
- integrated GPU, no answer, CPU/Metal -> RAM-as-unified, unchanged

The probe runs only when no NVIDIA device answered and the configured
engine is Vulkan/HIP, and is cached per engine binary (the hardware
endpoint polls every few seconds). The CUDA --list-devices fallback for the
NVIDIA unified-pool quirk is left as it was on main.

Co-authored-by: Belaquaa <eee_pokkk@mail.ru>
2026-10-03 02:25:26 -07:00
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
Brooklyn Nicholson
80aab939c8 fix(docker): keep per-process temp mounts out of the reuse fingerprint
A symlink anywhere under skills/ makes _safe_skills_path serve the skills
mount from a fresh mkdtemp copy per process, so hashing its host path into
hermes-environment made the label differ across processes: the persistent
container was never reused and every session started fresh, losing the
long-lived-container contract (installed packages, filesystem state).

Canonicalize tempdir-sourced mount specs to a stable placeholder that
keeps the container path; real mounts, image and HERMES_HOME still force
a fresh container on change.

Follow-up to #131895.
2026-10-02 21:43:34 -05:00
ahamSel
b829f2af8c fix(tools): scope Docker reuse to immutable config 2026-10-02 20:27:05 -05:00