The bare `hermes plugins` picker preselected only rows listed in
plugins.enabled. Bundled platforms, backends and model providers are active
without a list entry, so they opened unticked, and the save on exit wrote every
unticked row into plugins.disabled: opening the picker and leaving without a
change disabled every messaging adapter on the next gateway restart.
Rows now open ticked by the load-time rule (_plugin_status), and only rows the
user flipped are written.
Ported onto the plugins_cmd_toggle sibling and the admission-authority save
(the original targeted the pre-split plugins_cmd.py). The nested
canonical-key composite test now ticks its row explicitly: it handed the menu
a checkbox state that contradicted the config and relied on the old
rebuild-everything save. The original helper-level tests are rewritten
against real admission in a follow-up commit.
(cherry picked from commit 3a0d5f1b8bce3b23b115ba4995b8c23dfbba9ad0)
Ink TUI and desktop never registered an inject host, and sharing
set_gateway_message_injector with a live messaging gateway would let
the last writer win. A separate host queues the reported session_key
onto that session's prompt queue and leaves other keys for the gateway.
Fixes#87412
A plugin that lives in a monorepo subdirectory (the Hindsight catalog entry:
vectorize-io/hindsight#hindsight-integrations/hermes) was cloned with every
file in the repository even at --depth 1: 170 MB for a 2 MB plugin folder.
On a slow link that exceeds any reasonable deadline, so `hermes update`'s
Hindsight migration failed with "Git clone timed out" every time.
Subdirectory installs now do a blobless clone (--filter=blob:none
--no-checkout) and a sparse checkout of just that folder, written as the
classic info/sparse-checkout file so older Git clients work too. Servers
without partial-clone support ignore the filter and fall back to the old
download.
Because a partial clone downloads file contents at checkout time, the
checkout step (pinned and unpinned) now gets the configured network
deadline and the credential fallback, same as clone and fetch. Every
timeout error names plugins.clone_timeout_seconds so users can find the
knob.
Existing test fakes of _clone_plugin_repo accept the new subdir argument.
Auxiliary LLM calls (titling, compression, MoA advisors/aggregator, vision,
approval, ...) never reached any plugin hook: hook-based observability and
cost plugins were structurally blind to them. Teknium's ruling on #79733:
NEW events rather than reusing the turn-scoped pre/post_api_request pair,
so existing subscribers keep their per-turn semantics.
- agent/auxiliary_hooks.py (new sibling): builds the pre_api_request /
post_api_request payload shape plus `aux_task`, `api_request_id`
(`aux-...`, shared by every attempt of one logical call), `retry_count`,
`streaming`, parent-turn `session_id`/`task_id`/`turn_id` when a main
turn is in flight; fail-open (a raising/hung subscriber is logged and
the aux task proceeds); post carries `error`/`error_type` on failure.
- agent/auxiliary_client.py: the three relay funnels every physical
attempt shares (_relay_sync_completion / _relay_async_completion /
_relay_sync_stream) run under the hook pair — retries and fallbacks
included. Main-loop *_api_request events do not fire for aux calls.
- Catalogue: VALID_HOOKS, bounded-timeout hook set, `hermes hooks test`
sample payloads, hooks.md / plugins index / observer-hooks / plugins.md
tables, agent + plugins AGENTS.md.
- tests/agent/test_auxiliary_hooks.py: 2 invariants (pair fires with
aux_task and no api_request events; raising subscriber never breaks
the call). First is red on origin/main.
Supersedes #32416 (@zrmnelson), #68060 (@JonZal), #77518 (@hsy5571615),
#79826 (@webtecnica) — their relay-boundary placement, usage
normalisation and fail-open policy shaped this implementation.
Co-authored-by: zrmnelson <zacharynelson1@gmail.com>
Co-authored-by: Jonas Zalys <jonas@tryholo.ai>
Co-authored-by: saitsuki <nukuom976228@gmail.com>
Co-authored-by: webtecnica <webtecnica@gmail.com>
A plugin whose import or register() never returns (an infinite loop, a blocking
network call) held PluginManager.discover_and_load() forever, and with it every
synchronous caller: `hermes chat`, gateway startup, ACP session/new (#108139).
Each plugin's import + register() now runs under `plugins.load_timeout_seconds`
(default 10, 0 disables, max 600) on a daemon worker. On overrun the plugin is
recorded as failed with "load timed out after Ns" (same channel as every other
load failure: startup WARNING, `/plugins`, `list_plugins()`), its pre-hang
registrations are disposed, and discovery continues with the next plugin. The
abandoned worker's later `ctx.register_*`/`subscribe`/`on_unload` calls are
refused with a WARNING (the context is marked abandoned), so a late registration
can never land in a registry the failure path already swept. Abandoned loaders
are capped per process (8); past the cap further loads are refused with a named
reason rather than run inline, which would recreate the hang (#98382 shape).
Because the worker cannot own the caller's RLocks: the deferred-platform eager
fallback now runs outside the replacement transaction, discovery re-entered from
a loader worker returns on the already-set discovered flag instead of blocking on
the sweep's lock, and such a worker never joins the background discovery thread
that is waiting on it.
Annotated-tag pins (F8): a catalog `sha` recorded as `git rev-parse <tag>` names
the TAG object, while HEAD can only ever be the commit it points at. The scan
trust check compared HEAD against the unpeeled sha (so every tag-pinned entry
lost the reviewed-pin bypass and prompted on caution findings) and the sidecar
recorded the peeled commit, so `update_available` was true forever and every
`update` re-installed. The installer now peels the pin (`<sha>^{commit}`) for
trust, records `pin` on the catalog block only when the checkout satisfies it
(empty for an off-pin `--ref` install), and every at-pin check goes through
`at_catalog_pin(sidecar, entry_sha)` (repin, dashboard payload, TUI rows).
Re-pin consent (F10): `hermes plugins update` on a catalog install replaced the
tree without asking, even when the new pin declared new tools, hooks, Python
dependencies, host capabilities or a Desktop half. `repin_catalog_plugin` now
diffs the installed manifest against the staged clone BEFORE anything moves
(`_install_plugin_core(before_swap=...)`) and, on a widening:
- CLI: prints the delta and asks y/N (non-interactive → not applied, fail
closed); after a changed re-pin it runs the same `_run_capability_consent`
grant path as the git-pull `update`.
- `plugins.manage update` RPC and the dashboard REST route answer
`{ok: false, consent_required: true, delta, delta_lines}` with nothing
changed; a retry with `accept_capabilities: true` applies it. Desktop shows
the delta in its confirm dialog; the web dashboard uses `window.confirm`.
- Gateway contract regenerated (`accept_capabilities` param; `consent_required`,
`delta`, `delta_lines`, `error` result fields).
Catalog audit findings F8 and F10 (low severity, no issue filed).
`~/.hermes/hooks/` auto-loads every valid `HOOK.yaml` + `handler.py` at
gateway startup with no `plugins.enabled` gate. That is the documented
contract since 3988c3c245 ("Implicit (dir trust)" in the comparison
table), but the plugins page's "disabled by default" promise read as if
it covered gateway hooks too (#37963). Maintainer ruling: keep implicit
dir trust, fix the docs.
- hooks.md: new "Trust model" section stating exactly what loads, when,
how, and that placing the files is the opt-in; comparison-table cell
links to it and the plugin-hooks consent cell now says
`plugins.enabled`.
- plugins.md: note scoping `plugins.enabled` away from gateway hooks.
- developer-guide/plugins: one sentence at the gateway-hook recipe.
- security.md: "Trusted-by-placement extension points" section
cross-linked from hooks.md.
updating.md never said what requires-python >=3.11,<3.15 with every dep
gated on >=3.14 means for users: only 3.14 runs Hermes, the wider range
only lets pre-PM venvs run one more hermes update, and pip/brew installs
on older interpreters get a dependency-less package.
security.md described truststore but not that the startup CA guard,
SSLConfigurationError and HERMES_SKIP_SSL_GUARD are gone, or that the
CA env vars are still read by plain requests/urllib calls.
pm.plugins_state.read_home_selection raises on any unparsable profile
config.yaml and that blocks dependency prep for every home; plugins.md
and profiles.md now say so.
Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).
Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.
uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
Every catalog card gets an 'Open in Hermes Desktop' link:
hermes://plugin/install?catalog=<name>. The app resolves the reviewed pin
itself, so the page hands it a catalog name and never a repo URL; the CLI
install command stays in the expanded card for people without the app.
In the in-app picker embed the existing '+ Add to this Agent' button is
unchanged.
A `catalog=<name>` deep link now resolves the name against the live plugin
catalog feed (the same `/docs/api/plugins.json` the Capabilities → Plugins
picker renders) and opens the Install Plugin dialog in its reviewed/pinned
catalog mode — identical to an in-app catalog pick, via one shared
`openCatalogPluginInstall` helper the Plugins tab now uses too.
The `catalog` param claims the link outright: an unknown, invalid, or
unresolvable name is a clear error toast and nothing else. It is never
reinterpreted as a git identifier, so a link cannot smuggle an unreviewed
repo behind a familiar-looking name (a `repo=` riding along is ignored).
`hermes doctor` reported "GitHub token configured (authenticated API access)" for any
value in .env, including an expired classic PAT — the very token that shadowed a working
`gh` login for every git-auth clone (#115257). The resolver half (fall through to the gh
CLI when GitHub refuses the .env token) landed separately; this adds the diagnostic half:
a "GitHub token" row under API Connectivity that sends the configured token to
api.github.com/user and, on 401, names the variable and the .env file so the user can
remove or replace it. No token configured: the row is skipped (the Skills Hub section
already reports gh-CLI / no-token state).
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim
salvage + its test and workflow steps dropped), the shim re-exec stays retired,
package.json carries no electron-builder block (afterExtract identity stamp wired
into electron-builder.config.cjs instead; after-pack.mjs keeps signing only),
Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap
(export_scratch_tmp_env) is taken and re-run after profile resolution.
Conflicts resolved toward the branch: PM owns dependency preparation, the
Windows shim re-exec/hand-off path stays retired (main's shim-parent wait,
gateway-resume env token and update_cmd_deps tests dropped), docs describe
the PM update flow. The docker workflow parks install-stamp.json around the
toolchain step instead of deleting it so tests/docker can compare provenance.
A plugin repository is a codebase, and the threat regexes were written for a SKILL.md
the agent executes verbatim. The same text in a README uninstall step, a refusal list,
a test fixture, a JSON scenery data URI, a redaction regex or a `gh api | base64 -d |
grep` dev script was scored as the plugin's own runtime behaviour: crypto-prices
(#115353) was hard-blocked by `rm -rf "$HOME/.hermes/plugins/crypto-prices"` in its
README, and a dozen catalog pins sat at `caution` on fixtures and prose alone.
`tools/plugin_guard_context.py` recognises each class of inert context and only ever
lowers a finding; nothing is deleted and every finding stays in the report:
- documentation prose (`.md/.txt/.rst/.html`, not `SKILL.md`, `after-install.md` or a
bundled `skills/` tree): command/path shapes step down once, so a doc line can never
be `dangerous`; the plugin's own-install-dir `rm` is a note. Injection, Markdown
exfil, agent-config edits, `curl | sh`, `authorized_keys` and leaked keys keep full
severity.
- test trees / fixtures (root test dirs, `__tests__`/`__fixtures__` at any depth,
`*.test.*`, `test_*.py`): quoted-only hostile strings and key-shaped corpora are
notes; test code that executes on import steps down once (caution).
- whole-line comments and CHANGELOG.md score as prose.
- `encoded_exfil` on base64 whose decoded head is a media magic (PNG/JPEG/WOFF/PDF...)
is informational.
- `sudo` / `env|` as an alternation member inside a regex or string literal is a note;
the same word in a command string is not.
- `base64 -d` piped into a text filter is a note; into a shell/interpreter it is not.
Bumps PLUGIN_SCANNER_VERSION to v5 so cached verdicts re-evaluate.
Closes-blocker-for: #115353
Mechanical `check_doc_links.py --fix` pass over website/docs (hand-authored
and generated pages) and the zh-Hans mirror: 1,868 route-style links
(`](/section/page#anchor)`, `](/docs/...)`) become `](../section/page.md#anchor)`.
Every target was asserted to exist on disk; anchors and query strings are
preserved; fenced code blocks and inline-code examples are untouched.
Two dead targets found by the converter were fixed by hand first:
memory-providers.md linked `/user-guide/plugins` (page is
`user-guide/features/plugins`), and the zh-Hans learning-path still linked the
removed `rl-training` page — ported the EN treatment (external Atropos link).
Docusaurus build after: EN locale 0 unresolved Markdown links, 0 broken links,
0 broken anchors.
Widen the anonymous-first decision from #114545 to the whole class. The
clone-only wrapper (_clone_with_auth_fallback) folds into one runner,
git_credentials.run_git_with_credential_fallback, used by every site that
attached a stored credential unconditionally:
- plugins_cmd._run_plugin_git -> clone, the pinned --ref fetch in
_checkout_exact_revision, and the pull in _git_pull_plugin_dir
(`hermes plugins update`), which the thread on #114526 showed still
failing with "could not read Username ... terminal prompts disabled"
after the clone succeeded anonymously
- mcp_catalog._do_git_install (catalog MCP git installs)
- profile_distribution._git_clone (profile distributions from a git URL)
Why the runner and not per-caller guards: the decision belongs to the
credential layer once; a rejected credential sent pre-emptively is what
turns a working anonymous clone into a 401 that git can only answer with
the prompt noninteractive_git_env disables. The credential-required
classifier also matches git's "returned error: 401" spelling (the picked
version needed a trailing space that git never prints) and bytes output.
Side effect: `gh auth token` is now shelled out only after a refusal, so
public installs no longer pay that subprocess at all.
Tests: the sibling verbs (fetch, pull) x (public, refused) and the negative
that a non-credential failure never resolves a credential; the SSH no-op
case from the pick is already asserted by the private-remote test. Docs:
website/docs/user-guide/features/plugins.md.
Fixes#114526
Co-authored-by: holny <holny@users.noreply.github.com>
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
Follow-up to the cherry-picked #112146 (@KoNit-K):
- Move the __main__ demotion to the end of _filter_findings behind a
severity == "critical" guard and a MAIN_GUARD_DEMOTIONS table (the
DOC_PROSE_DEMOTIONS idiom), so it is a one-step cap that can never
re-raise a finding an earlier remap already lowered.
- Treat ValueError from ast.parse (NUL bytes, undecodable text) like a
SyntaxError: no cap, the file is runtime code (fail closed).
- Bump PLUGIN_SCANNER_VERSION to plugin-guard-v4: the verdict semantics
for a plugin shape changed, and the version is what install output
and recorded provenance show.
- Trim to two invariant tests: the reporter's repro shape (token inside
the guard -> caution + confirmable/--force, same token above the guard
-> dangerous, --force refused) and narrowness (destructive payload and
a provider-shaped sk- key inside the guard, plus an unparseable file,
all stay critical -> dangerous).
- Docs: one paragraph on the __main__ cap next to the test-tree cap.
Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
Skipping `tests/`, `spec/`, ... in EXCLUDED_DIRS made those trees
invisible to the guard, but `plugins_loader._load_directory_module`
sets `submodule_search_locations=[plugin_dir]`, so a plugin
`__init__.py` doing `from .tests import evil` imports and runs whatever
lives there: a `tests/evil.py` with a destructive root remove scanned
`dangerous` on main and `safe` on this branch. `_walk` also matched the
names at any depth, so `src/spec/handler.py` — plain runtime code — went
unscanned.
Keep scanning everything; instead cap a critical finding located under a
ROOT-level test dir at `high`, so the verdict is `caution` (confirmation
required, `--force` overridable) rather than the un-overridable
`dangerous`. Fixture strings still cannot brick an install, which was
the reported problem, while a critical in any runtime file (`setup.sh`,
`src/spec/...`) still yields `dangerous`. Trade-off stated in the PR
body: hostile code deliberately placed under `tests/` is now
force-installable rather than blocked outright.
Docs no longer claim test code never runs.
User-visible scanner behaviour changed in this PR (test trees skipped, the block
reason names the critical rule ids), so the plugin docs say so in the same PR.
Reapplied onto current main. The branch had drifted ~3348 commits and a trial
merge produced 48 conflict markers, so this is the same change re-landed rather
than a rebase of the old history.
_interrupt_and_clear_session interrupts the running agent without signalling
plugins, so a plugin holding a per-turn external resource — an outbound RPC
waiting on a tool result the loop will never consume — has no way to learn the
turn is gone. Dispatch agent_loop_stopped immediately after
running_agent.interrupt(), gated on a real running agent: the pending-sentinel
/stop path has no in-flight work, so firing there would be noise.
Per review on #27208, the current helper's behaviour is preserved untouched —
multiplex-aware _adapter_for_source() resolution and cached-agent eviction both
still run; the hook is additive and its dispatch failures are swallowed so a
misbehaving plugin cannot break an interrupt.
Tests fail without the change (hook registration and dispatch) and pass with
it. The three failures in tests/hermes_cli/test_plugins.py::TestPluginDiscovery
are pre-existing on this checkout and reproduce with the change stashed.
Only the CLI could install a plugin at an exact commit (`--ref <sha>`); the
Desktop "Install from Git" dialog, the TUI gateway `plugins.manage install`
method and the dashboard `/agent-plugins/install` endpoint all called
`_install_plugin_core` without a ref, so a team that wanted everyone on the
same private-plugin commit had to leave the app for a terminal.
- `dashboard_install_plugin(ref=)` threads the pin to `_install_plugin_core`;
the 40-hex validation and HEAD verification are unchanged. The gateway
method and dashboard body accept `ref`.
- Desktop dialog gains an optional "Pin to commit" field (custom sources only,
client-side 40-hex check disables Install on anything shorter).
- `plugins.manage list` rows carry `pinned_sha`; the Desktop plugins tab shows
a `pinned @ <sha8>` badge and `hermes plugins list` prints `git pinned@<sha8>`
in Source, so a team can eyeball that everyone runs the same commit.
Plugins were split across two pages that each showed half the picture:
Settings → Plugins listed desktop plugins plus "Install from Git" and a
pointer saying agent plugins live elsewhere; Capabilities → Plugins listed
agent plugins plus the catalog picker but knew nothing about desktop
plugins. A user asking "what extends my Hermes and where do I add more?"
had to visit both and still could not see the whole set in one place.
Capabilities → Plugins is now THE plugins page:
- Agent plugins section (scoped to the profile selector) with the
"Install from Git" button in its header — installs target the scoped
profile, not whichever one is active.
- Desktop plugins section beneath it (same for every profile), with the
folder/rescan controls and the "agent half missing here" drift chip,
whose repair also lands in the SCOPED profile.
- The catalog picker underneath, unchanged.
Settings → Plugins is removed. `/settings?tab=plugins[&plugin=…]` and the
existing `?tab=mcp` redirect share one table (`settings/moved-tabs.ts`) so
old bookmarks and palette links land on the same row on the new page.
Command palette: plugins moved from the Settings group to the Capabilities
group; installed-plugin rows deep-link to `/skills?tab=plugins&plugin=…`.
Dead `settings.plugins.agent.*` and `settings.nav.plugins` i18n keys dropped;
docs and in-code pointers say Capabilities → Plugins.
`hermes plugins install owner/private-repo` failed for every private repository
even when the same user could `git clone` it from their shell: the hardened
`noninteractive_git_env` disables credential helpers, askpass and global config
(so a hostile repo can't make our plumbing prompt or hang), which also blocks
the user's own stored credential. The result was "could not read Username" or a
60s hang on a GUI askpass, with no hint about how to authenticate.
New `hermes_cli/git_credentials.py` resolves a credential up front from sources
the user already owns — GITHUB_TOKEN/GH_TOKEN (profile-scoped), `gh auth token`,
then `git credential fill` against their configured helpers with prompting
disabled (any host) — and hands it to git as a one-shot
`http.<origin>/.extraheader` via the GIT_CONFIG_* env block. Nothing lands in
the URL, `.git/config` or install metadata. The same path covers
`plugins update`, catalog MCP git installs and profile-distribution staging,
which share the same hardened env and the same failure.
A private-repo clone with no credential now fails fast with an actionable hint.
Keep upstream's reviewed catalog as the only plugin name index.
Catalog pins and custom update sources share staged PM validation.
Publish code and dependencies with recovery after process death.
Reject a concurrent enablement change before publishing disabled code.
Use the manifest loader's supported version in the installer. Keep
probe cooldowns for timeouts, not TLS failures that a CA change fixes.
Preserve the backup, uninstall, browser and memory-provider repairs.
Verified with the canonical runner on native Windows ARM64, real Git
repositories, local TLS endpoints and UV dependency generations.
Desktop catalog tests and both TypeScript checks pass. The full suite
and native release builds were not run. No remote push.
- hermes_cli/plugins_cmd_catalog.py: new sibling owning resolution, the
.hermes-catalog.json provenance sidecar, search/info/validate, re-pin on
update, and the dashboard/TUI payload builders. plugins_cmd.py only
gains the hooks (cmd_install catalog branch, cmd_update / dashboard
update re-pin, dashboard_install_plugin catalog_name + kill list,
dispatch entries); the community index (plugin_index.py) is gone.
- hermes_cli/plugin_catalog.py: catalog_dir parameter replaces the
test-only HERMES_PLUGIN_CATALOG_DIR env var; live refresh reads ONE
published document (/docs/api/plugin-catalog.json, 6h cache, in-tree
fallback) instead of the unauthenticated GitHub contents API (60 req/h,
1 request per entry); in-tree and live removals are unioned so a stale
cache can never un-block.
- Catalog route lives in web_routers/dashboard_ui.py (the facade is off
limits); _plugin_runtime_status shared from web_server_dashboard.py;
hub rows carry removed_reason. TUI plugins.manage gains catalog_name
install, catalog row fields and an update action.
- plugin_validate: the probe context honours ctx.get_config defaults
(real plugins do int(ctx.get_config("timeout", 180)) in register()).
- Installed-state merge matches through the sidecar's catalog_name
first — catalog names rarely equal manifest names.
- extract-plugins.py emits plugin-catalog.json; deploy-site triggers on
plugin-catalog/** so entry merges republish it.
Python plugin CLI/loader/web/tui files taken from main wholesale; the
catalog layer is re-ported onto main's decomposed shapes in the
following commits. plugin_index.py removed (catalog is the sole
discovery system).
Merge upstream b1f003e186 while preserving PM runtime ownership and
Python 3.14 worker startup, Windows signing, and macOS wait recovery.
Keep retired runtime modules deleted. Port upstream updater preflight
checks into the checkout strategy and preserve live build logging.
Carry checkpoint filename handling and process recovery into the current
module layout. Regenerate locks and adapt incoming platform test markers.
Focused Python and JavaScript tests, desktop and root-test typechecks,
conflict-path lint checks, lock validation, and retired-import checks pass.
The full test suite and packaged release builds were not run.
Each fix verified against main @ ee84ccd on 2026-09-08.
- windows-native: the troubleshooting entry told users to set HERMES_GATEWAY_FORCE_STARTUP (no code reads it), query a task named HermesGateway (hermes_cli/gateway_windows.py names it Hermes_Gateway), and described the Startup-folder fallback as a cmd.exe shortcut (it writes a .vbs run via wscript.exe). Closes#88077.
- faq: 'hermes config set HERMES_MODEL ...' does not change the default model; 'model.default' does. Closes#65855.
- messaging/index, slash-commands, irc: gateway settings are read from ~/.hermes/config.yaml (gateway/config.py), not gateway-config.yaml. Closes#65857, closes#78276.
- telegram: reaction lifecycle is 👀 then 👍/👎 (plugins/platforms/telegram/adapter.py on_processing_complete), docs said ✅/❌. Closes#78698.
- plugins: bundled memory providers win on a name collision (plugins/memory/__init__.py docstring: bundled, user, project, entry point, first seen wins); the page said user plugins override. Model providers keep the documented last-writer-wins. Closes#100281.
- architecture, index: terminal backend count is seven (tools/terminal_tool.py: local, docker, singularity, modal, daytona, vercel_sandbox, ssh); two surfaces said six. Closes#78252.
- quickstart: the Portal quick path was called 'free'; the login is free, the inference is billed to the subscription. Wording now matches integrations/nous-portal. Closes#78254.
Finish bootstrap uv before PM replaces its store entry. Keep failure
receipts stdlib-only and align the cryptography requirement and override
with the locked version.
Let bundle builders declare launch paths and update ownership. Remove
payload discovery, Store probing, and the unused develop command.
Derive Nix Python from the PM lock and share its provenance stamp.
Document setup, activation, optional dependencies, and distribution
ownership. Targeted Windows tests, relocated runtime launches, Electron
bundling, and bilingual docs builds pass. Native Nix and signed-package
acceptance remain CI gates.
- finalize_update_receipt folds the newest pm sync receipt's
venv_rebuild / plugin_bisect / feature_list into the update receipt
(pm_-prefixed) so ONE latest.json carries the whole update story —
the embedding contract both receipt plans declared. Additive,
exception-swallowing (an embed failure never breaks the update
receipt).
- pm/receipt: _write_rotated mkdirs itself instead of depending on
_receipt_dir()'s mkdir side effect (found via the embed tests — a
patched dir lambda made writes FileNotFoundError under the OSError
swallow).
- website plugins.md: the three new verbs (check-updates, adopt,
trust-update-url) + the provenance/cadence/auto_apply/trust flow,
user-facing.
tests: embed happy/absent/failure-isolated; existing receipt suites
green.
The interim index (#84919) merged while this PR was open, leaving two
side-by-side discovery systems. Rip the index out, keep only what the
catalog track uses:
- delete hermes_cli/plugin_index.py, the bundled seed JSON, its test
file, the .gitignore exception, and the plugins.index_url surface
- cmd_search is catalog-only (term + --json); --capability/--refresh die
- cmd_install: unknown bare catalog-like names hard-error again instead
of falling through the removed index fallback
- plugin packs bare-name resolution rewired onto plugin_catalog
(load_catalog_live), the only shipped index consumer
- docs (features/plugins.md, cli-commands.md) rewritten for the catalog
Allowlist hot-path hooks for abandon-on-timeout, keep subagent_stop on the caller thread, suppress re-fires of hung callbacks, and block tools when pre_tool_call times out.