Review follow-up on the host-rendezvous isolation:
* Excluding Desktop-owned children from ROLE_SERVE broke #119644's terminal
path: `hermes plugins install` found "the running Desktop backend" only via
read_record(ROLE_SERVE), so on a Desktop-only box open chats no longer lit
up. A Desktop child now publishes ROLE_DESKTOP_SERVE (own lock, record and
0600 token). The attach/refuse ladder (_host_backend_attachment) still reads
ROLE_SERVE only, so a supervised public dashboard is never blocked by it;
notify_serve_backend prefers the host owner and falls back to the Desktop
record. /api/host/identity reports the role actually published.
* is_desktop_owned_backend() missed Desktop's SSH spawn — `env HERMES_DESKTOP=1
hermes serve --isolated --ssh-session-token-file F` carries NO token env var
(remote-lifecycle tests assert the var name never appears) — so the SSH child
still claimed ROLE_SERVE on the remote host and its MCP discovery flipped to
deferred. The predicate now accepts the token-FILE argv shape; the argv half
lives in _startup_fast.is_desktop_ssh_backend_argv (stdlib-only, importable
before main.py's import wall) and replaces the two duplicate substring checks
in main.py and dashboard_procs.py.
* web_server.py's remaining three bare HERMES_DESKTOP reads (cron ticker,
managed-gateway teardown, orphan serve reap) route through the predicate;
the tests that modelled a Desktop child with the bare flag set the spawn
token, as a real pool child does.
The onboarding install card installs its rows concurrently, and each install
runs load_and_go_live: a forced plugin rediscovery, then a read of the plugin's
MCP server configs and skills, then an MCP connect. A forced rediscovery
unloads every plugin first (PluginManager.unload clears _portable_mcp_servers,
_plugin_skills and each server's liveness declaration) and only then loads them
again. When NVIDIA App and NVIDIA Broadcast finished installing together, the
Broadcast pass unloaded the manager while the NVIDIA App go-live was reading
it: NVIDIA App went live with no MCP servers and no skill (the card row said
"Installed" with no tool count, and the build chat had no NVIDIA App tools
until the app restarted).
load_and_go_live now runs one at a time in the process (_GO_LIVE_LOCK), so a
second install's rediscovery cannot run while the first reads or connects.
The connect is inside that lock because it reads the server's liveness
declaration (tools/mcp_tool_transport.py::_live_endpoint), which a forced pass
also clears. The reads share the manager's discovery lock with the pass that
produced them, for forced passes that do not go through load_and_go_live, and
connect_plugin_mcp takes the server configs read there instead of reading the
manager again. The MCP connect stays outside the discovery lock, so chats that
call discover_plugins() are not held for the length of a connect.
Installing a plugin from any surface now makes its MCP servers and skills
usable in every open chat of that profile on the chat's next turn. There is
no Connect-now button, no /reload-mcp, no relaunch.
- hermes_cli/plugins_activation.py::load_and_go_live: after the forced plugin
rescan, connect the plugin's portable MCP servers one by one
(register_mcp_servers, the connector flow's call), then refresh that
profile's open chats and queue a turn note listing the servers, their
tools and the plugin's skills. activation gains live_now; deferred keeps
only Python tools and prompt sections.
- MCP tools are deferred behind tool_search / tool_call, so appending them
(preserve_prefix) leaves the model-facing tool array and the cached prompt
prefix unchanged; the note rides the existing one-shot turn-note channel,
so the system prompt stays byte-stable. /reload-mcp keeps its consent gate:
it is a full rebuild.
- tui_gateway/methods_tools.py: one session walk (_refresh_live_sessions)
shared by reload.mcp and plugin activation, filtered to the plugin's
profile home.
- hermes plugins install/enable (another process) asks the running Desktop /
dashboard backend to do the in-process half through the new
POST /api/dashboard/agent-plugins/activate, found via the host rendezvous
record and its session token.
- Desktop: the Connect-now toast and its strings are removed in all six
locales; the install toast says what went live ("3 tools connected ·
skill X ready") and warns per server that did not connect.
- Contract: PluginActivation.live_now; regenerated TS/OpenRPC.
- register_skill docstring: plugin skills are listed by skills_list.
A plugin that finished loading after an adapter connected never got its platform
handlers (slash commands, button callbacks, inbound transforms) registered until a
gateway restart, silently. Three pieces, one seam shared by every surface:
1. Discovery listener: PluginManager.on_plugin_loaded(cb) fires from INSIDE
discover_and_load for the plugins a sweep newly loaded (diff of the loaded set),
with a per-plugin activation summary (hermes_cli/plugins_activation.py):
activated_now {gateway_commands, gateway_transforms, hooks, callbacks} vs
deferred {tools, prompt, mcp_servers}. Every mid-run load path now performs a real
discover_plugins(force=True): CLI install/enable (via the gateway), Desktop/TUI
plugins.manage install/toggle/update, dashboard REST install, tool-triggered
force re-discovery, the new `reload-plugins` control-socket verb. A non-forced
discover_plugins() short-circuits on _discovered, which is why reload.mcp after
a mid-run install used to reload the OLD server set.
2. Idempotent re-wire: BasePlatformAdapter.rewire_plugin_handlers() runs only
factories not yet wired on the live native client (keyed (plugin, qualname);
a force reload hands back new function objects). Telegram hoists late handlers
ahead of core's catch-all filters.COMMAND / CallbackQueryHandler (PTB dispatches
the first match per group) and re-wires on the transient-init rebuild; Slack
dedupes register_slack_action_handler per AsyncApp. The gateway runner
subscribes per served profile and re-wires on the loop.
3. Scope limit + honest messaging: handlers only. Tools/prompt stay deferred to
the next session (prompt-cache invariant), MCP servers to mcp.reload; the CLI
hint and plugins.manage results (activation, gateway_reloaded,
restart_required only when no gateway answered) say exactly that.