Commit Graph

34784 Commits

Author SHA1 Message Date
teknium1 002ee41cfc fix(cron): keep unowned Bot Chat delivery on its resolved home
Extend deferred dispatch's destination pin to ordinary CLI fallback, so
custom-root and active-profile changes cannot redirect a checked target.
Refuse a missing destination before launch and name the target on failure.
Replace the old env-clearing expectation with two behavioral invariants
and retain the native Electron custom-root reproduction.

Adapted from the root-boundary fix and diagnosis in #104066.
Related #104055, #104066.

Co-authored-by: fangliquanflq <fangliquan@qq.com>
2026-09-14 17:29:32 -07:00
teknium1 3b0fe0cc2b fix(cron): keep deferred Bot Chat delivery bound to admission
Carry the original destination home and delivery ID into deferred drain and
its child, rather than re-resolving a mutable profile/root. Missing destinations
fail closed; supported-owner handoffs remain transferred, not ambiguous failures.
Capture the producer root before the background thread starts, and retain/log
malformed JSON without stopping healthy admissions or the whole cron tick.

Two invariants reproduced failures on the published head. Real Electron root
change and malformed-record cases are red before and green after; nested DM
control remains passing. No automatic retry of claimed or uncertain turns.
2026-09-14 17:29:32 -07:00
teknium1 6af84ead60 test(cron): retain native Bot Chat delivery reproduction 2026-09-14 17:29:32 -07:00
teknium1 5d8390d1a4 fix(cron): retain Bot Chat output while a CLI owner is open
Keep never-started output behind unsupported owners and drain in admission
order after release. Persist claims before execution and never replay uncertain
started turns. Existing supported-owner receipts keep their authority.

Credits 686f6c61's residual queue proposal in #100319. This is a scoped
implementation, not general retry of failed CLI subprocesses.

Native Electron before/after: CLI-owned target previously returned
SESSION_NOT_OWNED and remained empty after release/tick; now its queued
output and reply appear once in the target Bot Chat. Nested quiet CLI
message_agent delivery to a named Desktop owner also passes on base.
2026-09-14 17:29:32 -07:00
Adolanium e7bd70d92a feat(catalog): add shodan community plugin 2026-09-14 17:29:03 -07:00
Adolanium b5b8df57b7 fix(catalog): pin hermes-terminal without self-updater 2026-09-14 17:28:30 -07:00
Adolanium 0491ae8101 feat(catalog): add hermes-terminal community plugin 2026-09-14 17:28:30 -07:00
Adolanium 4b8e183912 feat(catalog): add hermes-rss community plugin 2026-09-14 17:27:59 -07:00
Adolanium 010ebe4736 fix(catalog): pin read-only Resetwatch package without updater 2026-09-14 17:27:30 -07:00
Adolanium 55fe7f6340 catalog: pin Resetwatch DeepSeek weekday pricing fix 2026-09-14 17:27:30 -07:00
Adolanium 2c70c41adc feat(catalog): add hermes-resetwatch community plugin 2026-09-14 17:27:30 -07:00
Adolanium 23effb598d feat(catalog): add hermes-office community plugin 2026-09-14 17:27:01 -07:00
Adolanium 69e857425c fix(catalog): pin hermes-nous-prices without self-updater 2026-09-14 17:26:31 -07:00
Adolanium b92dfc2d95 catalog: pin Nous pricing Gateway companion release 2026-09-14 17:26:31 -07:00
Adolanium eab8871978 feat(catalog): add hermes-nous-prices community plugin 2026-09-14 17:26:31 -07:00
Adolanium 5acad84150 fix(catalog): pin hermes-ledgerline without self-updater 2026-09-14 17:26:02 -07:00
Adolanium 681d6c63fb feat(catalog): add hermes-ledgerline community plugin 2026-09-14 17:26:02 -07:00
teknium1 b8bf484351 test(bot-mode): keep the reproducible mock patch whitespace-clean 2026-09-14 17:04:56 -07:00
teknium1 de5ff9ba85 test(bot-mode): prove native profile fallback and nested delivery ownership
Retain executable Electron evidence for the named-unowned and live-owner
paths; stale PATH fails on base and passes with the contributor fix.
Clarify that --in selects cwd rather than the profile database.
2026-09-14 17:04:56 -07:00
liuhao1024 37243bd668 fix(tools): resolve hermes CLI beside the interpreter in bot_mode_dm deliveries
Bot-to-bot message_agent delivery builds both transport argvs (local
teammate chat and peer dm) with a bare "hermes" as argv[0]. Since #96631
the delivery runner spawns under terminal_tool's isolated host-local
environment, which does not inherit the gateway's PATH — so on
docker/service installs (venv at /opt/hermes/.venv) every delivery exits
with FileNotFoundError: 'hermes'.

Resolve the CLI with bot_relay._hermes_cli() (#93590) — the venv sibling
of this interpreter, then shutil.which, then the bare name — at both
argv construction sites. The turn-lock matcher in _delivery_lock()
already matches argv[0] by basename, so absolute paths lock exactly as
before.

Fixes #100662
2026-09-14 17:04:56 -07:00
teknium1 1f1f02e354 fix(desktop): preserve qualified bot identity in primary handoffs
Keep the salvaged botHandle normalization, but remove the unconditional
hermes alias on remote default profiles: the parser's last-wins map
otherwise retargets a local @hermes handoff by roster order.

Consolidate regression coverage into two invariants for persisted primary
handles, both handoff directions and three-source qualified identity.
Capture real Electron screenshots, durable logs and source receipts;
exercise the reverse live handoff too. Document the repair and bump the
bundled Desktop patch version.
2026-09-14 17:04:27 -07:00
teknium1 52972d15f3 test(desktop): live e2e for a teammate's @hermes handoff driving the primary bot
Real-Electron Playwright spec for #100406: a two-member room (primary
profile + code-farmer). The user addresses only @code-farmer; its
scripted reply @mentions hermes; the assertion is a `default`-authored
"B" entry in the persisted room log. On origin/main the room settles
after Code Farmer's line and the spec fails at that assertion; with the
mention-alias fix it passes.

The mock inference server gains a per-speaker script for group rooms:
`E2E_SAY(<handle>)[<line>]` tokens in the user's send answer the member
whose turn prompt opens with `You are @<handle>`; unscripted members
reply "(pass)". `{at}` stands for `@` so the script itself never
mentions anyone and round one only drives the member the user tagged.
2026-09-14 17:04:27 -07:00
Cursor Agent d92ce52e71 fix(desktop): keep @hermes callable when the primary handle is default
Group mention parse used member.handle before botHandle, so a persisted
or union-stamped handle of "default" never mapped to the user-facing
@hermes alias. Bot-to-bot handoff toward the primary profile then
settled with no continuation, while the reverse direction still worked.

Co-authored-by: Noa <rainbowgore@users.noreply.github.com>
2026-09-14 17:04:27 -07:00
teknium1 1a990f3062 fix(skills): keep scanning link-shaped arguments inside fenced code blocks
Masking every balanced [x](dest) in a .md file also blanked
`cp [k](../../../.ssh/id_rsa) /tmp` inside a ```sh fence, which main scored
caution and the branch let through as safe. A link inside a code fence is a
command argument, not a hyperlink: toggle masking off between fence markers.
2026-09-14 16:28:49 -07:00
KoNit-K 5f6e3a6fe3 test(skills): trim link-traversal coverage to two invariants
Split the salvaged regression into the two facts the fix must hold:
a 3-level doc link no longer produces a traversal finding or blocks a
community install, and traversal outside a link destination (a shell
script, or prose on the same line as a link) still fires. The shell
script case is taken from #110978 by @KoNit-K; the same-line case is
what distinguishes masking link destinations from gating by extension.
2026-09-14 16:28:49 -07:00
Puvaan Raaj b259f2401d fix(skills): ignore traversal in markdown links
(cherry picked from commit 92b48941b707df4fd3c13029b157a3f28188b4c4)
2026-09-14 16:28:49 -07:00
teknium1 de16ce9d0c fix(threat-scanner): gate ssh_access on every mutating verb, not just copy verbs
Review of the write-verb gate found sed -i, chmod, truncate, curl -o, wget -O,
git clone and a scripted open(...) against ~/.ssh all slipping to no finding,
where the bare path regex on main caught them. Add those verbs and the open(
shape to the gate; read-only mentions stay clear.
2026-09-14 16:16:08 -07:00
teknium1 101a1db3e6 chore: map contributor email for @garadice (#89249) 2026-09-14 16:16:08 -07:00
teknium1 b1733fd085 fix: keep the ssh_access id, word-bound the verb gate, collapse tests to two invariants
Salvage trim of #89249:
- keep pattern id `ssh_access` (test_memory_tool and callers key on it; renaming buys nothing)
- word-bound the verb alternation: the unanchored form matched `add` inside "address" and
  `dd` inside "middle", so read-only prose still fired; `\b` closes that leak
- fold the second "bare leading redirect" regex into the same alternation (`>>?` branch)
- tests: one parametrized "write shapes still fire" (echo, cat, cp, tee, mv, install,
  printf, dd, scp, rsync, ln, leading redirect, option clusters) and one "read-only
  mention does not fire"; the trade-off/change-detector tests are dropped
- test_memory_tool: the persistence fixture used a read-only mention; use a write shape
2026-09-14 16:16:08 -07:00
Martin Mogis fe68349cd6 fix(threat-scanner): close reviewer-noted ssh_access_write bypass shapes
Extend the write-verb alternation with mv/install/printf/dd/scp/rsync/ln,
allow short option clusters between verb and path, and add a bare-leading-
redirect branch (a > ~/.ssh/... line carries no verb word at all). Prose
that names a write primitive before an SSH path still fails closed — a
false positive costs a review, a false negative is a backdoor.

(cherry picked from commit 4865b96ca3c8661c0ea6f5c479c198ccf68f86c4)
2026-09-14 16:16:08 -07:00
Martin Mogis 34304b722d fix(threat-scanner): require a write verb before SSH paths in the ssh_access strict pattern
The bare \$HOME/.ssh|~/.ssh regex fired on ANY mention of SSH paths in
scanned content, so operational documentation (VPS recovery notes, SSH
configuration write-ups stored in memory or skills) was blocked as a
persistence threat. Require an echo/cat/cp/tee/append/add/write/>>
verb before the path so only backdoor-insertion shapes match.

(cherry picked from commit 8c9b4c8972ac849e4c82fa20d355733ca3e5a231)
2026-09-14 16:16:08 -07:00
teknium1 8c82e93132 fix(gateway): migrate --multiplex rolls back or resumes when the default cannot come up; every installed unit counts
`hermes gateway migrate --multiplex` ran its one fallible step LAST (install +
start the default gateway) with nothing around it. On a fleet whose secondary
ran a system unit as root (#110850) that step raised, leaving the flag on, the
secondary's unit removed and no gateway anywhere, and the re-run hit the
"already multiplexing (flag on)" short-circuit over an empty fleet.

- apply_migration(): the default bring-up runs inside a rollback. On failure the
  manifest written before the first destructive step restores the flag and
  reinstalls every recorded per-profile gateway with its recorded User=.
- MigrationPlan.interrupted: flag on + manifest present + no live default
  gateway is a half-applied migration, not "already multiplexed"; the re-run
  resumes from the manifest (target manager and User= read from it, since the
  units themselves are gone) instead of refusing. Flag off + leftover manifest
  refuses to overwrite it and points at --standalone.
- ProfileGateway.services records EVERY installed unit (user and system) and the
  manifest carries them; apply stops/uninstalls all of them and rollback
  reinstalls all of them, so a second owner is never left live beside the
  multiplexer. The unattended hook treats a two-unit profile as an ambiguous
  topology and refuses (review finding on #110205).
- gateway_identity(): an unresolvable User= on a system unit stays None instead
  of borrowing the profile directory's owner; the unattended hook treats the
  unknown principal as a boundary (review finding on #110205).
- auto_migration_opted_out(): reads the effective config (load_config_readonly
  under the default home), so a managed `false` wins over a user `true` and a
  YAML string "false" is an opt-out, not a truthy value (review finding on
  #110205).

Builds on KoNit-K's #110854 (run_as_user threaded through install, preserved
from the removed system unit).
2026-09-14 16:16:06 -07:00
KoNit-K 18a45464f3 fix(gateway): preserve system service user during migration 2026-09-14 16:16:06 -07:00
KoNit-K 3bb44dc7b6 test(config): reject leftover auto_migrate key
Pin that hermes config set no longer treats gateway.auto_migrate as a known key after DEFAULT_CONFIG uses auto_multiplex_migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 16:16:06 -07:00
KoNit-K c8a6b3ca94 fix(gateway): keep auto_multiplex_migration spelling
Rename DEFAULT_CONFIG to the documented key so the reader, docs, and defaults agree. Do not reintroduce auto_migrate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 16:16:06 -07:00
teknium1 23ef64081c fix(gateway): bind the session profile once around command.dispatch and bundle routing (#110695)
Follow-up on the salvaged #110698 (which scoped `_dispatch_skill` alone):

- One `_session_home_scope(session)` binding around the whole stage loop in
  `command.dispatch`, so quick commands (`_load_cfg` → `_active_config_path`
  honours the override), bundles (`skill-bundles/` is home-relative) and skills
  all resolve against the SAME profile the routing guard used.
- `slash.exec` resolves `_bundle_key_for(base)` under the same scope, so a
  bundle that exists only in the secondary profile is routed to dispatch at all.
- `_dispatch_skill` uses the home-keyed `get_skill_commands()` (the guard's
  reader) instead of an unconditional `scan_skill_commands()`.
- Test (red on origin/main): a bundle only under profile B's `skill-bundles/`
  is dispatched for a session bound to B.
2026-09-14 16:14:33 -07:00
teknium1 a21747fe4e fix(kanban): an anchorless thread subscription warns once instead of vanishing (#110919)
Follow-up on the salvaged #110928 (CLI `--parent-chat-id` / `--guild-id`):

- `_claim_for_sub` skipped a thread-shaped row that matched no `profile_routes`
  entry at DEBUG on every tick. Legacy rows written before the flags existed can
  never match a channel-level route (no `parent_chat_id`), so the notifier now
  logs ONE WARNING per row naming the task, the thread and the re-subscribe
  command. Still fail-closed: the events stay unclaimed.
- Docs: the kanban user guide explains the anchors and shows the Discord-thread
  subscribe command under `profile_routes`.
- Test (red on origin/main): two collects → exactly one WARNING, events unseen.
2026-09-14 16:14:33 -07:00
teknium1 45b42202fa fix(cron): a raising profile gate ticks nothing; housekeeping respawns a dead ticker (#111010)
Follow-up on the salvaged #111034:

- `_start_multiplex` published the enumerated home list before the gate had
  filtered it, so a raising `profile_gate` (the Desktop stand-down probe from
  #100489) kept the thread alive but ticked every profile UNGATED — racing the
  gateway that owns them for the same cron store. The list is now assigned only
  after gating; a gate failure yields zero ticks for that cycle.
- `cron/scheduler_thread.py::SupervisedTickerThread` wraps the gateway ticker
  thread; `_start_gateway_housekeeping` gets a per-tick "Cron ticker supervisor"
  chore that respawns a ticker that ended without a stop request and logs the
  outage at ERROR. Every guard inside `start()` keeps the loop alive, but nothing
  outside it could notice a thread that had already ended.
- Tests trimmed to the invariants, proven red on origin/main: a REAL corrupt
  `executions.db` (no patched recover) no longer kills the ticker; a raising gate
  keeps the thread alive with zero ticks; housekeeping restarts a dead ticker and
  leaves a stopped one alone.

Root cause: the unguarded pre-loop `recover_interrupted_executions()` +
`record_ticker_heartbeat()` were added by d9dd05b69d (#61791, "truthful
execution ledger", 2026-07-09). The reporter's build (e440bf35) also carried
#107485's `completed_occurrence()` in the due scan, which opens the same ledger
on every tick — the first traceback in their errors.log is that in-loop hit
(caught); the restart then hit the SAME corrupt ledger from the pre-loop
recovery scan, which nothing caught: thread dead, no heartbeat, no error marker.
2026-09-14 16:14:33 -07:00
KoNit-K 4c306e9e8c fix(gateway): scope skill dispatch to session profile 2026-09-14 16:14:33 -07:00
KoNit-K 38bda39562 fix(kanban): preserve Discord thread route anchors 2026-09-14 16:14:33 -07:00
Konstantin Khlopkov 8fce4baf8c fix(cron): keep the ticker thread alive through startup recovery and marker writes (#111010)
The gateway runs InProcessCronScheduler on an unsupervised daemon thread, so any exception escaping outside the guarded tick body ends cron silently while the gateway keeps serving. Guard the three remaining escape windows: startup recovery + initial heartbeat in start(), per-cycle profile enumeration/gating in _start_multiplex, and every status-marker store write (heartbeat/error/clear) in both loops. A failing store now degrades to a logged warning and a failed cycle instead of thread death.
2026-09-14 16:14:33 -07:00
teknium1 082e802ea4 chore: map contributor email for @0genlab (#94615) 2026-09-14 16:14:23 -07:00
teknium1 4ea7c882fc test(skills-guard): cover the wider attribute-access and stdin-password cases
Extend the two salvaged invariant tests instead of adding more: the
`.profile` negative now covers `?.`, `().` and `].` access (the
lookbehind was widened from `[A-Za-z0-9_]` to `[\w)\]?]` during salvage),
the positive covers a bare `.profile` token, and the sudo positive covers
the `-S` (read password from stdin) form, which the gateway's own masked
password prompt relies on.
2026-09-14 16:14:23 -07:00
unsupportedpastels 4f00c456f0 fix(skills-guard): stop flagging sudo.request/sudo.respond event names as sudo usage
`sudo.request` and `sudo.respond` are gateway wire events: the masked
sudo-password prompt the terminal tool raises, which every client surface
(desktop, TUI, any plugin that relays secure prompts to another device)
has to name to forward it. The `sudo_usage` rule matched the bare word,
so any plugin listing those events scored `high` and every install of it
landed on `caution`, which Hermes Desktop cannot confirm past.

A dotted event name is never a shell `sudo` invocation. Exclude exactly
those two names with a negative lookahead; a real `sudo cmd` still fires.

(cherry picked from commit a7a3a31126de8057c2dbb9b7048724aa9d55a7fd)
2026-09-14 16:14:23 -07:00
chenxue 63395922bf fix(skills-guard): stop shell_rc_mod matching attribute access
The shell-startup-file pattern is
`\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\b`.
Six of those names are distinctive enough that seeing them after a dot
means the file. `profile` is not: it is also how every language spells
attribute access, so `self.profile`, `user.profile`, and
`request.profile` each score a medium persistence finding.

The cost is signal, not blocking -- `_determine_verdict` treats
medium/low alone as informational -- but a plugin that happens to name a
field `profile` buries the findings a reviewer has to read. A model-
provider plugin whose tests exercise a `profile` object contributed 36
of 38 findings in its scan report, all of them this pattern.

Split `profile` into its own entry anchored on a non-identifier
character before the dot. Real references keep matching in the forms they
actually take (`~/.profile`, `"$HOME/.profile"`, `./.profile`,
bare `.profile`); attribute reads no longer do. The other six names are
untouched. Both entries keep the `shell_rc_mod` id, and scan_file
deduplicates on (pattern_id, line), so a line holding both still yields
one finding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 705d0bddc61a1c55be82956d343b229544350eae)
2026-09-14 16:14:23 -07:00
teknium1 dce7768cb1 chore: map contributor emails for #110307, #110632, #110634 salvage 2026-09-14 16:13:51 -07:00
teknium1 818d781c0b test(tools): trim the salvaged #110632 / #110307 tests to two invariants each
The file-safety salvage carried five tests and the execute_code salvage four;
fold them into the two behaviour contracts per fix: (a) the named-profile scope
exempts the root's direct files and the write lands with no prompt; a child env
sees the ACTIVE profile's home per turn; (b) negatives hold: a checkout's
.hermes/config.yaml stays gated fail-closed, a lookalike profiles/ tree exempts
nothing; a dedicated process with no override is untouched. Both red on base.
Also compress the _hermes_exempt_homes docstring (WHY only; cite #110630).
2026-09-14 16:13:51 -07:00
teknium1 90a65d6e38 fix(gateway): gateway-stop teardown fires memory-provider lifecycle hooks under the owning profile
Under `gateway.multiplex_profiles: true` every gateway-hosted session that ended
inside the gateway process logged `Memory provider 'openviking' on_session_end
failed: get_secret('OPENVIKING_API_KEY') called with no profile secret scope
active` and skipped its end-of-session commit. The provider lifecycle hooks
(`flush_pending` -> `on_session_end` -> provider teardown -> `close`) read
credentials and home at call time; the stop-time finalize pass and the
idle-cache sweep run on the main loop outside any adapter handler, so
`_run_in_executor_with_context` copied an EMPTY scope into the worker.

Route `_cleanup_agent_resources_off_loop` and `_finalize_session_off_loop`
through `_run_release_in_profile_scope` (the seam cache eviction already uses):
a scoped caller keeps its scope; an unscoped caller passes the session key and
the owner's profile scope is entered from it. The stop path passes the key for
both active and idle-cached agents. Cron's post-run cleanup thread is the
sibling seam and is fixed by the cherry-picked #110634.

Live repro (fake OpenViking recording the X-API-Key header, alpha profile,
multiplex on): base - WARNING + traceback, no commit; head - no warning,
`POST /api/v1/sessions/<sid>/commit` arrives with alpha's key.

Fixes #110622.
2026-09-14 16:13:51 -07:00
teknium1 09a7c297ec fix(tools): uncached check_fn probes classify UnscopedSecretError from the live scope
Under `gateway.multiplex_profiles: true` every gateway start logged a WARNING +
traceback from `tools.registry` for `_check_vault_available`. The browser vault
gate is a `no_cache_check_fn`, so `_check_fn_cached` short-circuited into
`_run_check_fn_uncached(fn)` without consulting the cache scope and the default
`unresolved_scope=False` hint reported an EXPECTED boot-time fail-closed read as
"scope was resolved" — the #100697 fix only reached the cached branch.

Derive the verdict from `current_secret_scope()` at the catch site instead of a
branch-derived hint: no scope installed → DEBUG without traceback (expected,
re-probes on the first scoped turn); scope installed but the read still failed
closed → WARNING + traceback (the probe dropped the scope on a bare thread/hop,
a spawn-site bug that must stay loud). Every uncached probe that reads a
credential is covered, vault included.

Fixes #110635. Credit @KoNit-K (#110638) for the report-side diagnosis; that PR
disabled the legacy-cloud probe for the vault gate only, which would have hidden
the vault from legacy Browser Use cloud users and left the class open for every
other uncached probe.
2026-09-14 16:13:51 -07:00
Jony 45aebc11b0 fix(cron): preserve cleanup profile scope 2026-09-14 16:13:51 -07:00