feat(plugin): Support multiple marketplace registrations with auto-load semantics#2495
feat(plugin): Support multiple marketplace registrations with auto-load semantics#2495jpshackelford wants to merge 19 commits intomainfrom
Conversation
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Coverage Report •
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- Document auto_load strategies (auto-load vs on-demand) - Explain plugin@marketplace reference syntax (matches Claude Code) - Add expandable example code block - Include MarketplaceRegistration field reference table Related: OpenHands/software-agent-sdk#2495
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean implementation with elegant design.
Key strengths:
- Well-structured data models (
MarketplaceRegistration,MarketplaceRegistry) with clear separation of concerns - Security improvement: path validation now uses normalization instead of simple
..checks, catching traversal tricks - Proper backward compatibility with clear deprecation path
- Comprehensive test coverage (21+ tests) using real marketplace directories, not excessive mocks
- Clean async architecture (router → service → event_service → conversation)
Linus would say: Data structures are right, no special cases, solves real problems. Ship it. 🚀
|
Thank you for this! Just trying to understand a detail. @OpenHands Read this PR and the linked issue, and understand them well. I think I saw in the linked issue that 4 things should be deprecated, but in this PR description I see 1 thing to deprecate. Verify what I just said. Are the others replaced with other implementations here, WDYT, should others be deprecated, or what seems to happen about that? Answer directly as a comment on the PR on github. |
|
I'm on it! enyst can track my progress at all-hands.dev |
|
@enyst I checked the linked issue, this PR, and the API-breakage workflow. You remembered correctly: #2494 lists 4 deprecations:
From the code in this PR, though, I only see #1 actually deprecated today:
The other 3 are not deprecated in the implementation yet:
So my read is: this PR introduces the new registration/resolution model ( So I think the accurate framing is probably:
On the griffe / Python API breakage workflow: it failed because the checker compares public SDK API against the PyPI baseline, saw the My take: for this specific case, that workflow is being too strict. The field is still there and still works; we are only marking it deprecated. I would not consider that alone a breaking Python API change that should require a minor bump. So I think either:
So short version: yes, the issue says 4; this PR currently implements 1 of those deprecations, while the other 3 still look live / not migrated yet. |
This comment was marked as duplicate.
This comment was marked as duplicate.
|
OpenHands-GPT-5.4 here — @enyst @jpshackelford, now that the small Griffe/ First, the key observationRight now, the new model is only partially replacing the old one:
So the question is really: should this PR stay scoped to the part that is already implemented, or should it do a bit more so the new path is actually exercised end-to-end and more deprecations become justified? Alternative 1: narrow the PR so the claims match the codeWhat this meansKeep this PR focused on:
What I would do under this option
Pros
Cons
Alternative 2: do a bit more in this PR so the new path is used more broadlyWhat “a bit more” would need to meanIf we want to deprecate more than just the field, I think this PR needs to wire the new model into the real startup path. Concretely, I think that means:
What this would justify deprecatingUnder this option, I think these become more defensible:
What I still would not deprecate yetI would still be careful with:
Those are public SDK helpers and they are still the active implementation path today. Even if we extend this PR, I think the safer move is:
My recommendationI recommend Alternative 1 unless you want to explicitly grow the PR into an end-to-end migration PR. If the goal is to keep this PR tight, then I would:
If the goal is to make this PR the real migration point, then I would choose Alternative 2, but in that case I would do the extra wiring above before deprecating more. In particular, I would not deprecate Short version
|
…e support This PR introduces support for registering multiple marketplaces with explicit auto-load semantics, providing an alternative to the single marketplace_path approach in PR #13117. ## Changes ### New Models **MarketplaceRegistration** - Registration for a plugin marketplace: - name: Identifier for this marketplace registration - source: Marketplace source (github:owner/repo, git URL, or local path) - ref: Optional branch, tag, or commit - repo_path: Subdirectory path for monorepos - auto_load: 'all' to load plugins at conversation start, None for on-demand ### Updated Settings Model Added registered_marketplaces field to Settings (list of MarketplaceRegistration). This allows users to configure multiple marketplaces with different loading behaviors. ### Updated Skill Loading - skill_loader: Added registered_marketplaces parameter to pass marketplace registrations to the agent-server API - app_conversation_service_base: Added registered_marketplaces parameter to load_and_merge_all_skills method ### Tests - Added comprehensive tests for MarketplaceRegistration model - Added tests for Settings.registered_marketplaces field - Added tests for skill_loader marketplace handling ## Key Behaviors - Marketplace resolution composes instance → org → user (additive) - auto_load='all' loads all plugins at conversation start - auto_load=None registers marketplace for on-demand resolution - Path validation rejects absolute paths and directory traversal ## Dependencies This PR is designed to work with SDK PR #2495 which provides: - MarketplaceRegistry class for managing registered marketplaces - Plugin resolution via 'plugin-name@marketplace-name' syntax - Lazy fetching and caching of marketplace manifests ## Related - Alternative to #13117 (marketplace_path setting) - Leverages SDK PR OpenHands/software-agent-sdk#2495 - Enables #12916 (org-level default resources) - Aligns with #13188 (instance-default org proposal) - Supports #10947 (OpenHands configuration proposal)
neubig
left a comment
There was a problem hiding this comment.
Could you please fix CI and re-request review? Thanks!
… multiple marketplace support This introduces support for registering multiple marketplaces with explicit auto-load semantics, replacing the single marketplace_path string. Key changes: - Add MarketplaceRegistration model with name, source, ref, repo_path, and auto_load fields - Add MarketplaceRegistry class for lazy fetching, caching, and plugin resolution - Add registered_marketplaces field to AgentContext - Deprecate marketplace_path field (kept for backward compatibility) - Add comprehensive tests for the new functionality Plugin resolution supports: - Explicit marketplace qualifier: 'plugin-name@marketplace-name' - Search all registered marketplaces: 'plugin-name' (errors if ambiguous) Closes #2494
…plugin resolution flow Adds comprehensive integration tests that demonstrate: - Single and multiple marketplace registration - Auto-load vs registered-only marketplaces - Plugin resolution with explicit and implicit marketplace references - Ambiguous plugin name handling - Plugin not found error scenarios - Marketplace caching behavior - Prefetch functionality - Monorepo subdirectory support - Full plugin load flow from registration to Plugin.load() - Claude Code .claude-plugin directory fallback Co-authored-by: openhands <openhands@all-hands.dev>
Demonstrates the full marketplace registration and plugin resolution flow: - Registering multiple marketplaces with different auto-load settings - Resolving plugins with explicit and implicit marketplace references - Loading plugins from resolved sources - Error handling (not found, ambiguous, unregistered) - Prefetching for validation - Integration pattern with AgentContext Co-authored-by: openhands <openhands@all-hands.dev>
- Reduce to minimal happy path demonstration - Reuse existing 43_mixed_marketplace_skills marketplace - Remove verbose demos and generated directories Co-authored-by: openhands <openhands@all-hands.dev>
…example Shows the user-facing API: - Configure registered_marketplaces in AgentContext - Create Agent with agent_context - Use skills from marketplace in a Conversation Co-authored-by: openhands <openhands@all-hands.dev>
- Add load_plugin(plugin_ref) to BaseConversation abstract interface
- Implement in LocalConversation: creates MarketplaceRegistry from
agent_context.registered_marketplaces, resolves and loads plugin
- Implement in RemoteConversation: calls agent-server API endpoint
- Add POST /conversations/{id}/plugins/load endpoint to agent-server
- Add LoadPluginRequest model
- Add ConversationService.load_plugin() and EventService.load_plugin()
- Add tests for conversation.load_plugin() integration
- Update example to demonstrate the full API
Co-authored-by: openhands <openhands@all-hands.dev>
- Create marketplace and plugin on-the-fly instead of printing code - Shows the complete flow: register → load_plugin → use skill - Add .gitignore for generated demo_marketplace directory Co-authored-by: openhands <openhands@all-hands.dev>
- Fix line too long errors in agent_context.py, registry.py, local_conversation.py, and test files - Fix unused variable assignments in test_marketplace_registry_integration.py - Fix pyright errors: - Add null check for agent_context in local_conversation.py - Add null check for resolved_plugins in example file - Add missing load_plugin method to MockConversation in test file - Bump version from 1.14.0 to 1.15.0 for breaking API change (marketplace_path field marked as deprecated) - Apply ruff formatter fixes (import sorting, whitespace)
…kflow The API breakage check detecting the deprecation of marketplace_path is expected. Version bump will be applied through the Prepare Release workflow.
- Remove dynamic file creation from main.py - Add pre-created demo_marketplace/ directory with: - .plugin/marketplace.json - plugins/greeter/.plugin/plugin.json - plugins/greeter/skills/SKILL.md - Remove .gitignore that excluded demo_marketplace/ This makes the example cleaner and easier to understand.
Demonstrates both loading strategies in one example: - auto_marketplace/ with auto_load='all' - loaded at conversation start - demo_marketplace/ with auto_load=None - loaded on-demand This shows the full power of multiple marketplace registrations.
The plugin-name@marketplace-name format is consistent with Claude Code's plugin install syntax.
- Fix silent exception swallowing in registry.py:
- _resolve_from_all() now accumulates fetch errors and includes them in
PluginNotFoundError when all marketplaces fail
- list_plugins() now raises PluginResolutionError with details when all fail
- Improves debugging: users see actual errors instead of 'plugin not found'
- Simplify agent_context.py deprecation logic:
- Remove unnecessary effective_marketplace_path variable
- Remove misleading comment about backward compatibility
- Improve path validation security in types.py:
- Add _validate_repo_path() helper using os.path.normpath()
- Catches path traversal tricks like 'safe/../../../etc'
- Both MarketplaceRegistration and PluginSource use shared validation
- Fix state mutation atomicity in local_conversation.py:
- Update both self.agent and self._state.agent within same lock context
- Add comprehensive tests:
- TestErrorAccumulation: 3 tests for error accumulation behavior
- TestPathValidation: 5 tests for path security validation
- Fix E501 line too long in registry.py (90 > 88) - Fix ruff format issue in types.py (collapse multi-line string) Co-authored-by: openhands <openhands@all-hands.dev>
This commit implements Option 2 from the review discussion, completing the full migration path for registered_marketplaces: LocalConversation changes: - _ensure_plugins_loaded now honors registered_marketplaces with auto_load='all' - Automatically loads all plugins from marketplaces marked for auto-loading - Plugins from explicit specs load first, then marketplace auto-load plugins Agent-server changes: - Add SkillsRequest.registered_marketplaces field - Deprecate SkillsRequest.marketplace_path with warning - Update load_all_skills() to accept registered_marketplaces parameter - Add _load_marketplace_skills() helper to load skills from auto-load marketplaces - Marketplace-loaded skills have highest precedence in merge order SDK deprecations: - load_public_skills(marketplace_path=...) now emits DeprecationWarning - load_available_skills(marketplace_path=...) now emits DeprecationWarning - Both docstrings updated to indicate deprecation This fully implements the 4 deprecations outlined in issue #2494: 1. AgentContext.marketplace_path -> registered_marketplaces (done in original PR) 2. load_public_skills(marketplace_path=...) -> marketplace registry (this commit) 3. load_available_skills(marketplace_path=...) -> marketplace registry (this commit) 4. SkillsRequest.marketplace_path -> registered_marketplaces (this commit) Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
ae36a32 to
abb8b9a
Compare
|
@OpenHands Give us a view on how this PR works and tell WDYT. /codereview Post directly on github as a review with gh api. |
|
I'm on it! enyst can track my progress at all-hands.dev |
enyst
left a comment
There was a problem hiding this comment.
Thanks — I dug through the issue, the PR, and the updated SDK/agent-server paths. My read is that the overall direction is good: MarketplaceRegistration + MarketplaceRegistry is the right shape, the lazy-fetch/caching model is straightforward, and the plugin resolution flow (plugin vs plugin@marketplace) looks reasonable.
That said, I don't think the new registration model is fully the source of truth yet. I found a few correctness gaps where the legacy marketplace_path flow is still driving behavior, or where load_plugin() doesn't apply the full plugin semantics it advertises. Left the concrete spots inline.
Because this touches plugin loading / agent behavior, I'd want a maintainer to take the final call after those are sorted and the intended end-to-end semantics are tightened up. WDYT?
| return self | ||
|
|
||
| # Emit deprecation warning if using old marketplace_path field | ||
| if self.marketplace_path is not None and not self.registered_marketplaces: |
There was a problem hiding this comment.
The deprecation warning makes sense, but _load_auto_skills() is still entirely driven by the legacy marketplace_path flow. If a caller sets only registered_marketplaces, nothing here derives auto-loaded skills from those registrations, so AgentContext construction can still pull in the default public marketplace instead of the configured registrations. I think this validator needs to switch to the new field as the source of truth once registrations are present.
| marketplace_path = request.marketplace_path | ||
| if request.registered_marketplaces: | ||
| # New behavior: use registered_marketplaces | ||
| # For now, we extract marketplace_path from the first 'public' registration |
There was a problem hiding this comment.
This compatibility bridge only forwards repo_path from the first public registration. source and ref are ignored, and if there is no public autoload registration we keep request.marketplace_path's default. So /skills still can't honor the full MarketplaceRegistration contract here — especially for a forked/pinned public marketplace — even though the request uses the new field.
| dict(self.agent.mcp_config) if self.agent.mcp_config else {} | ||
| ) | ||
|
|
||
| # Update agent and state atomically |
There was a problem hiding this comment.
This only swaps in a new agent_context / mcp_config object. After initialization, though, the conversation has already built its tools and hook processor, so load_plugin() does not actually apply the full plugin semantics promised above: plugin hooks are not merged into _hook_processor, plugin agent defs are not registered, and MCP tools are not rebuilt from the updated config. In practice this looks like 'load skills after init', which seems materially different from the API contract.
Summary
This PR introduces support for registering multiple marketplaces with explicit auto-load semantics, replacing the single
marketplace_pathstring.Closes #2494
Changes
New Models
MarketplaceRegistration- Registration for a plugin marketplace:New Classes
MarketplaceRegistry- Manages registered marketplaces with:plugin-name@marketplaceUpdated
AgentContextDeprecations
AgentContext.marketplace_pathAgentContext.registered_marketplacesBackward compatibility is maintained - if
marketplace_pathis set andregistered_marketplacesis empty, a deprecation warning is emitted and the old behavior is preserved.Plugin Resolution
Reference formats (same as Claude Code plugin syntax):
plugin-name@marketplace-name— Explicit marketplace qualifierplugin-name— Search registered marketplaces (error if ambiguous)Testing
All 21 tests pass.
Documentation
Related
marketplace_pathsupport, released in v1.13.0)marketplace_pathsetting)Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22golang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:18d92ce-pythonRun
All tags pushed for this build
About Multi-Architecture Support
18d92ce-python) is a multi-arch manifest supporting both amd64 and arm6418d92ce-python-amd64) are also available if needed