Commit Graph

22221 Commits

Author SHA1 Message Date
Brooklyn Nicholson a3da6d8071 feat(desktop): quiet suggestions the user has repeatedly ignored
The bus now keeps a session-scoped declined ledger: a pill the user
watched appear and let die three times stops re-offering for the rest
of the session. Acting on a pill clears its count, so a suggestion
that was taken can come back for the next trigger. In-memory on
purpose — a fresh session is a fresh chance.
2026-08-13 02:17:42 -05:00
Teknium fe5e7799f2 refactor: fold tailored intents guidance into the connect classifier
The cherry-picked #79448 predated #85049's _classify_connect_exception,
so it added a parallel PrivilegedIntentsRequired branch ahead of the
classifier (plus its own _is_privileged_intents_required detector).
Fold the tailored guidance into the classifier's existing intents arm
instead: one classification path, one error code (discord_intents_required),
and the message now names exactly the intents Hermes requested (Message
Content always; Server Members only when username/role allowlists need it).
Wizard callout, docs corrections, and tests from #79448 kept as-is.
2026-08-13 00:10:30 -07:00
rainbowgits b10e7890b6 fix(discord): name missing privileged intents and stop reconnect loop
PrivilegedIntentsRequired is a Developer Portal config error; surface which
intents Hermes requested as a non-retryable fatal and teach setup/docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 00:10:30 -07:00
Daniel Magro 590d547b40 fix(auth): tolerate legacy Codex suppression data 2026-08-12 23:47:38 -07:00
Daniel Magro c28114a5f8 fix(tests): isolate suite fixtures from host auth 2026-08-12 23:47:28 -07:00
Carl Taylor 654435210c feat(cron): surface model drift impact in Desktop 2026-08-12 23:47:22 -07:00
hermes-seaeye[bot] 1ef0a366da
fmt(js): `npm run fix` on merge (#85098)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-13 06:44:04 +00:00
Teknium 89a84e1ae6
feat: profiles.list/profiles.create ws RPC + plugin session-navigation doors (#85093)
Desktop plugins reach the backend exclusively through the generic ws
JSON-RPC door (host.request), but profile enumeration/creation only
existed on the dashboard REST router, which plugins cannot reach — so
anything 'one chat per agent profile'-shaped (bot rosters, profile
pickers, team panes) was impossible to build as a plugin.

- tui_gateway/methods_profiles.py: new @method handlers
  * profiles.list — profiles + optional last_session preview per profile
    (mirrors session.list's kanban/tool deny-list; best-effort per-profile
    state.db probe degrades to null instead of failing the call)
  * profiles.create — ws twin of POST /api/profiles (clone_from/clone_all/
    no_skills/description), plus optional SOUL.md content and a best-effort
    model+provider pin; mirrors the CLI flow (seed skills, safe alias)
  Both run on the RPC pool, not the WS reader thread (list_profiles walks
  skill trees; create copies bundles).
- SDK: host.openSession(id, { profile, intent }) — open a stored session
  the way core surfaces do, soft-swapping to the owning profile's backend
  first (ensureGatewayProfile), and host.newChat(profile) — fresh draft in
  a named profile (same door as the sidebar's per-profile '+').
- Docs: desktop-plugin-sdk.md gains both surfaces.

First consumer: a Grok Bot-style 'Bots' roster plugin (one persistent
chat per agent profile with a New Agent dialog) built on exactly these
four doors.
2026-08-12 23:33:58 -07:00
Brooklyn Nicholson 91a30705eb refactor(desktop): generalize the composer suggestion pills into a provider bus
The pill strip from the inline-MCP work is worth more than one source, so
the MCP-specific store splits into two layers with the same UX contract
(session-scoped, capped, self-limiting, one-click with narrated
idle→working→done):

- store/composer-suggestions.ts — the bus. Draft providers register into
  the existing debounced sampler; event providers push/withdraw directly.
  Offerings merge (event before draft), dedupe by provider-namespaced key,
  and keep reference identity on no-ops.
- store/suggestion-providers/mcp.ts — the founding provider, behavior
  unchanged: directory keyword/host matching, configured-server exclusion,
  one-click connect with OAuth cancel + config rollback.
- composer/suggestion-pills.tsx — the generic strip; phases and cancel
  live here, action/rollback/toasts stay with the provider's invoke.

No new pills yet — this is the seam for them.
2026-08-13 01:22:35 -05:00
Brooklyn Nicholson c7a1bfea07 docs: note the clarify recommended-choice ordering in the tools reference 2026-08-13 01:19:42 -05:00
Brooklyn Nicholson fd6af8f832 feat(desktop): render the clarify (Recommended) label in tertiary text
The card reads the labelled choices off the gateway request rather than
the raw tool args -- the backend applies the label there, and the card
only mounts once the request exists, so the args are a hydration-race
fallback. RECOMMENDED_LABEL and bareChoice live in the clarify store so
the component and the choice-length guard share one definition; without
the guard a long option could be dropped for length the label added.
2026-08-13 01:19:42 -05:00
Brooklyn Nicholson 10cf651484 feat(clarify): label the agent's recommended choice on every surface
The clarify schema now tells the model to order choices best-first, and
mark_recommended tags element 0 with "(Recommended)" at the tool layer --
the one platform-agnostic entry point -- so CLI, TUI, desktop, and every
messaging adapter inherit the label without a copy each. Each surface
already defaults its cursor to index 0, so the recommendation is the
pre-highlighted row too.

The label is presentation only: strip_recommended takes it back off
user_response, and choices_offered reports the bare list, so the agent
never reasons about (or echoes back) a string it did not write. Typed
replies on messaging platforms match with or without the suffix.
2026-08-13 01:19:42 -05:00
Brooklyn Nicholson 08a3b20dff test: register setup_mcp in the desktop_ui toolset + post-hook contracts
The toolset inventory and the post-hook ownership contract both
enumerate the GUI tools; the new tool joins both lists (and the
emit-once parametrization actually exercises its executor path).
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson 6ef0fc4f62 fix(desktop): cancel MCP OAuth flows server-side so a retry doesn't 409 2026-08-13 01:06:51 -05:00
Brooklyn Nicholson 3efce9b98c feat(desktop): suggest MCP servers from the composer draft as brand pills
A renderer-local directory of official hosted MCP remotes (URL-only,
vendor-documented endpoints — deliberately not the reviewed install
catalog) powers keyword and pasted-link suggestions: typing jira or
pasting a *.atlassian.net URL floats an 'Add Atlassian' pill in the
composer's micro-action strip. Matching is whole-word/phrase (unicode
boundaries) plus strict host-suffix on links, host hits outrank
keywords, capped at two, debounced 600ms, and excludes servers already
in mcp_servers. Pills are session-scoped like the micro-action badges
and self-limiting rather than dismissible — they exist only while a
trigger is in the draft. A click drafts the setup request; the agent's
setup_mcp card carries the consent. Brand glyphs extracted from the
mcp-tab into lib/mcp-brands (shared, monochrome marks follow the theme
so GitHub/Notion/Vercel survive dark mode).
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson 6cd4793081 feat(desktop): render setup_mcp as an interactive consent card in the transcript
The card follows the approval bar's consent vocabulary (primary-tinted
action + ghost decline, ⌘⏎/Esc with clarify's focus-stand-down rule) on
clarify's widget shell. Install prefers the reviewed catalog entry (env
prompts inline, background installs polled to completion) and falls back
to the desktop suggestion directory via the validated add-server POST +
OAuth; success reloads live MCP tools before unblocking the agent so it
resumes with the tools it was just promised. Esc stays live mid-flight
as cancel — the abandoned flow aborts at its next poll and a post-write
cancel rolls the config entry back. Typing while the card is pending
declines it and sends normally (skipClarifyRequest's pattern), and the
request/tool.start rows merge on the server arg so reconnects can't
double-render the card.
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson adbc77eb50 feat(desktop): setup_mcp tool — inline MCP consent card over the clarify-style blocking bridge
New desktop_ui tool: the agent proposes an MCP server (install/enable/
authorize + a one-line reason) and blocks on mcp.setup.request until the
renderer's consent card answers mcp.setup.respond with the outcome
(installed/enabled/authorized/declined/unanswered/error). Same lifecycle
as clarify: 10-min timeout, allow_expired late answers, tool lifecycle
events forced on so the card mounts even with tool progress off. Desktop
prompt hint steers the model to the tool instead of hand-editing config;
every other surface keeps the schema out and is pointed at hermes mcp
install.
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson a9eb7e09d9 feat(desktop): marquee clipped inline row titles on hover
The one-line session row gets the exact treatment the inbox card's title
already has: hovering a truncated title glides the clipped tail into view —
one direction at constant speed, a short hold at each end, then a snap back.
Same armMarquee/disarmMarquee handlers, same CSS, so overflow is measured on
pointerenter, short titles never move, hover state lives in DOM attributes
(no re-render of the memoized row), and reduced motion disables it.
2026-08-13 00:55:33 -05:00
Teknium f84ecd3607 chore: map zhjay@stu.xjtu.edu.cn -> ZHJay for contributor attribution
(cherry picked from commit 06899b54437f7bb36b126d96a1e740afeb34d855)
2026-08-12 22:38:17 -07:00
张豪杰 1535c114c9 test(desktop): cover connection.json owner-only mode end to end
The helpers were tested; nothing proved main.ts called them. Reverting both
call sites and both imports in readDesktopConnectionConfig /
writeDesktopConnectionConfig left the whole suite green (947 passed / 2
skipped, tsc 0, eslint clean, e2e 1 passed 1 skipped) while connection.json
went back to 0644 — the user-visible fix this PR promises was untested.

The e2e spec could not catch it by construction: it asserts the ENCRYPTION
contract with a raw-bytes scan, and safeStorage keeps the token opaque
regardless of the file's mode, so a 0644 file passes that scan every time.
There was no mode assertion anywhere in e2e/.

Adds the missing third contract — unreadable by other local accounts — on all
three paths that can produce the file:

- write: assert the mode of the artifact test 1 already proves the app wrote.
- read, valid file: seed the app's own encrypted connection.json back to 0644
  and assert launch tightens it. Scoped to the MODE only, so it is independent
  of the still-deferred plaintext migration — the fixture's token is already
  ciphertext, so nothing re-encrypts, no #62319 opt-in marker is involved, and
  no rotation guidance is owed.
- read, corrupt file: a truncated file still holds the token bytes and throws
  into the swallowing catch, so it would be the one file never tightened. This
  is the only test that distinguishes the chmod's placement relative to the
  parse.

Also moves the tighten above JSON.parse for exactly that reason, and pins the
cache invariant the placement depends on: the tighten must be a chmod, not a
rewrite, because it sits inside the function whose cache keys on mtimeMs.

Asserted as `mode & 0o077 === 0` rather than `=== 0o600` to avoid a
change-detector, and skipped on win32, where chmod maps to the read-only bit
and the fix deliberately no-ops (ACLs are PR #77527).

Every assertion was mutation-tested: reverting the full wiring fails all three;
reverting only the write path fails only the write test; deleting only the
tighten-on-read fails only the two read tests; moving the tighten below the
parse fails only the corrupt test; making the tighten a rewrite instead of a
chmod fails the mtime assertions. Bundle greps confirmed each mutation reached
dist/electron-main.mjs before the run.

(cherry picked from commit 99cfc16e7c)
2026-08-12 22:38:17 -07:00
张豪杰 7e151bd9d3 fix(desktop): create connection.json owner-only
`connection.json` under the desktop app's Electron `userData` was written with no
file mode, so it landed at the `0644` umask default — while its two
credential-bearing neighbours in the same directory, `desktop-installation.json`
and `native-oauth-tokens.json`, were already `0600`. That file holds the
safeStorage-encrypted gateway token plus the fields that are NOT encrypted: the
gateway URL and the SSH host, user, and key path.

- Route the single write choke point through a helper that creates the file
  owner-only and atomically.
- Tighten an already-existing `0644` file once per launch on the read path, so
  installs that already have one do not stay world-readable until the next save.
- Refuse to act on a path that is a symlink or not owned by the current user,
  matching the guards `desktop-installation.ts` already applies to its sibling.

The symlink guard alone turned out to be insufficient, and that is worth
recording: `writeSecretFileAtomic` tightens its *temp* path, so a symlink planted
at `connection.json.tmp` meant `writeFileSync` followed it, the guard correctly
bailed, and `renameSync` then moved the link onto `connection.json` permanently.
Measured, guard-only vs. as-landed:

    guards only          token leaked: true    config is a symlink: true   755
    guards + temp unlink token leaked: false   config is a symlink: false  600

So the temp path is unlinked before the write.

Issue #77486's headline claim — that a dashboard session token is persisted in
plaintext — does not hold against main. The token has been safeStorage-encrypted
since the desktop app reached mainline in 51c68d4ab, and `encryptDesktopSecret`
aborts with an actionable message rather than degrading to plaintext when
safeStorage is unavailable. The `{ encoding: 'plain', value }` literal does exist
at main.ts:7084, but only on the `persistToken: false` branch, whose sole caller
is the connection-test handler, which never writes. So no mainline path *writes*
a plaintext token. The commits that did contain a plaintext-writing fallback
(d3d177283, d208f2c2c) are not ancestors of main — they live only on
upstream/bb/gui-* and the desktop-pr20059-installers pre-release tag.

At-rest migration of legacy non-safeStorage payloads is deliberately NOT included.
An earlier revision of this branch implemented it and it was removed after review
reproduced two token-loss paths: it force-converts the opt-in plaintext choice
PR #62319 adds (silently reverting the user's decision, then destroying the token
on the next launch without the `--password-store=basic` flag), and it converts a
portable credential into a keychain-bound one with no consent — destroying the
only recoverable copy while not remediating the real exposure, since every
existing backup still holds the plaintext and the true remedy is rotation. It also
persisted raw `parsed`, bypassing `sanitizeConnectionProfiles`. A comment at the
read path records the three preconditions any future attempt needs.

`decryptDesktopSecret`'s non-safeStorage read fallback is untouched — it is what
lets a pre-release or hand-edited config work at all.

Windows still inherits the userData directory ACL rather than an explicit
owner-only one; mode bits are advisory there, so that half is deferred to
PR #77527 rather than growing a second ACL implementation here.

e2e: `at-rest-connection-token.spec.ts` asserts the at-rest contract
implementation-independently — the token's plaintext value (and its base64 form)
must not appear in a raw-bytes scan of any file under userData or HERMES_HOME,
AND the app must still put the exact original token on the wire after a restart,
so a fix that simply drops the token cannot pass. Proven non-vacuous by mutation:
writing `{ encoding: 'plain', value }` still fails the scan while the
file-exists and gateway-URL guards pass. The migration case is a documented
`test.fixme` naming its three blockers.

Electron project 928 -> 924 tests (-9 migration, +5 new guard and
mechanism-isolation). Two of those five exist because reverting either owner-only
mechanism alone initially scored zero failures — they were masking each other, so
either could have been deleted green.

(cherry picked from commit 6e01add657)
2026-08-12 22:38:17 -07:00
brooklyn! 7626105380
Merge pull request #84943 from NousResearch/bb/review-summary-chrome
Self-improvement review row wears the same gold→purple chrome as a memory write
2026-08-13 00:18:44 -05:00
Teknium 6397776fe8 refactor: simplify discord classifier + move attention threshold to config.yaml
- Collapse the duplicated discord LoginFailure/PrivilegedIntentsRequired
  classification (name-match + isinstance blocks repeated the same code/
  message tuples) into a single _is() helper — one message per failure.
- Replace the user-facing HERMES_RECONNECT_ATTENTION_AFTER_SECONDS env var
  with agent.reconnect_attention_after in config.yaml (default 7200, 0
  disables), bridged internally like gateway_timeout. .env is for secrets.
- Use _float_env for robust parsing instead of bare int(os.getenv(...)).
- Document terminal classification + needs_attention escalation in
  website/docs/user-guide/configuration.md.
2026-08-12 22:16:12 -07:00
Shannon Sands 91bc822330 fix(gateway): classify terminal adapter connect failures + escalate long-lived retry loops (OOF-156)
Fleet triage after the 2026-08-11 storm resolution found agents whose sole
platform had been silently 'retrying' for weeks: revoked Telegram tokens,
Discord privileged-intent rejections, and Photon sidecars that can never
start were all funnelled into the indefinite reconnect queue with no owner
signal (OOF-151/152/153, epic OOF-156).

Two-part fix:

1. Per-adapter classification — by exception TYPE only, never message text:
   - telegram: InvalidToken/Forbidden -> telegram_auth_error, retryable=False
     (new _looks_like_auth_error, mirrors _looks_like_network_error)
   - discord: LoginFailure -> discord_auth_error, PrivilegedIntentsRequired
     -> discord_intents_required (both retryable=False); every other path now
     sets an explicit code (previously the generic branch set NO fatal info,
     which the gateway read as 'probably transient')
   - photon: new typed PhotonSidecarStartupError; deps-install failure ->
     SIDECAR_DEPS_MISSING and missing node binary -> SIDECAR_NODE_MISSING
     (retryable=False); ambiguous startup crashes stay retryable
   - email: IMAP/SMTP failures now always set a fatal code;
     SMTPAuthenticationError -> email_auth_error, retryable=False (IMAP4.error
     is type-ambiguous between bad creds and transient NOs, so IMAP stays
     retryable)

2. Gateway escalation — platforms continuously in the reconnect queue past
   HERMES_RECONNECT_ATTENTION_AFTER_SECONDS (default 2h, 0 disables) get
   needs_attention=true + retrying_since stamped into runtime status, once
   per episode, cleared on successful reconnect.

Deliberately NOT a circuit breaker: retries never stop. The auto-pause
mechanism was removed for good reason (transient DNS outages left bots
silently dead); this preserves that and only adds visibility. No new
platform_state enum values — NAS's status schema is strict — only additive
fields.

Unknown exception types always stay retryable: a false terminal recreates
the silently-dead-bot problem, and the escalation path covers
misclassified permanent failures.
2026-08-12 22:16:12 -07:00
Brooklyn Nicholson 5af2c2ff52 fix(desktop): messages typed during approval/sudo/secret prompts run as the next turn
Typing while the turn was parked on a blocking prompt routed the text
through steer (session.redirect), which sat undelivered behind the
blocked tool batch — nothing rendered, and stopping the turn to force it
through resolved the prompt to empty and ended the turn as the literal
"Operation interrupted." row, eating the message.

Clarify already had a carve-out (typing skips the question and steers)
because a real message IS an answer to a clarify. Approval/sudo/secret
have no such answer path, so the busy submit now queues the words as the
next turn instead: the prompt stays answerable, the queue drains on
settle, and the busy button advertises queue rather than steer while one
is pending. Slash commands still execute inline, and another session's
prompt never affects this one.
2026-08-13 00:13:36 -05:00
Hermes Agent 04846b2120 ci: retrigger after 0-job workflow startup failure 2026-08-12 22:11:15 -07:00
Teknium 1706502aa7
feat(computer_use): spill full element tree to a cache file and report numeric bounds_scale (#85047) 2026-08-12 21:53:11 -07:00
Teknium 4ea2a0e546 Revert "Inspired by Perplexity Computer: Model Council mode for Mixture of Agents"
This reverts commit 8d9e18d40b.
2026-08-12 21:50:35 -07:00
Teknium 825a9753c1
fix(computer_use): resolve cua-driver at its official Windows installer path (#85038) 2026-08-12 21:30:42 -07:00
Teknium 6c9d6d9d5b
fix(computer_use): keep capture responses inside the tool-result budget and surface coordinate-space + typed-page hints (#85037) 2026-08-12 21:28:34 -07:00
hermes-seaeye[bot] 08606fc231
fmt(js): `npm run fix` on merge (#85024)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-13 04:08:31 +00:00
Brooklyn Nicholson 4d9202b963 feat(desktop): right-click the shell chrome for window-level actions
Right-clicking anywhere the app owns no menu of its own — the titlebar
gutter, an empty pane body, the sidebar background — did nothing at all.
Electron's native handler bails on non-editable, non-selected content by
design, so those surfaces had no menu to fall back to.

Wrap the shell in a fallback context menu carrying the verbs that belong
to the window rather than to a row: new session, new window, command
palette, toggle the status bar, settings, update Hermes. Every row reuses
the store action and the copy its Cmd+K twin already uses, so the two
can't drift.

A guard on an inner element keeps it a fallback: a right-click that lands
inside a surface with its own context menu, on an editable, or on a live
selection stops propagating before Radix's trigger sees it, leaving that
surface's menu — or Electron's native edit menu — in charge.
2026-08-12 22:55:37 -05:00
Brooklyn Nicholson 071d27d1c3 feat(desktop): paste a PR review comment as structured composer context
A pasted GitHub PR comment deep link (#discussion_r… / #issuecomment-…)
now lands as a typed review attachment instead of a bare url chip. The
card attaches optimistically and resolves through gh in the background —
author, file:line anchor, body, and the diff hunk — expanding at send
into an anchored fenced block, so "address this" carries exactly what
"this" is. When gh can't answer (offline, unauthenticated, foreign repo,
remote gateway) the card downgrades to the plain url ref and nothing is
lost.
2026-08-12 22:48:00 -05:00
Brooklyn Nicholson 2960bf37ab feat(desktop): steer a queued prompt into the live turn
Queued turns could only wait for the settle or interrupt the turn to jump
the line. Text-only queue entries now carry a steer action while the agent
is busy: the entry rides the existing mid-turn redirect (no interrupt, no
drain lock), is consumed only when the gateway accepts it, and lifts a
park so the rest of the queue keeps flowing. Slash commands and entries
with attachments keep their existing semantics.
2026-08-12 22:48:00 -05:00
hermes-seaeye[bot] 07ed2fdb60
fmt(js): `npm run fix` on merge (#85010)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-13 03:33:45 +00:00
Brooklyn Nicholson 8018f9e016 test(desktop): shipped sidebar defaults now include the preview line
The inbox card's preview ships on, so the default row-meta contract is
['preview', 'updated'] — the reset test pins the new shipped view.
2026-08-12 22:24:37 -05:00
Brooklyn Nicholson 8403595f78 style(desktop): satisfy jsx-prop sort on the recents section card prop 2026-08-12 22:24:37 -05:00
Brooklyn Nicholson 92620e4ad3 feat(desktop): live task progress on inbox cards
An inbox card whose session is working through a todo list shows its "X/Y"
fraction at the far right of the footer line, updating live as items
complete. The count projects the runtime-keyed todo map to stored session
ids through the same lineage-alias fallback the working/attention
projections use, emits pre-rendered strings so unchanged rows never repaint,
and skips cancelled items on both sides of the fraction.
2026-08-12 22:24:37 -05:00
Brooklyn Nicholson c96e0daba5 feat(desktop): marquee clipped card titles on hover
Hovering a truncated inbox-card title glides the clipped tail into view —
one direction at constant speed, a short hold at each end, then a snap back
to the start. Overflow is measured on pointerenter and the animation arms
only when the text actually clips, so short titles never wiggle. State lives
in DOM attributes and CSS variables, so a hover never re-renders the
memoized row, and the blanket reduced-motion override already disables it.
2026-08-12 22:24:37 -05:00
Brooklyn Nicholson 45f663b746 feat(desktop): opt-in inbox-style session cards in the sidebar
A new "Inbox style" toggle in the sidebar filter menu renders the flat
recents list as cards: a workspace header line (project when it resolves,
else the cwd leaf, else Home) with the age at its right edge, the title
grouped with a one-line last-message preview, and a model + size footer.
The preview line ships on by default and has its own Show-menu toggle,
offered only while Inbox style is active — the one-line row has nowhere
to put it.

A render variant, deliberately not a grouping — it composes with whichever
grouping is active and only the flat recents list opts in; pinned, project,
and messaging surfaces keep the one-line row. Spacing hangs off a single
--card-gap variable; the title/preview pair is one grouped cell with its own
tighter internal gap. The age and kebab sit in flow inside the header line
rather than a full-height side column, so title, preview, and footer span
the card's entire width.

The card's project label reads through a selector that resolves the label
string, so tree polls with fresh atom identity repaint only rows whose label
actually changed.
2026-08-12 22:24:37 -05:00
Brooklyn Nicholson b6ed6542a1 fix(desktop): stop double scrollbar gutters shaving the recents list
The virtualized recents list sat inside a wrapper that was itself a themed
scroller, so two 4px classic-scrollbar gutters stacked and every row ended
8px short of the sidebar edge the pinned list reaches. Drop the wrapper's
scroller when the virtual list owns scrolling, use the overlay scrollbar on
the virtual scroller (zero gutter, native fade), and neutralize both axes of
the wrapper overflow — `overflow-y-visible` next to `overflow-x-hidden`
computes to `auto` and still reserved a gutter.
2026-08-12 22:24:37 -05:00
Brooklyn Nicholson fb206fd12b fix(desktop): idle sessions without a project color get a visible dot
The idle dot variant had no background of its own, so a settled session
outside any project rendered a literally invisible dot — the row read as
missing its status indicator next to its neighbours. Fall back to the
faintest filled grey; a project color still wins when there is one.
2026-08-12 22:24:37 -05:00
Teknium e3983f91eb feat(plugins): capability-gated ctx.platform_actions facade (#64176)
Minimal v1 platform action surface for plugins, routed through the live
gateway adapter registry — the sanctioned alternative to monkeypatching an
adapter:

- ctx.platform_actions.add_reaction(platform, chat_id, message_id, emoji)
- ctx.platform_actions.set_thread_title(platform, chat_id, thread_id, title)

Gated behind a new 'gateway.platform_actions' capability in
CAPABILITY_REGISTRY (legacy key plugins.entries.<id>.allow_platform_actions,
default OFF), re-checked on every call via plugin_capability_granted (the
#84912 consent registry). Verbs validate the adapter exists and is connected,
return structured {ok, error, detail} results with stable error codes, and
never raise into hook dispatch. Every action is audit-logged with plugin id,
verb, platform, and outcome.

Telegram routes to _set_reaction / rename_dm_topic; Discord to
fetch_message().add_reaction / rename_thread. No adapter handles or raw SDK
objects are exposed.

Docs: plugins.md platform-actions section with the security note and the
explicit raw-SDK-not-shipped statement.
2026-08-12 20:10:51 -07:00
Teknium 3b7c940208 feat(gateway): more normalized gateway_platform_event types (#64176)
Extend the normalized-envelope pipeline shipped in #82063 with new event
types, each with its own versioned, event-local payload contract:

- Telegram: message_edited (edited_message updates; editor-identity auth
  extraction, forum topic thread_id, bounded text/caption, ISO edited_at)
- Discord: message_edited, message_deleted, thread_created, thread_renamed
  (on_message_edit/delete, on_thread_create/update fire-sites with has_hook
  no-subscriber fast-paths, bot-authored events dropped, rename-only
  filtering on thread updates)

All events flow through the same gateway-owned post-auth boundary; malformed
or unauthorized events drop, fail closed. Raw SDK payload access is
deliberately NOT shipped (round-2 correction: needs its own
gateway.raw_events capability and design).

The Discord fire-site machinery (no-subscriber fast-path, observer isolation,
connect-time wiring) adapts the observer-hook design from PR #62584
(@paoloantinori) onto the normalized-envelope contract; PR #36875's raw
telegram update hook is superseded by the same correction.

Docs: hooks.md gains per-event payload contract tables.

Co-authored-by: Paolo Antinori <pantinor@redhat.com>
2026-08-12 20:10:51 -07:00
Teknium fe8b44dac4 fix(ci): sync lazy_deps SDK pins + update WAL vacuum test contract
The Aug 12 pin bump (91345435a) updated pyproject/uv.lock but not
tools/lazy_deps.py, tripping the #31817 downgrade-guard tests; the
post-VACUUM TRUNCATE fix landed without updating the checkpoint test
that pinned the old no-TRUNCATE rule. Both red on pristine main.
2026-08-12 20:02:07 -07:00
Teknium 46e20083d8 feat(plugins): plugin packs — declarative, shareable plugin sets (#64166)
Adds hermes-pack.yaml: a single YAML file pinning a set of plugins to
exact 40-char commit SHAs with optional non-secret plugins.entries
config seeds and a declared (not yet installed) skills list.

CLI:
- hermes plugins pack install <path|https-url> [--force]: mandatory
  review screen (plugins + refs + declared capabilities), one summary
  confirmation, then fan-out through the existing pinned install path.
  Per-plugin capability consent rides the standard #64228 flow — a pack
  never bulk-grants. Partial failures reported per plugin; non-zero
  exit when any fail. Interactive only (no --yes in v1).
- hermes plugins pack export [--enabled-only] [--name]: pack YAML on
  stdout from install metadata (repo + exact SHA); local-only plugins
  become warning comments; secrets/capability grants stripped.
- hermes plugins pack show <path|url>: dry-run view.

Supply chain: refs must be exact 40-char SHAs (tags/branches rejected
naming the entry, same rule as the community index); config seeds
reject secret-shaped, capability, and allow_* keys; bare names resolve
through the community index; https-only URL fetch with size cap.

Tests: tests/hermes_cli/test_plugin_packs.py (36) — parse/validate,
SHA enforcement, mocked install fan-out, consent-per-plugin assertion,
export round-trip + sanitization, partial-failure exit code, parser
wiring. No live network.

Docs: user-guide plugins.md packs section (notes packs build on the
manifest v2 fields per #64165) + cli-commands.md rows.

Closes #64166
2026-08-12 19:56:44 -07:00
Teknium f4c2c263f0 docs(plugins): note autostash behavior on plugins update 2026-08-12 19:44:50 -07:00
Teknium eb214ad148 Inspired by Factory Droid: plugin updates autostash local changes
Factory Droid v0.188.0 (Aug 4, 2026): 'Updating a plugin marketplace now
succeeds when its checkout has local changes instead of failing.'

Hermes had the same failure: users who tweak an installed plugin in place
(config constants, small patches) hit 'Your local changes ... would be
overwritten by merge' on every 'hermes plugins update <name>' and the
dashboard update path — the plugin becomes permanently un-updatable
until they hand-run git.

_git_pull_plugin_dir() now autostashes before the pull and re-applies
after, reusing the ref-compared stash discipline hermes update already
uses for the main checkout (PR #70161):

- clean tree → identical single pull, no behavior change
- dirty tree → stash push --include-untracked (ref-compared so 'nothing
  saved' aborts before touching the checkout), pull, stash apply
- clean re-apply → drop the stash entry, note in output
- conflicted re-apply → reset to the updated revision (plugin stays
  importable, no conflict markers on disk) and KEEP the stash entry
  with recovery instructions
- failed pull with a stash → restore the user's edits before reporting

Covers both callers: cmd_update (CLI) and dashboard_update_user_plugin.
Real-git E2E tests for all four paths + sabotage-verified (tests fail
on the old single-pull implementation).
2026-08-12 19:44:50 -07:00
Teknium 6ee58f4088 Inspired by Muse Code: opt-in git worktree isolation for delegated subagents
Adds delegation.worktree_isolation (default: false). When enabled, each
delegate_task child gets its own git worktree branched from the repo's
current HEAD under <repo>/.worktrees/subagent-<id>, its terminal session
starts there, and its goal message carries the isolation contract
(work + commit in the worktree; parent reviews/merges the branch).

- tools/subagent_worktree.py: clean-room implementation from Muse Code's
  documented --subagent-worktree-isolation behavior (create per-child
  worktree, finalize/inspect after run, auto-prune clean no-commit
  worktrees, keep anything holding work).
- tools/delegate_tool.py: config gate + per-child setup in
  _run_single_child; result entries gain a "worktree" field (path,
  branch, commits, dirty, pruned) only when isolation engaged — the
  default-off wire shape is byte-identical.
- Git-only + local-terminal-backend-only; non-git dirs, remote backends,
  or any worktree failure degrade silently to shared-workspace behavior.
- Tests: tests/tools/test_subagent_worktree.py (15 tests, real git
  repos) + E2E through _run_single_child with a real repo verified
  parent-checkout isolation, branch reviewability, prune, and
  default-off shape pinning.
- Docs: delegation feature page section + configuration.md key.
2026-08-12 19:44:45 -07:00
Hermes Agent f508c6e40a Inspired by Perplexity Computer: session-librarian skill — prompt-driven session library management
Adds a bundled productivity skill that lets Hermes organize the user's own
session library conversationally: find sessions by topic via session_search,
summarize goals/decisions from bookends, rename them meaningfully, propose
archives/prunes with a mandatory plan-first + dry-run discipline, and split
requests into parallel workstreams via delegate_task.

Inspired by Perplexity Computer's session management by prompt (changelog
07/27/26): find/summarize past sessions, fork focused follow-ups, rename,
pin/archive with plan-first confirmation, and fan one request out into
parallel per-task sessions.
2026-08-12 19:44:39 -07:00