`run_git_with_credential_fallback` sent exactly one stored credential after an
anonymous refusal: the first one `resolve_git_basic_auth` found, which is
`GITHUB_TOKEN`/`GH_TOKEN` from .env whenever it is set. An expired classic PAT
left there (older setup flows encouraged it) therefore shadowed a working
`gh auth login` for every private plugin/MCP/profile clone, and the failure
read as git's generic "could not read Username ... terminal prompts disabled".
Why: the remote, not the resolution order, knows which credential is live.
The run now walks every credential the user owns for the host (.env token,
`gh auth token`, git credential helper) until one is accepted or the failure
stops being about credentials, logs which source was rejected, and appends an
actionable hint naming the .env token when GitHub refused all of them.
`gh auth token` is asked with GH_TOKEN/GITHUB_TOKEN stripped from its
environment, otherwise it echoes the exported dead token back instead of its
keyring login and the fallback dedupes to nothing.
Test file also gets encoding="utf-8" on its bare read_text/write_text calls
(windows-footgun scanner population for the touched file).
`_search_with_grep_pruned` built `find <root> -type f -exec grep …`; find's
`-type f` tests the link itself, so a symlinked root (file or directory)
handed grep nothing and search_files answered total_count 0 with no error
and no warning — byte-identical to "no match" on every platform. `find -H`
follows the operand and only the operand, so links met inside the tree keep
their traversal semantics, and the follow happens in the command on the
host that owns the link (SSH/container backends included). Same shape as
the files-lane fix in the previous commit.
The plain `grep -r` lane is unchanged: GNU grep already follows a
command-line symlink (measured); BSD grep skips it, which needs a
macOS-side follow-up.
Based on the analysis in #116271 by @liuhao1024, whose local
`os.path.islink` resolution would not see a link that exists only on a
remote execution host.
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
search_files(..., target="files") answered total_count: 0, error=None and no
warning for a symlinked root: find <link> -type f tests the link itself, while
rg --files follows the operand, so one call answered differently per installed
engine. find -H follows an operand symlink (and only an operand), so a linked
root is walked where the filesystem lives and the link path stays in the output,
the same shape rg reports.
The no-rg breadth guard now classifies the link's target: making a linked root
traversable would otherwise let a link to $HOME or / slip a recursive find past
it.
Refs #116270
Bumps sha to a6cf35afc3ca607271284844ee79c60ee4083296 (tag v0.4.5),
which carries the review fix: catch-all allow patterns refused at YAML
load and in `add --type allow` unless forced interactively.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
A bundled platform whose plugin.yaml `name:` differs from its directory
(dir `a2a`, name `a2a-platform`) could be enabled under the manifest name
— `plugins enable` writes that key and `gate_manifest` matches it — but
`Platform._missing_` only knew directory names, so `platforms.a2a-platform:`
in config.yaml raised inside `GatewayConfig.from_dict` and was silently
dropped: the gateway booted without the platform and its port never bound.
`_scan_bundled_plugin_platforms` now also collects each manifest `name:`
as an alias mapped to its directory, and `_missing_` resolves such an
alias to the directory-name member, so every consumer (registry lookup,
adapter creation, config round-trips) sees the canonical value and the
identity invariant holds for both spellings. Aliases never shadow a
directory name.
Fixes#116180
The three tests asserted literal phrases in a module docstring and two
markdown files; they guard prose, not behaviour, and would break on any
rewording. The docs change itself is the deliverable.
The prior commit only documented the <=2-member DM auto-classification
in the adapter.py module docstring — source an operator configuring
MATRIX_REQUIRE_MENTION etc. would never read. Issue #114733 explicitly
asks for the callout wherever MATRIX_ALLOWED_ROOMS,
MATRIX_FREE_RESPONSE_ROOMS, MATRIX_REQUIRE_MENTION, and MATRIX_AUTO_THREAD
are documented for operators, i.e. the env-var reference table and the
Matrix user guide. Add the same rule + bypassed vars + escape hatch to
both, matching the precedent set by 08aa67e473 for this kind of env-var
clarification, and extend the doc-content test to cover them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_resolve_room_identity() classifies any room with <=2 joined members as a DM
regardless of m.direct or an explicit room name, so those rooms silently
bypass MATRIX_ALLOWED_ROOMS, MATRIX_FREE_RESPONSE_ROOMS, and
MATRIX_REQUIRE_MENTION, and use DM threading instead of
MATRIX_AUTO_THREAD/MATRIX_SESSION_SCOPE. This was previously only visible in
an inline code comment, not in the env-var docs an operator would read.
Fixes#114733
macOS hands back DECOMPOSED path strings (NFD) for names stored in
composed form (NFC), so raw Path equality in kanban_db_workspace
reported a real repo root as 'not inside a git repo' and worktree
dispatch died with ValueError for every task on such a board.
Add _path_key() (NFC-normalized identity key) and route the four
path-identity comparisons on the dispatch path through it:
_resolve_worktree_workspace, _ensure_git_worktree,
_cleanup_worktree_workspace, and the fallback branch.
Patch authored by the issue reporter; independently verified against
main by @KeyArgo (RED/GREEN + collateral). The reporter's test file
runs green locally on macOS including both macos_only end-to-end
rows (3 passed).
Fixes#115080
backfill_acp_session_cwd had no production caller, so rows minted before
the column was written stayed unassigned in Desktop until someone ran it by
hand. The manager now runs the idempotent UPDATE once per process on first
DB use (injected or acquired), with a test through create_session().
Also maps the contributor email for attribution.
ACP sessions stored their workspace only inside the model_config JSON blob
(a correct choice when the cwd column did not exist yet), so Desktop, the
Projects sidebar and hermes sessions list showed every editor session as
unassigned. create_session now passes cwd, an existing row (the live path,
since the agent flushes the transcript incrementally) gets the column
promoted via update_session_cwd, update_cwd() moves it on reopen, and git
branch/root are probed off the interactive path under the same generation
contract tui_gateway/session_workdir.py uses. backfill_acp_session_cwd
promotes model_config.cwd for rows minted before this change.
Squashed from the five commits of #115707; the accidentally committed
Windows cache files under %SystemDrive% are dropped.
Two distinct defects in the Windows gateway pause/resume path, one
symptom: "hermes update" aborting exit 1 on a checkout that was
already current and needed no code change.
1. Asymmetric error handling between the two finish paths. The pull
path wraps the Windows gateway resume in
_resume_windows_gateways_and_merge_outcome, whose contract is
explicit: "Must never abort the update" -- it catches the failure,
marks the outcome incomplete, warns, and continues (receipt status
"partial", exit 1 only via the existing incomplete-repair gate).
_finish_already_up_to_date ("Already up to date" path) called
_resume_windows_gateways_after_update bare, so the identical
RuntimeError -- e.g. the relaunch-verification race with a parent
Job Object kill, #48820 -- killed the run as an unhandled exception
instead. Fixed by routing through the same merge helper and folding
a failed resume into current_checkout_complete, landing on the
pre-existing "partial" finalize + sys.exit(1) contract the repair
path already uses for other incomplete states.
2. The atexit callback could replay the same failure a second time.
Every foreground call site registers
_resume_windows_gateways_after_update via atexit.register as a
dead-process safety net, but the function only cleared
token["resume_needed"] on its OWN success path -- a raise from
_verify_relaunched_gateways_alive left the registration live, so
interpreter teardown called the function again with the same
still-armed token, repeated the whole relaunch + verify, failed
identically, and printed "Exception ignored in atexit callback".
Fixed by unregistering the function from atexit as soon as
execution reaches it (foreground or the atexit fallback itself) --
ownership transfers to whichever call site got there first, and a
failure below is reported exactly once. atexit.unregister is a
no-op when the function was never registered, so a non-Windows or
no-resume-needed call is unaffected.
Both fixes are surgical: they leave the "a failed relaunch keeps
resume_needed=True so the token is retryable" contract completely
untouched (see the existing
test_resume_windows_gateway_service_failure_stays_retryable /
test_resume_windows_gateway_launcher_refresh_failure_stays_retryable
tests, which this PR does not modify) -- that design is intentional,
not the bug.
Closes#115563.
Testing: Windows-specific behavior verified by code trace + full
resolution-chain tests against the real functions (mocked I/O, real
control flow) on Linux/macOS, per the "Don't fake the host OS" rule
this repo's own AGENTS.md sets -- no sys.platform patching, and the
Windows-only branch (_is_windows() == True) is exercised exactly as
the existing test suite for this module already does (monkeypatching
_is_windows on the shared test fixtures, not faking the platform).
True live-Windows process-topology proof is out of scope for this PR
(the wine2e lane is reserved for process-topology E2E per AGENTS.md);
this fix is control-flow/bookkeeping, not host-specific syscalls.
- 2 new regression tests, both proven red on the pre-fix code via
git stash (both fail with the exact defect signature: the "Already
up to date" path re-raises instead of demoting to partial; the
atexit unregister call never happens) and green on the fix
- scripts/run_tests.sh across the full affected surface (Windows
update/resume/reconciliation + related pause/resume/venv-repair
test files): 228 passed, 0 failed, 4 skipped (pre-existing,
unrelated to this change)
Threaded Mode is a bot-owner setting enabled via BotFather, not a
per-chat toggle. Same bug class as 18f2828e87, flagged as a residual
spot on PR #115025.
The dm_topics "not a forum" warning and the matching docs section told
users to tap the bot's name in the DM and toggle "Topics" in chat
settings. That toggle only exists for group forums; a bot DM has no
such control. The actual prerequisite is Threaded Mode, enabled by the
bot owner via the BotFather Mini App (Bot Settings -> Threads
Settings) -- already documented correctly a few sections later in the
same file, under the /topic prerequisites.
Fixes#115019
A launcher that spawns `hermes dashboard` (Desktop shell, link-style
integrations) mints HERMES_DASHBOARD_SESSION_TOKEN into the child's
environment and keeps the same token for its own /api probes. Every
dotenv layer in load_hermes_dotenv() loads with override=True, so a
persisted HERMES_DASHBOARD_SESSION_TOKEN line in ~/.hermes/.env replaced
the injected token: the child authenticated with the persisted value and
the parent got HTTP 401 from its own child.
Treat the token as a spawn credential in the dotenv publisher: a value
that dotenv did not put into os.environ (tracked by _DOTENV_PUBLISHED)
is left alone, while a value an earlier pass published still reloads,
so .env edits and home switches behave as before. Other keys, including
the documented HERMES_DASHBOARD_PUBLIC_URL, keep .env-wins precedence.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: fangliquan <fangliquan@qq.com>
Drops gui/ from the pinned tree per review (a catalog entry may not ship an app patch); the patch stays
on the plugin repo's gui-rows branch. Both gates re-checked at the new sha.
Adds the per-model voice catalogs (361 voices), the refreshed model lists (21 STT / 18 TTS) and
the CLI listing. Both gates re-checked locally at the new sha.
The entry now carries the GUI-row patch and the audio tests, so the pin moves with them
(README rule 4 — a new PR, re-reviewed as a SHA bump). Both CI gates re-checked locally:
the structural validator passes and the plugin at the new sha passes
`hermes plugins validate`.
Adds the OpenRouter voice plugin: speech-to-text and text-to-speech providers for
stt.provider / tts.provider = openrouter, reusing OPENROUTER_API_KEY. Stdlib only.
Owner submission (README rule 5): the plugin repository's owner is opening this PR.
Pinned to a1b01810cdd6be15be61f2c6f6e05e24dafbba14 (rule 2) — exact 40-hex, no
branch or tag.
Policy compliance:
- rule 3 (no self-updating code): pure Python; the plugin never fetches GitHub and
never rewrites its own distribution. Updates reach users only through
`hermes plugins update` and a SHA-bump PR here.
- rule 6 (declared capabilities match reality): registers no tools, hooks or
middleware; requires OPENROUTER_API_KEY, which is the only env var it reads.
Both CI gates checked locally before opening: scripts/validate_plugin_catalog.py
passes on the whole directory, and an anonymous clone at the pinned sha followed by
`hermes plugins validate` passes.
A WebSocket that drops right after sending session.resume or prompt.submit
has its disconnect cleanup (_close_sessions_for_transport) run before the
RPC's rebind. The rebind then registered the already-closed socket and
cancelled the pending orphan reap; nothing ever detaches that socket
again, so the detached session kept its active-session lease (surface=ios
Bot Chat) with no Timer until the 300 s idle-reaper repair sweep, and the
canonical Bot Chat stayed "connecting" for the next client (#116464).
_rebind_live_transport now treats a dead rebinding transport as "client
not back": it leaves the reap armed (re-arming it when the caller already
cancelled, as the reuse fast path does) and does not register the dead
socket as a viewer. prompt.submit goes through the same seam instead of
its own attach + unconditional cancel.
Live: loopback dashboard rig, grace 3 s, ios session, owning socket aborted
mid-submit + 12 resume-then-drop sockets. Before: lease still held 15 s
later, no reap logged. After: lease released within the grace, reap logged,
fresh resume returns the stored history. Control (plain 1001 close + reply-
awaiting resume/close burst) still releases.
Bot Mode room members each keep a hidden `Group: <roomId> · <thread>`
plumbing session that grows with every drive. When it bloats the member
degrades into empty replies, yet nothing could compress it: the session is
hidden from every list, the focused-chat /compress never targets it, and
`-q` chat forwards slash text to the model. Group settings now lists each
member with a "Compress history" action that resumes every session the
room holds for that member (thread-scoped pointers, the legacy pre-thread
pointer, or the pre-thread title as last resort) and runs the gateway's
session.compress against the live runtime id — the exact RPC pair the
reporter had to issue by hand over JSON-RPC. A 4007 (no such session)
skips; anything else surfaces in the toast. requestForBot/host.request
gain an optional per-request timeout so the 660 s compress budget the
focused-chat /compress already uses applies here too instead of the
socket's 30 s default reporting a false timeout.
Slim redo of #102333 by @fangliquanflq (same action, without the
auto-hygiene sweep and the group-turns facade appendage).
Co-authored-by: fangliquanflq <fangliquanflq@users.noreply.github.com>
Review follow-up on the Bot voice routing (#100864, salvage #101545 @FalconOrtiz).
- Owner identity is (connection, profile) per the Bot Mode standing ruling:
ComposerScope publishes `connectionId` beside `profile`; `voice-client-direct`
keys its config cache and `hermesApi` scope on both (new `ownerScoped` in
api/client.ts, beside `profileScoped`), and `voice-playback` mints the
speak-stream against the OWNER connection (`getConnectionFor`) and pins the
POST fallback the same way. Two `default` Bots on two gateways no longer
collide in the cache or mint against the active gateway.
- Main pane: `ChatRuntimeBoundary` publishes the session owner hint's
(connection, profile) on the composer scope when the ambient scope has no
owner, so a Bot chat opened in place (openStoredBotChat) speaks with its
owner voice; a tile's scope already names its owner and is kept as is.
- Tests: the two direct helper tests are folded into (1) a production-path
test that renders useAutoSpeakReplies under a Bot scope and asserts every
REST audio leg carries the owner (red when the hook's wiring is reverted to
base AND when only `profile` is threaded), and (2) the routing test on
resolveSpeakStreamUrl with an owner object.
- Docs: the fallback sentence matches the code (profile server defaults, active
profile only for ownerless chats); STT stays on the active profile.
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Voice playback resolved its (connection, profile) scope from the ACTIVE
gateway profile (`getApiRequestProfile()`) on every leg of the ladder —
client-direct `fetchVoiceClientConfig`, the speak-stream WS URL and the
`/api/audio/speak` relay — so every Bot spoke with the active profile's
voice regardless of its own `tts.*` config.
`VoicePlaybackOptions.profile` now carries the speaking session's owner
profile; `fetchVoiceClientConfig`/`directTtsConfig`, `resolveSpeakStreamUrl`
and `speakText` accept it and fall back to the active profile when absent.
The session tile publishes `ownerRoute.targetProfile || ownerRoute.profile`
on its ComposerScope, and the three speakers (read-aloud button, auto-speak,
voice conversation) pass it through. The config cache keys on the owner
profile so two Bots never share credentials.
Slim redo of #101545 (@FalconOrtiz).
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Review follow-up on the slim redo of #115168 (@jonpol01).
- `store/projects.ts::revealPath` (Projects kebab, worktree header) routes
through `revealFile`, so a path that is not on this computer toasts
`fileMenu.revealMissing` instead of silently showing nothing; both menus
drop the reveal item on a remote backend (`isDesktopFsRemoteMode`), like
the file trees.
- Statusbar "Open containing folder" is gated on the FOCUSED session's owner
(`isSessionRemote(focusedStoredSessionId)`), not the window's primary
connection: a Connections-tagged tile on a remote gateway inside a
local-primary window hides it, and a local tile in a remote-primary window
keeps it (#115167's per-tile rule).
- The facade unit test is replaced by a statusbar-rendering test that drives
`useStatusbarItems` under a remote connection and under a focused remote
tile in a local window; it goes red when only the hook's gate is reverted.
- `fileMenu.revealMissing` in every locale (ar, ja, ru, zh, zh-hant).
Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
Two seams made the statusbar's "Open containing folder" a silent no-op on a
remote bot's workspace: `hermes:fs:reveal` returned true after
`shell.showItemInFolder`, which silently no-ops on a path that does not exist
on this computer, and `revealDesktopPath` discarded the bridge boolean so
`revealFile` had nothing to toast. The statusbar also built the item
unconditionally while the sidebar trees already hide reveal on a remote
backend (`isDesktopFsRemoteMode`).
- `fs-ipc.ts`: reveal returns false when `fs.existsSync(target)` is false,
before touching the file manager.
- `desktop-fs.ts::revealDesktopPath`: throws `fileMenu.revealMissing` when
the bridge answers false, so `revealFile` toasts instead of reporting success.
- `use-statusbar-items.tsx`: the reveal item is dropped when the connection is
remote, mirroring `file-actions.tsx`.
Slim redo of #115168 (@jonpol01).
Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>