Skip to content

feat(plugin): Support multiple marketplace registrations with auto-load semantics#2495

Open
jpshackelford wants to merge 19 commits intomainfrom
feat/multiple-marketplace-registrations
Open

feat(plugin): Support multiple marketplace registrations with auto-load semantics#2495
jpshackelford wants to merge 19 commits intomainfrom
feat/multiple-marketplace-registrations

Conversation

@jpshackelford
Copy link
Contributor

@jpshackelford jpshackelford commented Mar 18, 2026

Summary

This PR introduces support for registering multiple marketplaces with explicit auto-load semantics, replacing the single marketplace_path string.

Closes #2494

Changes

New Models

MarketplaceRegistration - Registration for a plugin marketplace:

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

New Classes

MarketplaceRegistry - Manages registered marketplaces with:

  • Lazy fetching: Marketplaces are only fetched when first needed
  • Caching: Fetched marketplaces are cached for the session
  • Plugin resolution: Resolve plugin references like plugin-name@marketplace

Updated AgentContext

AgentContext(
    registered_marketplaces=[
        MarketplaceRegistration(
            name="public", 
            source="github:OpenHands/skills", 
            auto_load="all"
        ),
        MarketplaceRegistration(
            name="team", 
            source="github:acme/monorepo",
            repo_path="marketplaces/internal",
            auto_load="all"
        ),
        MarketplaceRegistration(
            name="experimental", 
            source="github:acme/experimental"
        ),  # registered, not auto-loaded
    ]
)

Deprecations

Deprecated Replacement
AgentContext.marketplace_path AgentContext.registered_marketplaces

Backward compatibility is maintained - if marketplace_path is set and registered_marketplaces is 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 qualifier
  • plugin-name — Search registered marketplaces (error if ambiguous)

Testing

uv run pytest tests/sdk/plugin/test_marketplace_registry.py -v

All 21 tests pass.

Documentation

Related


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 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:18d92ce-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:18d92ce-golang-amd64
ghcr.io/openhands/agent-server:18d92ce-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:18d92ce-golang-arm64
ghcr.io/openhands/agent-server:18d92ce-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:18d92ce-java-amd64
ghcr.io/openhands/agent-server:18d92ce-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:18d92ce-java-arm64
ghcr.io/openhands/agent-server:18d92ce-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:18d92ce-python-amd64
ghcr.io/openhands/agent-server:18d92ce-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-amd64
ghcr.io/openhands/agent-server:18d92ce-python-arm64
ghcr.io/openhands/agent-server:18d92ce-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-arm64
ghcr.io/openhands/agent-server:18d92ce-golang
ghcr.io/openhands/agent-server:18d92ce-java
ghcr.io/openhands/agent-server:18d92ce-python

About Multi-Architecture Support

  • Each variant tag (e.g., 18d92ce-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., 18d92ce-python-amd64) are also available if needed

@github-actions
Copy link
Contributor

github-actions bot commented Mar 18, 2026

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Contributor

github-actions bot commented Mar 18, 2026

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Contributor

github-actions bot commented Mar 18, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   conversation_router.py1191389%265, 323–326, 338–341, 354, 357–359
   conversation_service.py43810276%122–123, 150, 153, 155, 162–168, 196, 203, 224, 323, 329, 334, 340, 348–349, 358–361, 370, 384–386, 393, 426–427, 466, 469, 486–490, 492–493, 496–497, 500–505, 587, 594–598, 601–602, 606–610, 613–614, 618–622, 625–626, 630–634, 637–638, 644–649, 656–657, 661, 663–664, 669–670, 676–677, 684–685, 689–691, 709, 733, 965, 968
   event_service.py3258573%55–56, 74–76, 85–89, 92–95, 115, 219, 236, 290–291, 295, 303, 306, 352–353, 369, 371, 375–377, 381, 390–391, 393, 397, 403, 405, 413–418, 555, 557–558, 562, 576–578, 580, 584–587, 591–594, 602–605, 625, 629–634, 646–647, 649–650, 657–658, 660–661, 668–669, 671–672, 678, 684, 701–702
   skills_router.py68494%165–166, 168–169
   skills_service.py1625764%125, 159, 162–165, 168–170, 173–174, 177–181, 184–188, 190, 194–195, 197, 208–212, 353–354, 358–360, 377, 405, 411, 414–415, 417–420, 425–426, 428, 431, 438–441, 445–446, 451–452, 454
openhands-sdk/openhands/sdk/context
   agent_context.py134695%302–304, 333, 356, 362
openhands-sdk/openhands/sdk/context/skills
   skill.py4343392%94–95, 249–250, 436–442, 445, 562–565, 790–791, 850–851, 919–920, 1003, 1031, 1054, 1061–1062, 1125–1126, 1132–1133, 1139–1140
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py4453791%291, 296, 325, 352–353, 356, 360–361, 364, 369, 375–376, 419, 437, 453, 518, 696–697, 700, 852, 860, 862, 866–867, 878, 880–882, 907, 979, 1105, 1109, 1179, 1209, 1219, 1267–1268
   remote_conversation.py62510383%77, 79, 150, 177, 190, 192–195, 205, 227–228, 233–236, 319, 329–331, 337, 378, 525–528, 530, 550–554, 559–562, 565, 581, 737–738, 742–743, 757, 781–782, 801, 812–813, 833–836, 838–839, 863–865, 868–872, 874–875, 879, 881–889, 891, 928, 1059, 1131–1132, 1136, 1141–1145, 1151–1157, 1170–1171, 1206, 1262, 1269, 1275–1276, 1338–1339, 1381–1382
openhands-sdk/openhands/sdk/plugin
   registry.py117298%185–186
   types.py215498%205, 330, 661, 668
TOTAL21325527975% 

jpshackelford pushed a commit to OpenHands/docs that referenced this pull request Mar 19, 2026
- 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
@jpshackelford jpshackelford marked this pull request as ready for review March 19, 2026 01:32
Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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. 🚀

@enyst
Copy link
Collaborator

enyst commented Mar 19, 2026

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.

@openhands-ai
Copy link

openhands-ai bot commented Mar 19, 2026

I'm on it! enyst can track my progress at all-hands.dev

Copy link
Collaborator

enyst commented Mar 19, 2026

@enyst I checked the linked issue, this PR, and the API-breakage workflow.

You remembered correctly: #2494 lists 4 deprecations:

  1. AgentContext.marketplace_path -> AgentContext.registered_marketplaces
  2. load_public_skills(marketplace_path=...) -> marketplace registry
  3. load_available_skills(marketplace_path=...) -> marketplace registry
  4. SkillsRequest.marketplace_path -> SkillsRequest.registered_marketplaces

From the code in this PR, though, I only see #1 actually deprecated today:

  • AgentContext.marketplace_path is marked deprecated=True and also emits a DeprecationWarning in the backward-compat path.

The other 3 are not deprecated in the implementation yet:

  • load_public_skills(marketplace_path=...) still exists unchanged and is still used by the current skill-loading path.
  • load_available_skills(marketplace_path=...) still exists unchanged and is still what AgentContext/server skill loading call into.
  • SkillsRequest.marketplace_path is still present on the agent-server /skills API, and I do not see a registered_marketplaces replacement added there in this PR.

So my read is: this PR introduces the new registration/resolution model (MarketplaceRegistration, MarketplaceRegistry, conversation.load_plugin() resolution against registered marketplaces), but it does not complete the full deprecation rollout described in #2494.

So I think the accurate framing is probably:

  • This PR: deprecates AgentContext.marketplace_path and adds the new marketplace-registration mechanism for plugin resolution.
  • Not yet done here: deprecating the old marketplace_path parameters on skill-loading helpers / /skills request model.

On the griffe / Python API breakage workflow: it failed because the checker compares public SDK API against the PyPI baseline, saw the Field(...) value for AgentContext.marketplace_path change (specifically adding deprecated=True), and counted that as an ATTRIBUTE_CHANGED_VALUE break. The checker has an allowlist for metadata-only Field(...) changes (description, title, examples, json_schema_extra), but not deprecated, so it treats this as breaking and then fails SemVer because the package version did not get a minor bump.

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:

  • the checker should also treat Field(..., deprecated=...) as a non-breaking metadata/deprecation annotation, or
  • if the intended policy really is “adding deprecation metadata to a public field counts as a break,” then the current CI result is expected — but that would be a much stricter policy than the behavior this PR is trying to preserve.

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.

@openhands-ai

This comment was marked as duplicate.

Copy link
Collaborator

enyst commented Mar 19, 2026

OpenHands-GPT-5.4 here — @enyst @jpshackelford, now that the small Griffe/Field(..., deprecated=...) CI fix is merged, I think there are two viable paths for this PR.

First, the key observation

Right now, the new model is only partially replacing the old one:

  • registered_marketplaces is used for explicit plugin resolution via conversation.load_plugin(...)
  • but the old path still drives actual skill auto-loading:
    • AgentContext._load_auto_skills() -> load_available_skills(..., marketplace_path=...)
    • load_available_skills() -> load_public_skills(marketplace_path=...)
  • auto_load="all" is modeled, but I do not see production code actually using MarketplaceRegistry.get_auto_load_registrations() to auto-load marketplace plugins at conversation start
  • agent-server /skills still exposes SkillsRequest.marketplace_path, and I do not see SkillsRequest.registered_marketplaces added yet

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 code

What this means

Keep this PR focused on:

  • MarketplaceRegistration
  • MarketplaceRegistry
  • plugin resolution (plugin / plugin@marketplace)
  • conversation.load_plugin()
  • tests/examples for explicit marketplace-backed plugin loading

What I would do under this option

  • remove or soften the deprecation claims in the PR description
  • probably do not deprecate additional APIs yet
  • possibly even reconsider whether AgentContext.marketplace_path should be deprecated in this PR, since its replacement does not yet cover the same startup/auto-load behavior end-to-end

Pros

  • smallest, cleanest PR
  • avoids promising more migration than the code currently delivers
  • keeps the deprecation story honest

Cons

  • leaves the full migration / auto-load rollout for a follow-up PR

Alternative 2: do a bit more in this PR so the new path is used more broadly

What “a bit more” would need to mean

If 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:

  1. Make conversation/plugin startup honor registered_marketplaces with auto_load="all"

    • at conversation startup, fetch the auto-load registrations
    • resolve/load all plugins declared by those marketplaces
    • merge their skills/hooks/MCP the same way explicit plugins are merged today
  2. Keep backward compatibility for legacy users

    • if legacy marketplace_path is used and registered_marketplaces is empty, keep the current behavior working
    • deprecate the legacy field/path with warnings
  3. If we want to deprecate the server-side request field too, add the additive replacement now

    • add SkillsRequest.registered_marketplaces
    • prefer it when present
    • keep SkillsRequest.marketplace_path working as the deprecated fallback

What this would justify deprecating

Under this option, I think these become more defensible:

  • AgentContext.marketplace_path -> AgentContext.registered_marketplaces
  • SkillsRequest.marketplace_path -> SkillsRequest.registered_marketplaces if the new request field is added in this PR

What I still would not deprecate yet

I would still be careful with:

  • load_public_skills(marketplace_path=...)
  • load_available_skills(marketplace_path=...)

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:

  • either keep them for now,
  • or later deprecate the marketplace_path parameter / old code path, not necessarily the whole helper function.

My recommendation

I 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:

  • keep the new marketplace registration / resolution work
  • describe it as the foundation for auto-load semantics
  • avoid deprecating the other 3 APIs yet
  • maybe even narrow the current deprecation language so it does not imply full replacement already exists

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 /skills request fields or SDK helper paths without also shipping the additive replacement in the same PR.

Short version

  • Alternative 1: keep this PR narrower; deprecate less
  • Alternative 2: wire registered_marketplaces into real startup/auto-load behavior, then deprecate more
  • My recommendation: Alternative 1, unless you want to intentionally expand the implementation scope now

jpshackelford pushed a commit to OpenHands/OpenHands that referenced this pull request Mar 19, 2026
…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)
Copy link
Contributor

@neubig neubig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@jpshackelford jpshackelford force-pushed the feat/multiple-marketplace-registrations branch from ae36a32 to abb8b9a Compare March 20, 2026 00:34
@enyst
Copy link
Collaborator

enyst commented Mar 21, 2026

@OpenHands Give us a view on how this PR works and tell WDYT. /codereview Post directly on github as a review with gh api.

@openhands-ai
Copy link

openhands-ai bot commented Mar 21, 2026

I'm on it! enyst can track my progress at all-hands.dev

Copy link
Collaborator

@enyst enyst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@openhands-ai

This comment was marked as duplicate.

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.

feat: Support multiple marketplace registrations with auto-load semantics

5 participants