Skip to content

fix(plugins): isolate profile runtime state - #75648

Open
xaviersudre wants to merge 355 commits into
NousResearch:mainfrom
xaviersudre:fix/73230-profile-scoped-plugin-isolation
Open

fix(plugins): isolate profile runtime state#75648
xaviersudre wants to merge 355 commits into
NousResearch:mainfrom
xaviersudre:fix/73230-profile-scoped-plugin-isolation

Conversation

@xaviersudre

@xaviersudre xaviersudre commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix profile plugin isolation in long-lived shared Hermes runtimes.

Desktop/WebUI sessions can select different profiles inside one backend process. Previously, plugin discovery and registration mutated process-global managers, provider registries, tool handlers, schemas, and caches. Loading one profile could therefore overwrite or expose another profile's capabilities, while runtime profile/home/secret bindings could outlive the turn that established them.

This change:

  • scopes plugin managers, module namespaces, provider registries, tool handlers, schemas, checks, and generation caches by profile;
  • preserves immutable handler/schema/check snapshots for already-live sessions during forced rediscovery;
  • makes same-size/same-mtime plugin source reload deterministic;
  • atomically rolls back all 18 PluginContext publication surfaces when registration fails;
  • preserves concurrent later writers using generation-aware rollback;
  • keeps tool-override authorization plugin-specific in both legacy and profile-qualified module namespaces;
  • binds selected-profile configuration and HERMES_HOME correctly and restores runtime profile/home/secret state after success or exception;
  • keeps launch-global/deferred capability exposure fail closed.

Fixes #73230.

Security properties tested

  • Both profile load orders and same-name plugin/tool collisions
  • Profile-specific tool visibility, schemas, handlers, checks, and policies
  • Provider configuration/home isolation across profiles
  • Runtime secret/profile/home cleanup on normal and exceptional exits
  • Immutable old-session snapshots across force rediscovery
  • Atomic failed-plugin rollback across all 18 registration surfaces
  • Generation-safe concurrent provider/tool replacement
  • Plugin-specific override and deregistration authorization in legacy and profile-qualified namespaces

Test plan

Independent QA reviewed exact commit 9db4314ae0ef1e56c9f746113026c113f5b8a551, one commit over 4b60979dc188655eb4fb81abf292890147ec2d4c.

  • Focused FR Add logging for first 100 chars of the tool call args json / tool response #9 suite: 62 passed
  • Security/runtime QA broad lane: 2,654 passed; 8 optional-dependency failures reproduced identically on the exact base
  • Compatibility/gateway/schema QA broad lane: 717 passed
  • TUI/lazy-session lane: 503 passed
  • Gateway/profile/platform lane: 135 passed
  • Kanban/project RPC lane: 56 passed
  • Exact-blob override/deregistration/load-order canaries: passed
  • Six-provider failed-registration rollback canary: passed
  • py_compile for all 17 changed Python paths: passed
  • git diff --check, exact parent/tree/path accounting, and clean-worktree checks: passed

Optional dependencies were not installed during QA; lazy installation remained disabled.

Scope

One non-merge commit, 17 files, 3,366 insertions and 359 deletions. No deployment, service/configuration change, or local activation is included.

Current-upstream convergence for PR #75648

  • Verified replacement head: 3d5acc378d1b8d13f7cfb421f5edb11008c18bfd
  • Exact official upstream base: 226e27035b73e940151e757daab26c5c53ea45ad
  • Manifest scope: agent/browser_registry.py, agent/image_gen_registry.py, agent/plugin_profile_scope.py, agent/transcription_registry.py, agent/tts_registry.py, agent/video_gen_registry.py, agent/web_search_registry.py, hermes_cli/plugins.py, model_tools.py, tests/agent/test_plugin_provider_registry_profile_scope.py, tests/hermes_cli/test_dashboard_auth_plugin_hook.py, tests/hermes_cli/test_plugin_profile_scope.py, tests/test_model_tools_profile_scope.py, tests/tools/test_registry_profile_scope.py, tests/tui_gateway/test_profile_plugin_isolation.py, tools/registry.py, tui_gateway/server.py
  • Stable patch identity: 1e45aa00859923434de44114ac20ca41a488f191
  • Independent QA receipt: t_3dc37a0d

The replacement head is a non-force fast-forward successor of the prior fork branch. The adapter verified the exact manifest file scope against GitHub before mutation and read back the fork ref and PR body after mutation. No feature scope outside the approved carry manifest was added.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for addressing the shared-runtime profile boundary. The reported premise is present in the inspected checkout: hermes_cli/plugins.py:1270-1292 keeps plugin publications in one manager, hermes_cli/plugins.py:1305-1306 makes its discovery cache idempotent, and hermes_cli/plugins.py:2048-2056 exposes that manager as a process-global singleton despite scanning get_hermes_home() at hermes_cli/plugins.py:1368-1373.

The PR addresses the required boundary by attaching a profile-specific manager to each TUI session and binding it during construction and turns (tui_gateway/server.py:1251-1260, tui_gateway/server.py:1973-1975, PR head 9db4314ae0ef1e56c9f746113026c113f5b8a551). I found no verified blocking defect in the reviewed diff.

Automated hermes-sweeper review.

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/plugins Plugin system and bundled plugins area/profiles Multi-profile isolation, HERMES_HOME scoping P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 31, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #63702 and #27336: this current-head patch uses broader profile-scoped registry snapshots and runtime bindings, rather than only a home-keyed plugin-manager cache. Please choose or consolidate the overlapping profile-isolation mechanisms.

@xaviersudre

Copy link
Copy Markdown
Contributor Author

Thanks for flagging these. #63702 overlaps the plugin-manager portion of this change; #75648 is intended as the consolidation target because the keyed manager cache alone does not isolate the other process-global publication, registry, schema, handler, and runtime-binding surfaces exercised by the shared TUI/WebUI process.

#27336 appears complementary rather than duplicative: it updates agent logging-home selection in agent/agent_init.py, while #75648 does not modify that logging path. My proposed consolidation is therefore to retain #75648 for plugin/runtime isolation, treat #63702 as the narrower overlapping implementation, and keep #27336 separate.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 31, 2026
@alt-glitch alt-glitch removed sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) labels Jul 31, 2026
Baophan00 and others added 10 commits July 31, 2026 22:36
…-deletion (NousResearch#75403)

guess_category() classified any file whose name starts with 'test_' or
'tmp_' as disposable, even when the file lived under user-authored
directories like patches/, projects/, skins/, or themes/. Files in
these trees were silently deleted on session end.

Added the missing user-project directories to the exclusion list so
that basename-based classification only applies to files in temporary
or scratch locations, not durable project trees.
…s from sweep

Address review feedback from teknium1:

1. Re-validate stale 'test' category entries in quick() — existing
   tracked.json entries under now-protected directories (patches/,
   projects/, etc.) are re-classified via guess_category() and
   dropped instead of deleted, mirroring the cron-output pattern.

2. Add patches, projects, skins, themes, contributors to
   _EMPTY_DIR_PROTECTED_TOP_LEVEL so the empty-directory sweep
   never traverses into these user-authored project trees.
…meout

524ab53 widened the media read timeout from send_video to "all upload send
paths" - send_voice/send_audio/send_photo/send_document/send_media_group/
send_animation. Both send_photo calls inside send_image() were missed, so
they still ran on the short timeout the rest of the Bot API is tuned for
while the sibling media paths already pass it.

The missed pair is the worst one to miss: send_image tries a URL send first,
then falls back to downloading the image and uploading the bytes - the path
documented as "supports up to 10MB", i.e. the slowest send in the file and
the one whose server-side processing wait most often outlasts the short
budget. When it times out the handler's last resort posts the bare URL as
text, so the picture silently never arrives as a picture.

Pass _MEDIA_SEND_READ_TIMEOUT on both, covered by two behavioral tests that
drive send_image for real - the URL send and the forced byte-upload fallback
- and assert the read_timeout that actually reaches the Bot API.
…lay name

7b5a188 migrated the sibling slug sites to custom_provider_slug, which
keeps a keyed providers: entry's config key as its durable identity. It
covered find_custom_provider_identity_by_model; canonical_custom_identity's
third recovery source - the configured-provider fallback - still built
f"custom:{normalized}" out of whatever string the caller happened to hold.

_get_named_custom_provider matches on either spelling, so a display name
that differs from its config key matches the entry and then heals to
custom:<display-name>. That is a second identity for one endpoint: the
endpoint- and model-based sources of the same function return
custom:<config-key>, and so does everything that persists or restores a
session's provider override. canonical_custom_identity exists precisely to
make a bare "custom" routable again, and tui_gateway calls it on the
session-persist, resume and recovery paths - so the divergence lands in
stored session identity.

Re-resolve through the endpoint the matched entry owns, reusing the
function's own URL-based canonicaliser rather than duplicating the match
logic. Legacy unkeyed custom_providers: entries keep their name identity,
and an unconfigured candidate still returns None.
…toast

Toast when a send fails because the disk is full
_is_env_config_key() already routes _API_KEY and _TOKEN suffixed
keys to .env for safe credential storage. Add _SECRET to the suffix
list so keys like CLIENT_SECRET and ENCRYPTION_SECRET are stored
in .env (excluded from git by default) rather than config.yaml.
…th-cancel-race-ia01

fix(web_server): stop Codex OAuth worker from finishing after cancel
setup_path() only wrote a 'hermes' launcher into the command-link dir,
even though pyproject.toml declares three [project.scripts]:
hermes, hermes-agent (run_agent:main), hermes-acp (acp_adapter.entry:main).

Fresh venv installs (the common case on macOS/Linux) leave
~/.local/bin/{hermes-agent,hermes-acp} empty, so external tools
expecting 'hermes-acp' as a standalone command (documented as
first-tier supported in website/docs/user-guide/features/acp.md)
fail to find it after a fully successful install.

Loop over the three console-script names, writing a shim per entry
that exec's the venv interpreter with the right checked-in
entrypoint. --no-venv keeps the old single-shim behaviour since
it does not manage the venv and only 'hermes' is guaranteed on PATH.

Fixes NousResearch#74819
@xaviersudre

Copy link
Copy Markdown
Contributor Author

Converged PR #75648 to the independently verified current-upstream carry.

  • Why: keep the reviewed carry merge-reviewable while preserving its approved behavior and file envelope.
  • Verified fork head: 3d5acc378d1b8d13f7cfb421f5edb11008c18bfd
  • Exact upstream reconciliation base: 226e27035b73e940151e757daab26c5c53ea45ad
  • Allowed file scope: agent/browser_registry.py, agent/image_gen_registry.py, agent/plugin_profile_scope.py, agent/transcription_registry.py, agent/tts_registry.py, agent/video_gen_registry.py, agent/web_search_registry.py, hermes_cli/plugins.py, model_tools.py, tests/agent/test_plugin_provider_registry_profile_scope.py, tests/hermes_cli/test_dashboard_auth_plugin_hook.py, tests/hermes_cli/test_plugin_profile_scope.py, tests/test_model_tools_profile_scope.py, tests/tools/test_registry_profile_scope.py, tests/tui_gateway/test_profile_plugin_isolation.py, tools/registry.py, tui_gateway/server.py
  • Stable patch identity: 1e45aa00859923434de44114ac20ca41a488f191
  • Immutable independent QA receipt: t_3dc37a0d

The branch update used GitHub's non-force ref update only. The PR body was refreshed, this explanation was posted, any configured review thread received a direct reply, and the branch, body, comment, and thread reply were read back from GitHub before the verified-head receipt was sealed.

@alt-glitch alt-glitch added comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/desktop Electron desktop app (apps/desktop/*) area/nix Nix flake, NixOS module, container packaging labels Aug 2, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #63702 overlaps the plugin-manager portion, while #27336 is complementary. The current 520-file live diff needs a focused rebase/rescope before these approaches can be compared for consolidation.

@alt-glitch alt-glitch added comp/acp Agent Communication Protocol adapter comp/gateway Gateway runner, session dispatch, delivery tool/browser Browser automation (CDP, Playwright) tool/tts Text-to-speech and transcription tool/vision Vision analysis and image generation tool/web Web search and extraction backend/docker Docker container execution area/docker Docker image, Compose, packaging platform/windows Native Windows-specific behavior or breakage sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data and removed comp/gateway Gateway runner, session dispatch, delivery comp/acp Agent Communication Protocol adapter backend/docker Docker container execution area/docker Docker image, Compose, packaging area/nix Nix flake, NixOS module, container packaging sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows comp/desktop Electron desktop app (apps/desktop/*) comp/dashboard Web dashboard / control panel UI (dashboard/, landing) platform/windows Native Windows-specific behavior or breakage labels Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/browser Browser automation (CDP, Playwright) tool/tts Text-to-speech and transcription tool/vision Vision analysis and image generation tool/web Web search and extraction type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop sessions do not activate plugins for the selected profile