Skip to content

feat(marketplace): auto-load user-installed plugins from ~/.openhands (parity with load_user_skills)#3877

Draft
jpshackelford wants to merge 1 commit into
mainfrom
feat/marketplace-load-user-plugins
Draft

feat(marketplace): auto-load user-installed plugins from ~/.openhands (parity with load_user_skills)#3877
jpshackelford wants to merge 1 commit into
mainfrom
feat/marketplace-load-user-plugins

Conversation

@jpshackelford

@jpshackelford jpshackelford commented Jun 25, 2026

Copy link
Copy Markdown
Member

Why

The marketplace-registration stack (#3824#3831) gave plugins a great deal of new loading machinery (registry, registered-marketplace auto-load, runtime load_plugin, agent-server APIs). But it left one asymmetry with skills: there is no startup path that auto-loads user-installed plugins from ~/.openhands/plugins/installed/ the way AgentContext.load_user_skills does for skills.

Today:

Capability Skills Plugins (before this PR)
install_*~/.openhands/.../installed/
list/load_installed_* API
Auto-loaded at conversation startup from home ✅ (load_user_skills) missing
Auto-load from registered marketplaces ✅ (this stack)

LocalConversation._ensure_plugins_loaded() only loaded plugins from (1) registered_marketplaces with auto_load=True and (2) explicit plugins=[...] specs — it never read the installed-plugins home dir. The only callers of load_installed_plugins() were an example. This is the gap where Claude Code supports "drop a plugin in your home dir and it's picked up," and we did not.

What changed

  • AgentContext.load_user_plugins: bool (default False) — opt-in, mirroring load_user_skills. Like load_project_skills, it is resolved lazily by LocalConversation (the workspace/runtime context isn't known at AgentContext validation time).
  • LocalConversation._ensure_plugins_loaded() — after marketplace/explicit plugin loading, if load_user_plugins is set, merge enabled installed plugins via load_installed_plugins(). Installed plugins are already on disk and version-pinned by install_plugin, so they are loaded directly (not fetched) and are not added to _resolved_plugins (which tracks remote sources for deterministic resume). Loading is best-effort so a single broken installed plugin can't block conversation startup (same guard pattern as load_user_skills / project-skills).
  • Refactored the per-plugin merge (skills / MCP / hooks / agents) into a local _merge_loaded_plugin helper reused by both the fetched-plugin loop and the new installed-plugin path — no behavior change for existing plugin loading.

Precedence / semantics

Installed plugins are merged after marketplace/explicit plugins using the same add_skills_to / add_mcp_config_to / hook / agent merge as fetched plugins, so behavior is consistent with the existing plugin path. Default is False, so this is fully opt-in and non-breaking.

Tests

  • tests/sdk/conversation/test_local_conversation_plugins.py::TestLocalConversationUserPlugins
    • test_load_user_plugins_loads_installed_plugins — installs a plugin into a temp installed dir and asserts its skill is merged into the agent context; confirms it is not tracked in resolved_plugins.
    • test_load_user_plugins_disabled_by_default — with the default flag the installed-plugins loader is not even queried.
    • test_load_user_plugins_best_effort_on_loader_error — a failing loader does not block startup.
  • tests/sdk/context/test_agent_context.py::test_load_user_plugins_defaults_false_and_is_settable — default/serialization round-trip.
uv run pytest tests/sdk/conversation/test_local_conversation_plugins.py tests/sdk/context/test_agent_context.py -q
# broader: tests/sdk/context tests/sdk/marketplace tests/sdk/plugin tests/sdk/conversation all green (1247 passed)
uv run pre-commit run --files \
  openhands-sdk/openhands/sdk/context/agent_context.py \
  openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py \
  tests/sdk/conversation/test_local_conversation_plugins.py \
  tests/sdk/context/test_agent_context.py

Notes / scope

This PR was created by an AI agent (OpenHands) on behalf of @jpshackelford.

@jpshackelford can click here to continue refining the PR


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:986e075-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-986e075-python \
  ghcr.io/openhands/agent-server:986e075-python

All tags pushed for this build

ghcr.io/openhands/agent-server:986e075-golang-amd64
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-golang-amd64
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-golang-amd64
ghcr.io/openhands/agent-server:986e075-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:986e075-golang-arm64
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-golang-arm64
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-golang-arm64
ghcr.io/openhands/agent-server:986e075-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:986e075-java-amd64
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-java-amd64
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-java-amd64
ghcr.io/openhands/agent-server:986e075-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:986e075-java-arm64
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-java-arm64
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-java-arm64
ghcr.io/openhands/agent-server:986e075-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:986e075-python-amd64
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-python-amd64
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-python-amd64
ghcr.io/openhands/agent-server:986e075-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:986e075-python-arm64
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-python-arm64
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-python-arm64
ghcr.io/openhands/agent-server:986e075-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:986e075-golang
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-golang
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-golang
ghcr.io/openhands/agent-server:986e075-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:986e075-java
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-java
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-java
ghcr.io/openhands/agent-server:986e075-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:986e075-python
ghcr.io/openhands/agent-server:986e0750ad165a582a7b313650d47eb7b68d34b4-python
ghcr.io/openhands/agent-server:feat-marketplace-load-user-plugins-python
ghcr.io/openhands/agent-server:986e075-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 986e075-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 986e075-python-amd64) are also available if needed

Add `AgentContext.load_user_plugins` (default False) so local conversations
auto-load enabled plugins from `~/.openhands/plugins/installed/` at startup,
mirroring `load_user_skills` for plugins and closing the SDK-vs-Claude-Code
gap where installed plugins had install/list/load APIs but no startup
auto-load path.

- AgentContext.load_user_plugins: opt-in flag, resolved lazily by
  LocalConversation (like load_project_skills) since it needs runtime context.
- LocalConversation._ensure_plugins_loaded: after marketplace/explicit plugin
  loading, merge enabled installed plugins via load_installed_plugins().
  Installed plugins are already on disk and version-pinned, so they are loaded
  directly (not fetched) and not added to _resolved_plugins. Loading is
  best-effort so a bad installed plugin cannot block conversation startup.
- Refactor the per-plugin merge into a local _merge_loaded_plugin helper reused
  by both the fetched-plugin loop and the installed-plugin path.
- Tests: enabled load, default-off (loader not even queried), and
  best-effort-on-error; plus an AgentContext field default/serialization test.

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants