Problem Statement
PR #2253 added a marketplace_path string that filters which skills to load from the public skills repository. This design has two limitations:
- Single source: Users can only load skills from one marketplace (the public skills repo). Enterprises need to register multiple marketplaces — their own internal marketplace, team-specific marketplaces, and optionally the public marketplace.
- No registration vs auto-load distinction: The current model is all-or-nothing. There's no way to say "I want this marketplace registered and available, but don't auto-load its plugins into every conversation."
Proposed Solution
Replace the single marketplace_path: str with a list of MarketplaceRegistration objects:
class MarketplaceRegistration(BaseModel):
name: str # Identifier for this registration
source: str # github:owner/repo, git URL, or local path
ref: str | None = None # Optional branch/tag/commit
repo_path: str | None = None # Subdirectory within repo (for monorepos)
auto_load: Literal["all"] | None = None
Behavior:
auto_load: "all" — Fetch the marketplace and load all plugins declared in its manifest at conversation start
auto_load: None (default) — Marketplace is registered for plugin resolution but plugins are not auto-loaded
Examples:
AgentContext(
registered_marketplaces=[
# Standalone marketplace repo
MarketplaceRegistration(
name="public",
source="github:OpenHands/skills",
auto_load="all"
),
# Monorepo with marketplace in subdirectory
MarketplaceRegistration(
name="team",
source="github:acme/monorepo",
repo_path="marketplaces/internal",
auto_load="all"
),
# Registered but not auto-loaded
MarketplaceRegistration(
name="experimental",
source="github:acme/experimental"
),
]
)
Plugin Resolution
When a marketplace is registered (regardless of auto_load setting), the SDK resolves plugin references against it.
MarketplaceRegistry (New)
Introduce a new internal MarketplaceRegistry class to manage registered marketplaces:
class MarketplaceRegistry:
"""Manages registered marketplaces with lazy fetching and plugin resolution."""
def __init__(self, registrations: list[MarketplaceRegistration]): ...
def resolve_plugin(self, plugin_ref: str) -> PluginSource:
"""Resolve 'plugin-name' or 'plugin-name@marketplace' to a PluginSource."""
...
def prefetch_all(self) -> None:
"""Eagerly fetch all registered marketplaces (optional, for validation)."""
...
The registry is an internal implementation detail — users configure registered_marketplaces on AgentContext, and the SDK manages the registry internally.
Lazy Loading
Marketplace fetching happens lazily on first use, not at registration time:
- Registering a marketplace with
auto_load: None has zero startup cost
- The marketplace repo is only fetched when a plugin from it is first referenced
- Fetched marketplaces are cached for the session
- Code managing the registry may optionally call
prefetch_all() for eager validation
Manifest Lookup
Uses the existing Marketplace.load() behavior which checks MARKETPLACE_MANIFEST_DIRS (.plugin, .claude-plugin) for marketplace.json.
Reference Formats
plugin-name@marketplace-name — Explicit marketplace qualifier
plugin-name — Search registered marketplaces (error if ambiguous across marketplaces)
Resolution Flow
- Parse the reference to extract plugin name and optional marketplace name
- Look up marketplace in
registered_marketplaces
- Fetch/cache the marketplace repo if not already fetched (lazy)
- Load marketplace manifest via existing
Marketplace.load()
- Find the plugin in the marketplace's manifest
- Return resolved plugin source for loading
Deprecations
The following APIs added in #2253 (v1.13.0) will be deprecated:
| Deprecated |
Replacement |
AgentContext.marketplace_path |
AgentContext.registered_marketplaces |
load_public_skills(marketplace_path=...) |
Use marketplace registry |
load_available_skills(marketplace_path=...) |
Use marketplace registry |
SkillsRequest.marketplace_path (agent-server) |
SkillsRequest.registered_marketplaces |
Backward compatibility:
-
Deprecated fields/parameters continue to work with deprecation warnings
-
If marketplace_path is set and registered_marketplaces is empty, automatically convert to:
MarketplaceRegistration(
name="public",
source=PUBLIC_SKILLS_REPO,
ref=PUBLIC_SKILLS_BRANCH,
auto_load="all"
)
Implementation Scope
In Scope
- Add
MarketplaceRegistration model to plugin/types.py
- Add
MarketplaceRegistry class (new) for lazy fetching, caching, and resolution
- Add
registered_marketplaces: list[MarketplaceRegistration] to AgentContext
- Reuse existing
Marketplace.load() for manifest loading
- Add plugin resolution logic (
resolve_plugin(name, marketplace?) -> PluginSource)
- Update plugin loading to use resolution against registered marketplaces
- Update skill loading to iterate over registered marketplaces with
auto_load="all"
- Deprecate
marketplace_path APIs (see table above) with backward compatibility
- Tests for multiple marketplace loading and plugin resolution
Out of Scope (Future Work)
auto_load: ["plugin-a", "plugin-b"] selective loading
- Conflict resolution UI/UX for ambiguous plugin names (for now: error)
Related Issues
Directly unblocked by this issue:
- OpenHands/OpenHands#13117 — Alternative to
marketplace_path setting
- APP-708 — Enables org-level default plugins via scoped marketplace registration
- PRD-71 — SDK provides
MarketplaceRegistration model for CLI marketplace management
- APP-769 — SDK provides plugin resolution for
/plugin:command invocation
Builds on:
Problem Statement
PR #2253 added a
marketplace_pathstring that filters which skills to load from the public skills repository. This design has two limitations:Proposed Solution
Replace the single
marketplace_path: strwith a list ofMarketplaceRegistrationobjects:Behavior:
auto_load: "all"— Fetch the marketplace and load all plugins declared in its manifest at conversation startauto_load: None(default) — Marketplace is registered for plugin resolution but plugins are not auto-loadedExamples:
Plugin Resolution
When a marketplace is registered (regardless of
auto_loadsetting), the SDK resolves plugin references against it.MarketplaceRegistry (New)
Introduce a new internal
MarketplaceRegistryclass to manage registered marketplaces:The registry is an internal implementation detail — users configure
registered_marketplacesonAgentContext, and the SDK manages the registry internally.Lazy Loading
Marketplace fetching happens lazily on first use, not at registration time:
auto_load: Nonehas zero startup costprefetch_all()for eager validationManifest Lookup
Uses the existing
Marketplace.load()behavior which checksMARKETPLACE_MANIFEST_DIRS(.plugin,.claude-plugin) formarketplace.json.Reference Formats
plugin-name@marketplace-name— Explicit marketplace qualifierplugin-name— Search registered marketplaces (error if ambiguous across marketplaces)Resolution Flow
registered_marketplacesMarketplace.load()Deprecations
The following APIs added in #2253 (v1.13.0) will be deprecated:
AgentContext.marketplace_pathAgentContext.registered_marketplacesload_public_skills(marketplace_path=...)load_available_skills(marketplace_path=...)SkillsRequest.marketplace_path(agent-server)SkillsRequest.registered_marketplacesBackward compatibility:
Deprecated fields/parameters continue to work with deprecation warnings
If
marketplace_pathis set andregistered_marketplacesis empty, automatically convert to:Implementation Scope
In Scope
MarketplaceRegistrationmodel toplugin/types.pyMarketplaceRegistryclass (new) for lazy fetching, caching, and resolutionregistered_marketplaces: list[MarketplaceRegistration]toAgentContextMarketplace.load()for manifest loadingresolve_plugin(name, marketplace?)->PluginSource)auto_load="all"marketplace_pathAPIs (see table above) with backward compatibilityOut of Scope (Future Work)
auto_load: ["plugin-a", "plugin-b"]selective loadingRelated Issues
Directly unblocked by this issue:
marketplace_pathsettingMarketplaceRegistrationmodel for CLI marketplace management/plugin:commandinvocationBuilds on:
marketplace_pathsupport (released in v1.13.0)