The ordered publication pass moves nousresearch/hermes-agent:stable/latest
only after it reads the R2 stable channel head. When that read fails
(v0.21.6: "Channel repository authority mismatch"), the release publishes
and the aliases never move.
`python -m scripts.releases.docker promote --version X.Y.Z` reads the
published vX.Y.Z receipt tag and runs the existing promote_stable with the
digest it binds (slim checked against the receipt, desktop from its own
attempt tag). docker-promote-stable.yml dispatches it with the Docker Hub
credentials only CI holds.
- hermes_cli: _find_plugin_entry is the single resolver (key / manifest name / unique leaf /
catalog name via the installer-owned record). enable/disable/show/capabilities/update/remove,
the dashboard and plugins.manage settings all go through it, so the catalog name you
installed with keeps working (17 catalog entries install under another manifest name).
_catalog_install_on_disk reuses the same catalog lookup.
- Desktop hub: join agent rows to desktop halves by package folder (what Electron stamps),
not manifest name, so a self-cloned package is one row instead of two.
- Desktop: turning a unified package on (install dialog 'Enable after install' or the Agent
switch) also turns its opt-in desktop half on, unless the user switched it off. Off stays
per half. Capability consent is untouched.
- docs: plugin-catalog page.
The close result carried messages=None when a test stubbed the finalize boundary
(SessionCloseResult wants a list), dropped end_reason="tui_close" from the
teardown call, and two existing exact-result assertions predate the new field.
A plugin's on_session_finalize hook can now return a str or {"message": str}
and Hermes shows it at the session boundary without starting a turn or
rewriting the model reply:
- CLI /new: printed before the new session; CLI quit: printed above the
exit summary; one-shot -q/-z: printed to stderr (stdout stays the answer)
- TUI: session.close returns the messages; /new shows them as system lines
- Gateway /new and shutdown: sent to the owning chat via the adapter, while
adapters are still connected
on_session_end stays observer-only: it fires after every turn, not at a
session boundary. Gateway finalize dispatch moves to
gateway/run_shutdown_session_end.py.
The Desktop loads a unified package's desktop half from the copy in
desktop-plugins/<name>/, not from the package. Editing the source shipped
nothing and nothing said so:
- a package symlinked into plugins/ (the in-place dev layout the Python
loader already follows) was never copied at all — Dirent.isDirectory()
is false for a symlink;
- an installed package edited in place stayed stale until Rescan/restart.
Electron now follows symlinked package dirs, marks their copy linked, and
re-syncs it on any byte difference. The renderer watches a linked copy's
source plugin.js and, on save, runs the reconcile and hot-reloads. Catalog
and git installs keep the mtime rule. `hermes plugins doctor` warns when
the copy differs from the source and says how to refresh it.
Pin EaseIT-cz/vornik at cf817ad47d22 (CE release 2026.10.5), plugin 0.11.3.
Since 0.4.0 the plugin adds: forgetting reaches Vornik (/vornik-memory,
/vornik-forget), the Vornik approval transport (Hermes's own approval
prompts answered on the user's paired phone via the local vornikctl, opt-in
with hermes vornik approvals on), and fixes to both. The Disclosure line
covers the new behaviour.
Re-pins LetsFG to 437b91d. The bundled skill and README named three
tools the hosted server has since renamed, and described a booking
approval flow the server no longer has. The banner is the new LetsFG
logo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.4.3 (66a6efcc) -> 0.5.1 (a7303c89, main). The adopted range is
66a6efc..a7303c8:
#8 f936bfe _busy() no longer reads a bare .git/REBASE_HEAD as a rebase in
progress. On git 2.55 a finished `rebase --continue` leaves that
file behind on a clean tree and `rebase --abort`/`--quit` will
not remove it, so the worktree stayed busy for good and the hook
skipped its whole fetch/commit path for it.
#2 280c57c A direct push the remote refuses on policy grounds (branch
protection, a PR-required ruleset, GH013/GH006) no longer
strands a commit: the hook drops only its own unpushed commit,
leaves the edit staged and dirty, stops committing on that
branch and reports it once, instead of re-pushing and re-logging
the same remote error every pass.
#4 a7303c8 A dead cwd fails open instead of refusing the tool call.
capabilities: unchanged -- the same four hooks (pre_tool_call,
post_tool_call, post_llm_call, on_session_end), no tools, no middleware, no
env vars.
description: adds the refusal clause to the existing disclosure -- a branch
whose direct push the remote refuses on policy grounds is never committed to
again, and the edit stays staged and dirty.
Re-pins to 7be60e3, which stops calling the retired managed_uv shim and
reads security.allow_lazy_installs through hermes_cli.config, failing
closed when it is unset, false, malformed or unreadable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses review: a change that always denied webhook runs would have passed.
The webhook/msgraph_webhook case now runs under both deny and approve; all four
parametrizations are red on main.
Replaces the six TestUnattendedPlatformExecAskLeak cases in test_approval.py (past its
size cap) with two invariants in a sibling file, both red on main:
- webhook / msgraph_webhook deny from approvals.unattended_mode even with the
TurnRunner-registered notifier, and never reach the human gate;
- api_server denies without a notifier and keeps the ask path once a client bridge
(/v1/runs, streaming chat) registers one.
security.md: unattended_mode now names the api_server exception.
Root cause first identified in #100554 (@fangliquanflq); webhook leg from #134821.
The TurnRunner lane registers a gateway notify callback for every inbound
turn with no platform branch (run_turn_runner._run_conversation_with_approval),
so on webhook/msgraph_webhook the 'no notifier' discriminator can never fire:
is_ask stays True, the adapter renders no send_exec_approval surface, the
inbound lane is POST -> 202 with no reader, and the turn blocks in
_await_gateway_decision for the full approval timeout before failing closed
— the residual stall of #100532 that the notifier derivation missed.
A registered notifier only means 'someone can answer' on api_server, whose
turn paths choose whether to register one. Add _NOTIFIER_BLIND_APPROVAL_PLATFORMS
({webhook, msgraph_webhook}) and drop is_ask there regardless of registration;
api_server keeps the notifier test. New tests drive the gate with a registered
notifier on both platforms (the suite previously had no
HERMES_SESSION_PLATFORM=webhook case at all).
start_gateway() sets HERMES_EXEC_ASK=1 process-wide, so an api_server/webhook
turn inside a gateway still reads as 'ask' in _presence() even though
_is_gateway_approval_context() already excluded the platform. The unattended
branch then never runs: a smart ESCALATE (or any manual-mode dangerous
command) falls through to the interactive transport, and on a session with no
gateway notify callback — non-streaming /v1/chat/completions, webhook,
msgraph_webhook — the card parks as pending_approval that no client can ever
answer, while approvals.unattended_mode is never consulted (#100532).
Drop is_ask for unattended-platform sessions without a registered notify
callback, so the unattended branch resolves instantly from
approvals.unattended_mode before the guardian call runs. /v1/runs and
streaming chat completions register their callback and keep the ask path.