With no saved platform_toolsets, _toggle_plugin_toolset seeded
platform_toolsets.cli: [<plugin_ts>]. A list naming only plugin keys
replaces the hermes-cli composite, so the model lost every built-in tool
(26 -> 1, no terminal). The seed is dropped: an unknown plugin toolset is
already on by default when no list is saved.
plugins remove now clears the plugin's toolset from saved lists even when
plugin.yaml has no provides_tools (the toggle already no-ops without a
toolset); before, hermes config check warned about an unknown toolset.
Docs: say what provides_tools actually drives.
With the live-gateway refusal reverted (#134824), `hermes plugins uninstall`
on Windows still failed with [WinError 5] whenever the running gateway had
the plugin loaded with a file open or a native .pyd mapped: Windows will
neither delete such a file nor rename its directory. The command exited 1
and left a half-deleted plugin.
Removal now deletes what it can. When a held file stops the delete, the rest
of the tree moves out of plugins/ into <HERMES_HOME>/cache/plugin-pending-delete,
which works for mapped DLLs. When the directory itself is held and cannot move,
removal deletes its manifests so the plugin can no longer load. The install
record is dropped in both cases, and the gateway's next start sweeps what was
parked. POSIX is unchanged: a PermissionError still raises.
Live proof: tests/hermes_cli/test_plugin_uninstall_windows_live.py (a real
gateway with plain, open-handle and native-.pyd plugins) fails 2 of 3 on the
revert branch and passes 3 of 3 with this change on windows-latest.
This reverts commit 0f0b0a12aa.
#134189 refused plugin update, remove/uninstall and force-reinstall on every
platform whenever the profile gateway was live, so uninstalling a plugin
(notably on Windows, from Desktop) required shutting the gateway down. The
#70473 hazard it guarded (deferred imports in a loaded plugin hitting a
swapped checkout) does not justify blocking uninstall; it will be re-landed
scoped to the paths that need it.
Git plugin update/remove/force-reinstall ran git pull or tree swaps
directly in the installed checkout while the messaging gateway could
still be running with that plugin's callbacks loaded. A callback that
defers a relative import until first invocation then fails with
ModuleNotFoundError even though the command reported success (#70473).
One shared guard (hermes_cli.plugins_cmd._refuse_live_gateway_mutation)
now fails every mutating path BEFORE any git operation when the active
profile's gateway is live:
- plugins update (CLI) and dashboard_update_user_plugin
- plugins remove / rm / uninstall, dashboard_remove_user_plugin and the
plugins.manage RPC (shared _remove_user_plugin tail)
- plugins install --force (a forced reinstall replaces a loaded
checkout; a brand-new install is not gated)
The liveness read is the canonical profile-scoped probe
(profiles._check_gateway_running -> resolve_gateway_liveness), never
mutating the profile's identity files; a probe failure fails open so an
uninstallable plugin cannot strand. The message gives the
stop/apply/start recovery sequence; --allow-live-gateway on
update/install/remove is the explicit CLI-idiom override.
Integration test (tests/hermes_cli/test_plugin_live_gateway_guard.py)
spawns a real child holding gateway.lock with a gateway-shaped command
line against a temp HERMES_HOME, so the guard is exercised through the
platform's actual liveness probe — not a mock. Stopped-gateway behaviour
is asserted unchanged.
plugins_cmd.py had grown to 2,858 lines, past the ~2,000-line gate. Move
each verb family into a plugins_cmd_<topic>.py sibling: git (install
metadata + git plumbing), install, update (plus adopt / trust-update-url /
check-updates), remove, capabilities, toggle (composite UI) and listing.
The facade keeps the shared primitives, enable/disable selection,
discovery and the dispatch table, and re-exports the names other modules,
tests and the old-updater surface import (978 lines now).
Siblings never import the facade at module level; they read facade names
through _pc() at call time, so monkeypatching plugins_cmd.<name> still
intercepts calls made from a sibling. No behaviour change. Tests that
imported three sibling-only helpers now import them from the defining
module, and two subprocess.run patches target subprocess directly.