Commit Graph

39175 Commits

Author SHA1 Message Date
teknium1
dad0057271 fix(plugin-guard): v8 — four intake false-positive classes step down where inert
Real catalog pins from the 2026-09-20 intake batch scored on text that cannot run on
the installing host:

1. `.github/workflows/*.yml` — a CI step's own `os.environ['RUNNER_TEMP']` read scored
   `python_os_environ/high` and made a clean plugin `caution` (remarkable). A workflow
   runs on the forge's runner; it now takes the README prose cap (one step down,
   agent-facing shapes like `curl | sh` keep full severity).
2. "pip install" inside a user-facing message literal (`"... no pip install is needed"`,
   image-utils) scored `unpinned_pip_install/medium`; mid-literal, non-command position,
   no exec verb on the line → low. `"pip install x"`, `python -m pip install`, `uv pip`,
   `subprocess.run("pip install …")` keep medium.
3. `desktop_surface_findings()` was being run by batch tooling over every `*.js`/`*.mjs`
   in a repo and flagged a Node sidecar's lazy `import('jszip')` (remarkable). The
   product check was already scoped to `desktop/`; expose that scope as
   `is_desktop_surface()` / `desktop_surface_hits()` so tooling shares it.
4. `127.0.0.1:<port>` (README, .mcp.json, client defaults) scored `hardcoded_ip_port` as
   network egress; a line whose every IP:port is loopback → low. A routable address on
   the line keeps medium.

Every finding stays in the report. PLUGIN_SCANNER_VERSION → plugin-guard-v8 so cached
verdicts on quarantined pins are re-evaluated.
2026-09-20 11:49:00 -07:00
teknium1
dca9da4f6b test: keep the class docstring first and drop the now-redundant inline stub
The cherry-picked fixture landed above TestNodeRuntimeNpmResolution's
docstring, which turned the docstring into a bare string expression;
restore the order. With the class-level autouse stub in place the
per-test `with patch(warm_agent_browser_npx_cache)` in
test_node_failure_returns_failed_labels_and_warns guards nothing extra,
so one seam owns the stub for the whole class.
2026-09-20 11:48:28 -07:00
Konstantin Khlopkov
cbf7aae686 test(update): stub the npx cache warm-up in the remaining _update_node_dependencies test classes (#115034) 2026-09-20 11:48:28 -07:00
Konstantin Khlopkov
f9c632a193 fix(desktop): index explicitly delivered Office documents in Artifacts 2026-09-20 11:47:51 -07:00
Tyler Lyon
c7f7935272 fix(desktop): resolve home-relative attachment refs in preview/download
previewFileTarget() (apps/desktop/electron/main.ts) resolved a preview
target strictly against the agent's working directory (resolveHermesCwd()
/ process.cwd()). Attachment references stored in chat history are
frequently HOME-relative instead ("AppData/Local/hermes/attachments/
foo.xlsx" on Windows, or similarly under HERMES_HOME on macOS/Linux),
so the primary resolution never finds them: the attachment card shows
"File not found" and the Download button is dead, even though the file
is present on disk under HERMES_HOME/attachments.

Fix: when the primary resolution finds neither a file nor a directory,
retry against the user home directory and the HERMES_HOME/attachments
directory (basename fallback) before giving up. Additive only — a
successful primary resolution is never touched, and only relative,
non-file: targets are retried (absolute paths and URLs already had
their one real attempt).

The candidate-generation logic is extracted into a pure, exported
homeRelativeAttachmentCandidates(raw, home, hermesHome) in
electron/hardening.ts (which already owns every other IPC path-resolution
helper: resolveRequestedPathForIpc, resolveReadableFileForIpc,
rejectSensitiveFilePath) rather than inlined in main.ts. main.ts's
previewFileTarget is an unexported function inside an 18k-line facade
with Electron app/window side effects at import time, so it cannot be
unit-tested directly; the pure fallback logic can, DI'd with home/
hermesHome instead of reaching for app.getPath('home') / a module
constant.

Unlike the issue's proposed patch (hardcoded "AppData/Local/hermes/
attachments", Windows-only), this uses the module's existing
cross-platform HERMES_HOME constant (apps/desktop/electron/main.ts,
resolveHermesHome() — already resolves correctly to %LOCALAPPDATA%\
hermes on Windows and ~/.hermes on macOS/Linux per install.ps1/
install.sh), so the fallback works on every platform, not only Windows.

Closes #115609.

Testing:
- 6 new unit tests for homeRelativeAttachmentCandidates: the two real
  candidates in order, backslash normalization, empty-array on an
  absolute path / file: URL / empty input, and the basename-only
  fallback when a ref lost its directory prefix entirely
- Proven red on unmodified main via git stash (all 6 new tests fail
  with "homeRelativeAttachmentCandidates is not a function") and green
  on the fix
- npx vitest run electron/hardening.test.ts -- 51/51 passed (45
  existing + 6 new)
- npx vitest run electron/{preview-reach,preview-capture,
  preview-guest-preload,gateway-file-download,gateway-file-download.fs}.test.ts
  -- 58/58 passed, no regressions
- npx tsc -p tsconfig.electron.json --noEmit -- clean, no new errors
2026-09-20 11:47:15 -07:00
zqy1-1
c143e04498 fix(desktop): a sidebar row only owns presses that started inside it
React re-dispatches an event fired in a portal along the REACT tree, not the DOM
tree. DialogContent portals into <body>, but in the React tree the session
rename dialog is a child of the session row (SessionActionsMenu renders inside
the row's actions slot), so a pointerdown inside the dialog's input still reached
the row shell's own onPointerDown with a target outside the row. The shell's only
guard walks the DOM (target.closest('[data-reorder-handle], [data-row-actions]')),
which cannot match anything mounted at <body>, so the handler fell through to
startSessionDrag(...) — the shared drag session's threshold is 4px, and any mouse
text selection crosses it — and to the forwarded dnd-kit pointer activator:
selecting the session title with the mouse lifted the row, lit every drop target
and armed the reorder.

Gate each shell's own onPointerDown on shellOwnsPress() (a press that started
inside the row's own DOM) before the existing marker exclusion: the session row,
the project row and the gateway group header. The keyboard side of the same leak
was already fixed in #115333.

Coordinates are untouched; the grabber keeps the full dnd-kit handle, so a press
on the row itself still runs both drags off one gesture.

Fixes #116080
2026-09-20 11:46:38 -07:00
finn763
e1ffc7bad3 fix(desktop): stop the rail dropping its end-mark tooltips
Radix feeds `collisionPadding` into the hide (`referenceHidden`) middleware,
which INSETS the trigger's clip box: a trigger whose whole box lies within that
padding of a clipping edge reads as scrolled out, and its bubble mounts into
`visibility: hidden` — no bubble, no error, just a mark that looks dead on
hover. The rail's `.thread-timeline-tick` buttons are 7px tall and drawn flush
against the top and bottom edge of the strip they scroll in, so exactly the
first and last marks qualified. Probed on the middleware: 7px and 11px triggers
hidden at that edge, 13px and 24px visible — the 12px `collisionPadding`.

Rails keep `hideWhenDetached` off from now on. Their ticks unmount when the
strip scrolls them away, so the middleware has nothing to hide there; the
placements whose triggers stay put inside scrolling lists still get it.

Fixes #115723
2026-09-20 11:42:07 -07:00
chelsealong
8c2f9ca9bb fix(desktop): scroll Approval Needed to the pending approval, not the bottom
Fixes #115538

ScrollToBottomButton always requested a full jump-to-bottom, including
while labeled "Approval Needed". PendingApprovalStack is decoupled from
the message that requested it and can end up above newer transcript
content, so a bottom jump can overshoot it and leave the user with
nothing to approve at the destination.

Tag PendingApprovalStack with the owning session id and, when an
approval is pending, scroll directly to that element (scoped by
session so a split view can't jump into a sibling pane's approval)
instead of requesting a bottom jump.
2026-09-20 11:41:32 -07:00
teknium1
93537ddb77 fix(tests): title-upgrade join tolerates a stubbed agent.title_generator module
tests/tui_gateway/test_tui_gateway_server.py swaps agent.title_generator in
sys.modules for a bare ModuleType; the autouse sweep then called
wait_for_title_upgrades on the stub and errored every test in that file at
teardown. A stub spawned no threads, so a missing helper means nothing to
join.
2026-09-20 11:40:16 -07:00
teknium1
4a757f25dc test: join auto-title threads at teardown; stop titling in the sidecar replay test
tests/gateway/test_timestamp_sidecar_replay.py crashed the interpreter on CI
(native fault, green on rerun) on unrelated PRs. Root cause: every
run_conversation turn in its fixture spawns the auto-title upgrade daemon
thread (title_generator.maybe_auto_title). That thread outlives the test,
fails its model call (no provider under CI), and then writes the derived
title into the fixture's SessionDB after the fixture closed it, which
reopens sqlite on the daemon thread (_reopen_after_close_locked) and prints
the auxiliary-failure warning after pytest capture teardown. At the end of
the file the threads are still in native sqlite while the interpreter
finalizes: the check_same_thread=False-at-shutdown SIGSEGV shape of
#113186. Locally the thread finishes in ~250 ms so the race never shows;
on a loaded runner it lands on finalization.

Fix the class, not the file:
- tests/conftest.py: the autouse SessionDB leak sweep now joins the
  auto-title upgrade threads (bounded, agent.title_generator.
  wait_for_title_upgrades) before closing stores, so no title worker
  outlives its test in any file (5 other files spawn them today).
- tests/gateway/test_timestamp_sidecar_replay.py: titling is not under
  test; the fixture no-ops maybe_auto_title (same as
  tests/agent/test_tool_call_incremental_persistence.py), so its own
  db.close() no longer races a worker either.
- tests/hermes_state/test_session_db_leak_sweep.py: handoff pair pinning
  the invariant (a slow upgrade thread started in one test is dead by the
  next); red on base, green with the fix.

Proof (scratch plugin delaying the title model call by 1 s):
base: 2 auto-title threads alive at interpreter exit, every thread
"SessionDB reopened after close() on thread auto-title"; fixed: no thread
spawned / none alive at exit in this file and the other five.
2026-09-20 11:40:16 -07:00
teknium1
3d1c00e1e8 fix: multi-select clarify keeps the typed answer when picking a choice (review follow-up)
The previous commit only exempted multi-select in one direction (type, then
the picks survive). Picking a choice still ran `setDraft('')` / `draft: ''`
unconditionally, so typing the custom answer first and then clicking a choice
silently discarded the typed text and submitted only the picks (probe: type
'something else', click 'staging' -> Other empties; answer ["staging"]).
Guard the draft reset with `!multiSelect` in `selectChoice` and `moveActive`
and keep `stage.draft` in the batch card's `toggleChoice` for multi-select
questions, mirroring the existing `onDraftChange` / `draftFor` exemption.
Single-select stays mutually exclusive.
2026-09-20 11:32:32 -07:00
chelsealong
94939c06c4 fix(desktop): typing a custom clarify answer keeps multi-select picks
In a multi-select clarify card, typing into the "Other" free-text field
wiped every previously picked choice, because the choice/text mutual
exclusion built for single-select cards was applied unconditionally. For
multi-select, the typed text is an additional answer, not a replacement.

Single-question card: onDraftChange and the Other field's onFocus only
clear selectedChoices when the card is not multi-select; the submitted
answer now merges the staged choices with the trimmed draft.

Batch card: draftFor keeps a question's staged choices when it is
multi-select; stagedAnswer merges them with the trimmed draft the same
way.

Fixes #115044
2026-09-20 11:32:32 -07:00
chelsealong
0a32314c20 fix(desktop): clear the swap overlay a superseded wake set, not just the current one
openSession's finally block only cleared $gatewaySwapTarget when
generation === openSessionGeneration, so a wake superseded before its
hydration wait finished (or timed out) skipped the clear entirely. The
two other set/clear paths (activateGatewayForProfile, the agent
activation path) clear unconditionally in their own finally, but any
later open that never sets the target itself (e.g. a paint-first open
without awaitHydration) has nothing to clear it, leaving the "Waking
up..." overlay stuck forever even though the session underneath is
fully functional.

Track which generation actually set the target and key the clear off
that instead, so a superseded wake still runs its own cleanup while a
newer wake's overlay can't be clobbered by an older one's finally.

Fixes #115844.
2026-09-20 11:31:57 -07:00
teknium1
9048fbe27f fix(desktop): bound the persisted group-chat log by characters, not just entries
updateGroupChat persists the whole room map to localStorage on every
write and a failed setItem is swallowed, so a room past the origin quota
silently stops persisting every room. Measured on the real persist path
(scriptedStorage over appendGroupChatEntry): 400 uncapped 8k bodies
serialise to 3.25M chars, 400 64k pastes to 25.6M — well past the ~5M
char quota. Stored bodies are now cut to the same 8,000-char excerpt the
turn prompt renders (at append and in trimGroupChatLog, so remote merges
are bounded too) and the retained log is head-trimmed to a 256k-char
budget (8x the turn window) with watermarks kept consistent.

Wording: the window is bounded in String.length code units; comments and
the docs now say characters instead of KB.

Invariant test: a 400 x 64k room persists under GROUP_CHAT_LOG_RETAIN_CHARS
with the newest entry kept, marked, and its watermark intact.
2026-09-20 11:31:13 -07:00
teknium1
a767747a68 fix(desktop): byte-bound the Bot Mode group-chat turn window
A member's turn prompt rendered only the last 24 room messages since its
last turn, so a busy room routinely lost the head of an exchange (#114341
made the cut visible; this makes it rare). The window is now bounded by
size instead of a small count: up to 200 entries within a 32 KB character
budget, oldest dropped first, the omission marker naming the exact count.
One oversized body is cut to 8 KB with the existing '… [truncated]' mark
rather than evicting the messages around it. The local room log retains
twice the window so a member that skipped a whole window still gets an
exact count instead of a clamped watermark.

Docs: one sentence in the Bot Mode guide. Tests: the existing window
tests now cover the byte bound (exact omitted count, newest kept), a
150-entry delta that fits, and the single-paste truncation.
2026-09-20 11:31:13 -07:00
teknium1
8d08293cf4 test(cron): gate the deleted-cwd witnesses with the repo linux_only marker
Replace the bare skipif(os.name == "nt") on the two deleted-cwd tests with
@pytest.mark.linux_only. The repo convention gates OS-specific tests through
the declared markers (linux_only/macos_only/windows_only in pyproject) so the
OS matrix stays greppable; the witness only needs a host that lets a process
remove its own cwd, which the Linux lane provides.
2026-09-20 11:30:36 -07:00
686f6c61
c89be4c52d fix(cron): spawn bot-chat delivery from a live cwd
The cron bot-chat delivery child inherited the scheduler's cwd; when that
directory had been removed (a kanban worker whose scratch workspace was reaped
by completion cleanup) the child died in `hermes_cli/_startup_fast.py::
ensure_project_root_on_path` — a relative `sys.path` entry goes through
`os.getcwd()` inside `realpath`, which raises FileNotFoundError — before it
could parse argv, and the finished job was booked `delivery_failed`.

- `_run_bot_chat_turn` pins the child's `cwd` to the target home the lane has
  already verified exists (`env["HERMES_HOME"]`).
- `ensure_project_root_on_path` resolves entries through a `realpath` that
  tolerates a gone cwd, so any `hermes` invocation from a dead directory still
  starts (the second half of #102941).

Salvaged from #102967 (@686f6c61), resolved onto the report-driven Popen lane
(#113608); tests drive the CLI entry point and the delivery spawn seam from a
deleted cwd.
2026-09-20 11:30:36 -07:00
teknium1
3d14b20de9 chore(guard): bump scanner versions after the hardcoded_secret carve-out
Cached verdicts are keyed on the scanner version; without the bump a plugin or
skill already scanned "dangerous" for an env-var NAME constant would keep its
cached verdict and stay blocked.
2026-09-20 11:30:03 -07:00
liuhao1024
1b50e99a47 fix(skills): an env-var NAME constant is not an embedded credential
The generic hardcoded_secret pattern fired on constants whose value is the
NAME of the credential environment variable (an ENV_PASSWORD-style constant
holding the string "MYPLUGIN_" + "APP_PASSWORD"): a line-oriented regex
cannot tell a reference to a secret from the secret itself, so one critical
finding made such plugins uninstallable with no --force override (#116221).

Both copies of the generic pattern (the shared threat-pattern library and
the skill/plugin guard table the installer actually walks) now skip a value
that is itself a SHOUTY_SNAKE environment-variable name (at least two
underscore-separated segments). The carve-out is scoped case-sensitive
because both tables compile with IGNORECASE: a lowercase snake value is the
passphrase shape, and requiring an underscore segment keeps
underscore-free all-caps credentials (AWS access key IDs, base32 secrets)
matched. Prefixed provider tokens (sk-, ghp_, ...) and the dedicated
provider-signature patterns are unaffected.

Regression tests pin both directions: the env-var-name line no longer
flags, and the passphrase / AKIA / base32 / prefixed-token shapes still do.
2026-09-20 11:30:03 -07:00
Puvaan Raaj
f02a128dd1 fix(desktop): show empty project repository headers 2026-09-20 11:29:26 -07:00
whyyagswhy
a0caeda3ad fix(desktop): scope remote spawn umask 077 to the mkdir subshell 2026-09-20 11:28:51 -07:00
finn763
6f29d89767 fix(computer-use): windowless, non-interactive Windows autostart repair
The shared cua-driver install/refresh path reached _repair_cua_driver_autostart_windows, which spawned powershell.exe without CREATE_NO_WINDOW and without -NonInteractive. Under a windowless parent (Desktop backend, detached gateway, logon task) that child allocated its OWN console: a blank PowerShell window parked on the desktop for as long as the elevated -Verb RunAs -Wait child lived, with no interactive prompt it could unwind from. Measured live on Windows 11 (pythonw parent): production kwargs open a visible console/Terminal window, the same spawn with CREATE_NO_WINDOW opens none.

A failed repair also failed the whole install, so a compatible, working Computer Use toolset read as broken and the install was re-attempted on the next run. It now degrades instead: warn plus the elevated 'cua-driver autostart enable' hint.

Fixes #115017
2026-09-20 11:28:15 -07:00
Aaron08140
efa31fff8c docs(hooks): state the Windows interpreter routing the command contract now has
Line 1687 documents 'runs via shlex.split, shell=False', which after the
preceding commit is no longer the whole story on Windows: the bare script paths
every example on this page use are routed through their interpreter there. Left
unstated, the page keeps describing the behaviour that caused the bug.
2026-09-20 11:27:39 -07:00
Aaron08140
829c91aa00 fix(agent): run bare script-path hooks on Windows through their interpreter
A hook declared as `command: "~/.hermes/agent-hooks/x.sh"` — the shape every
example in website/docs/user-guide/features/hooks.md uses — cannot start on
Windows. _spawn() shlex.splits the command and Popen()s it with shell=False, so
the kernel reads the shebang on POSIX but CreateProcess on Windows receives a
text file and answers WinError 193. The hook then reports no returncode, which
a fail_closed gate treats as a failure and every other consumer silently skips.

Route a first argument that is an existing file with a mapped suffix through its
interpreter, reusing tools.environments.local._find_bash() so the resolution
keeps the ordering that avoids WSL's bash.exe (#115124) and surfaces Git-for-
Windows' own guidance when it is absent. POSIX argv is untouched. Suffixes we
cannot resolve an interpreter for still fail, but the diagnostic now names the
remedy instead of the OS's localized complaint.

Repairs five of this file's tests that have been red on native Windows
(TestCallbackSubprocess x4, hooks TestHooksTest::test_fires_real_subprocess_and_parses_block);
the two that stay red are drive-letter/`~` tokenization, which #68508 owns.

Verified on Windows 11 26200 / cp936 with real subprocesses: bare .sh, .bash and
.py hooks execute and carry their exit code; a missing path still reads
"command not found"; `git --version` is unaffected.
2026-09-20 11:27:39 -07:00
teknium1
fb97ade55a fix(desktop): trim the quit-finalization tests to two invariants and never parent the quit prompt on a hidden window
Fold the four salvaged tests into the two invariants that matter (#116376):
a Windows quit past its deadline forces exactly one hard exit and never arms
off Windows; a completed quit cancels the fallback and it never re-arms.

Also the §E one-liner from the report: pick the quit prompt's parent from the
VISIBLE windows. With a turn in flight and the main window already gone,
getAllWindows()[0] could be a hidden aux window, so the "Quit Anyway" dialog
was invisible and the held quit unanswerable.
2026-09-20 11:27:03 -07:00
joaomarcos
a598d5273e fix(desktop): bound Windows Electron quit finalization 2026-09-20 11:27:03 -07:00
teknium1
c2ccad49a6 chore: map contributor email for #97299 salvage 2026-09-20 11:26:29 -07:00
teknium1
dbf6ba142b test(desktop): give the cancellation gate fixture the hand-off dep
The previous commit widened UpdateGateDeps with isHandoffActive; this
fixture predates it and would fail the tsc pass in check:lint.
2026-09-20 11:26:29 -07:00
Screminpal
3b7549d26c fix(desktop): keep backend gate closed through update handoff
Preserve the local backend startup gate after the Windows cmd wrapper exits successfully but before the real detached updater claims the marker. Reuse the existing handoff quit state and cover the stale-wrapper interval with a regression test.
2026-09-20 11:26:29 -07:00
tobenwarrior
93940214ea fix(desktop): render pinned session rows in the Inbox-style card variant
Inbox style is a render variant, not a grouping — it rides whichever view
is active. The pinned section call was the one flat-list section that never
received the `card` prop, so with Inbox style on the same sidebar column
mixed 44px two-line inline rows (pinned) directly above 54px three-line
cards (recents), with two different model-name formats breaking exactly at
the section boundary.

Pass `card={cardRows}` to the pinned section, matching recents and the
project overviews. The toggle then governs both sections the same way; an
explicit opt-out for pinned rows (#89308) composes on top of this default.

Tests: the pinned section renders the card geometry when $sidebarCardRows
is on and stays inline when it is off (red on base: pinned row rendered
`min-h-[1.625rem]` inline geometry with Inbox style on).

Refs #116325
2026-09-20 11:25:52 -07:00
Jony
7d33bc00f9 fix(desktop): show group member failure reasons 2026-09-20 11:21:03 -07:00
teknium1
8fefd35bb9 test(telegram): skip the real-ffmpeg geometry check where ffmpeg is absent
``test_video_helpers_read_real_geometry`` renders a clip with ffmpeg and
reads it back with ffprobe. The Linux CI runner ships without either, so
the test errored with FileNotFoundError instead of exercising anything.
Skip it when the binaries are missing (same shape as
tests/gateway/test_voice_command.py); the two mocked tests still cover the
send path everywhere.
2026-09-20 11:20:26 -07:00
teknium1
c9f4a47827 test(telegram): trim the video-metadata suites and map the contributor email
Keep the entry-point tests (geometry + thumbnail reach bot.send_video / the
hermes send media path; an unprobeable file still sends) and the real-file
helper check; drop the no-ffmpeg degradation and non-video-extension guards.
2026-09-20 11:20:26 -07:00
Steve Hsu
97051e4194 fix(telegram): attach real video geometry and a thumbnail so large uploads aren't square
`sendVideo` gives back `width=320 height=320 duration=0` with no thumbnail once an
upload is large enough that Telegram skips its own video processing, and clients
then draw the message as a square tile — for portrait reels and 16:9 clips alike,
even though the delivered file itself is correct.

Measured on one 6 s 2560x1440 clip, inspecting the Bot API response: 4.9 MB and
9.8 MB keep `2560x1440` / duration 7 / a 320x180 thumbnail; 14.8 MB, 19.4 MB and
23.0 MB degrade to the square placeholder; the same 23.0 MB file sent with
`width`/`height`/`duration` plus a JPEG `thumbnail` comes back `2560x1440` with a
320x180 thumbnail.

Probe the local file with ffprobe and attach a 320px-wide JPEG frame from it, on
both Telegram send paths: the gateway adapter's `send_video` and the standalone
`hermes send` media sender. Both helpers return nothing when ffmpeg/ffprobe is
unavailable, which keeps the previous behaviour for hosts without them.
2026-09-20 11:20:26 -07:00
fangliquan
a22c29dd5c fix(tui_gateway): scope manual compression to session profile 2026-09-20 11:19:50 -07:00
teknium1
b787fb9128 fix(tui): Ctrl+D exits from an empty composer on macOS too
The exit binding matched isAction(key, ch, "d"), which on macOS means Cmd+D:
Ghostty consumes Cmd+D for split panes and literal Ctrl+D never matched, so
the TUI stayed open. Ctrl+D is the terminal EOF convention, not a Cmd
shortcut, so it now also routes through the existing isMacActionFallback seam
(target union gains "d"), and on every platform it exits only when the
composer holds no text, buffered lines or attachments, matching the classic
CLI. Slim redo of #116454.

Co-authored-by: Mohamad Kanso <91088196+MohamadKanso@users.noreply.github.com>
2026-09-20 11:15:12 -07:00
teknium1
8ac3215ac6 fix(desktop): spawn the Windows update hand-off wrapper with a hidden console it can share
`start "" /min` told cmd's `start` to allocate a NEW console for the
PowerShell hand-off script and only minimize it, so every Desktop update
created a visible ConsoleWindowClass window (#116161). Switching to
`start /b` (previous commit) makes the child share the wrapper's console
instead — but the wrapper was spawned `detached: true`, which libuv maps to
DETACHED_PROCESS: the wrapper has NO console (and the OS ignores
CREATE_NO_WINDOW alongside DETACHED_PROCESS), so under `/b` powershell
would have been forced to allocate its own visible console — the very
failure mode the original `start` layer worked around.

Spawn the wrapper non-detached instead: libuv then honours `windowsHide`
(CREATE_NO_WINDOW), cmd.exe owns one hidden console, and the script runs
inside it. Survival past our own exit does not need `detached` — libuv's
job object is created with JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, so
grandchildren are never members, and Windows does not tie a process's
lifetime to its parent. The recipe carries `detached: false` so the call
site cannot drift back to the detached shape.

Not live-run on Windows; mechanism per the reporter's SetWinEventHook
measurements (CREATE_NO_WINDOW + `start /b` → no window) and libuv's
process.c.
2026-09-20 11:14:36 -07:00
joaomarcos
f58605b5c6 fix(desktop): keep Windows update hand-off hidden 2026-09-20 11:14:36 -07:00
teknium1
a6deb961ea test: trim managed-local validation tests to the two invariants
Keep the Use-button case (staged but absent from the spawn-only live
listing is accepted) and the control (never staged stays rejected); the
other five re-asserted the same branch from different angles.
2026-09-20 11:14:00 -07:00
Konstantin Khlopkov
d02762ccca fix(models): credit the managed local-models library in /model validation
The validation ladder had no llamacpp branch: a switch to a freshly downloaded
local model fell through to the generic live-listing probe, which hard-rejects
when the spawn-only /v1/models listing has not learned the new file yet — so the
Local Models Use button and the composer picker could never succeed for a
non-catalog model. The managed runtime now validates against the staged library
on disk (the source of truth for what the user downloaded), case-insensitively;
ids that were never staged keep the live-listing verdict.

The activate flow's self-heal had the same blind spot: its rescan only ran when
this process supervised the server, but ensure_local_runtime returns None when
another process owns it — precisely the desktop situation after a download job
bounced the router once. Probe the live listing through the persisted endpoint
and bounce the router when it lacks the model.
2026-09-20 11:14:00 -07:00
liuhao1024
5141b3122b fix(skills): point the godmode skill's self-referencing install paths at its new location
The skill was moved to the security category as a pure R100 rename
(fdc90346ea), but the eleven install-path strings embedded in its own docs
and scripts kept the pre-move category. On a fresh install the skill lands
under the security category in the skills home, so every copy-pasteable
snippet in the docs and the loader fallbacks in the scripts raise
FileNotFoundError on first use. Rewrite both embedding forms — the
joined-string form and the segmented Path(...) form — and add a CI
contract test that fails when any optional skill references its own
install path under a category that does not match its location.
2026-09-20 11:13:24 -07:00
teknium1
314076ca63 fix(messaging): hermes send reports dropped MEDIA attachments instead of success:true
filter_media_delivery_paths kept only the survivors, so a MEDIA path that did
not exist on the host (or was denied by the delivery policy) vanished with a
host-side warning while the caller got success:true / exit 0 and booked a
delivery that never happened. _validated_delivery_path and
filter_media_delivery_paths take an optional `dropped` list that collects
{path, reason}; send_message_tool passes it and, when anything was dropped,
returns success:false + partial_success:true + media_dropped + an error line,
which hermes send already turns into a non-zero exit. Slim redo of #115913
(@fangliquanflq): same payload shape, out-parameter instead of a wrapper pair.

Co-authored-by: fangliquan <fangliquan@qq.com>
2026-09-20 11:12:48 -07:00
teknium1
c1b5a5012b fix: the Desktop-owned dashboard fallback spawn keeps its packaged web dist (review follow-up)
Keying the packaged-dist strip solely on headless_backend also stripped
HERMES_WEB_DIST from the Desktop's own legacy `dashboard --no-open` fallback
(taken when the `serve --help` probe times out on a cold host), sending a
packaged install with no node toolchain into _build_web_ui(fatal=True).
The fallback child is told apart by the per-spawn
HERMES_DASHBOARD_SESSION_TOKEN, which the terminal pane never receives and
the terminal tool env policy strips from agent children. One invariant test,
red on the previous head.
2026-09-20 11:12:12 -07:00
teknium1
1d1284765b test: trim #116107 dashboard dist tests to the two invariants
Drop the caller-managed-dist case that only holds with the predicate
rewrite in hermes_cli/main_dashboard.py, which this salvage does not
carry (the substring check is unchanged).
2026-09-20 11:12:12 -07:00
fangliquan
aa14b81423 fix(dashboard): isolate packaged renderer from browser launches 2026-09-20 11:12:12 -07:00
Konstantin Khlopkov
6abbc02228 fix(tui): no gateway respawn after graceful-exit kill (#114987) 2026-09-20 11:04:04 -07:00
teknium1
133004ac79 fix(gateway-windows): decode schtasks strictly as UTF-8 first, one localized denial vocabulary, seam tests
Follow-up on the salvaged commit: try strict UTF-8 before the ANSI code page (ASCII and real
UTF-8 pass, ANSI multi-byte text fails loudly instead of being mis-read by a dense codec such
as GBK), share the localized "access is denied" words between the fallback and elevation
patterns, and drive the tests through `_exec_schtasks` (fake subprocess with the reporter's GBK
bytes on Linux; a real schtasks task with a non-ASCII argument on the Windows lane).
2026-09-20 10:59:28 -07:00
joaomarcos
e333cc0099 fix(gateway-windows): preserve localized task arguments 2026-09-20 10:59:28 -07:00
Konstantin Khlopkov
1a3bbe6109 fix(desktop): let two-modifier navigation chords fire while typing
Rebinding session switching to mod+alt+arrows was inert while the
composer held focus: the input gate rejected every combo whose base key
is a navigation key before the primary-modifier carve-out could run, so
an explicitly rebound chord like mod+alt+left never dispatched. Since
every session activation re-focuses the newly shown composer, switching
degraded to one switch per background click.

A chord carrying Alt on top of a primary modifier has no native
text-editing meaning (it is not option+left word-jump or cmd+left
line-start), and the shipped mod+alt+t tab-strip default already
establishes this gesture class. Narrow the base-key rejection to
navigation chords that can be text navigation: single primary modifier
(plus/minus Shift) or bare Alt. The accidental-trap class stays
input-local, so ctrl+arrow and cmd+arrow bindings remain native editing
gestures while typing.

Refs #115980
2026-09-20 10:58:51 -07:00
joaomarcos
7dcb667716 fix(windows): ask gateway setup install questions once and stop after UAC hand-off
The setup wizard asked start-now/start-on-login, then called the Windows
installer without forwarding the answers, so the installer asked the same
two questions again. After a UAC hand-off (nothing registered yet in the
parent), the wizard then started the service, which re-entered the install
flow and re-offered the UAC prompt while the elevated child was still
waiting on consent.

Forward both answers into the Windows installer and let the installer own
the start decision: it starts the gateway itself on both the Scheduled
Task and Startup-folder paths, and reports a UAC hand-off as False so the
wizard parent stops instead of starting an unregistered service. Fixes #116550.
2026-09-20 10:58:14 -07:00