Skip to content

fix(agent-server): load skills marketplace from .plugin manifest first - #5767

Open
enyst wants to merge 1 commit into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-marketplace-public
Open

enyst wants to merge 1 commit into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-marketplace-public

Conversation

@enyst

@enyst enyst commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

HUMAN:
This PR proposes a fix for a bug uncovered during the first full agentic run of verify-agent-server.


AGENT:

I'm an AI agent (Claude Code) helping Engel Nyst (@enyst) with project maintenance.

Requested by Engel · project thread

Decision needed

Scope: minimal loader-order fix (this PR) or the shared marketplace snapshot the issue suggests. The issue's "Suggested Implementation" and two of its acceptance criteria ask for a shared, success-only snapshot cache used by both the skills and plugins catalogs. This PR implements only the manifest-layout fix: it makes the skills catalog load the marketplace in the same order the plugin catalog already does (Marketplace.load first, marketplace_path as fallback). I chose the smaller change because it fixes what a user sees (an empty Marketplace tab) with a few lines and no new shared state, and the cache behaviour is tracked separately on the map (F25.marketplace-stale-cache: the catalog caches empty results and sync does not clear it), to be fixed in its own PR.

The alternative (one shared snapshot) would also satisfy "skills and plugins within the TTL use the same fetched/parsed snapshot" and "transient failures do not cache an empty catalog", at the cost of a larger refactor across skills_service.py and plugins_service.py. Another contributor's open PR, #4785, is linked to the issue; I did not read it, to keep this fix independent. A maintainer should pick one of the two (or merge this one and keep the shared snapshot as a follow-up).

Why

GET /api/skills/marketplace (the Marketplace tab) returns {"skills": []} against the real OpenHands/extensions repository. _fetch_catalog_entries returned early when marketplaces/default.json was missing, before trying Marketplace.load, which finds the repository's actual manifest at .plugin/marketplace.json. The repository ships marketplaces/openhands-extensions.json and large-codebase.json, not default.json.

Summary

  • skills_service._fetch_catalog_entries: try Marketplace.load(repo_path) first; read the explicit marketplace_path only when discovery fails, and return [] only when neither works (mirrors plugins_service._fetch_plugin_catalog_entries).
  • Unit tests: the .plugin/marketplace.json layout without default.json lists entries (fails on main), the explicit-path fallback still works, and an empty repo still yields [].
  • Feature map: F25.marketplace-public loses its known bug marker; its prose and two Gotchas now describe the working behaviour.

Issue Number

Fixes #4665

How to Test

  • uv run pytest tests/agent_server/test_skills_service.py tests/agent_server/test_skills_router.py -q: 90 passed (the new manifest test fails on main with Marketplace file not found: .../marketplaces/default.json).
  • Map proof against the real OpenHands/extensions clone:
    • main (dfd8fe8): control-agent-server map run --file F25 --only F25.marketplace-public --fresh --repeat 2 --checkout <main checkout> reported xfail in both attempts (200 with {"skills": []}).
    • this branch: the same command launching this worktree reported xpass in both attempts; the catalog listed 73 entries with absolute sources inside the clone.
    • after dropping the marker: control-agent-server map check ok, and map run --file F25 --only F25.marketplace-public --fresh reports pass.
  • uv run pre-commit run --files <changed files>: clean.

Video/Screenshots

n/a

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

The 5-minute catalog cache (including caching an empty result, and not being cleared by POST /api/skills/sync) is unchanged here; it is the separate F25.marketplace-stale-cache case noted on the issue. REST schemas, plugin-over-skill name precedence and the fresh installed check are unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_014MKndVj7XWvRQmvW9w2bDg


Generated by Claude Code


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

• 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-slim amd64, arm64 python-node-runtime Link
python-minimal amd64, arm64 python-node-runtime Link
python amd64, arm64 python-node-runtime 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:d407be8-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:d407be8-golang-amd64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-golang-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-golang-amd64
ghcr.io/openhands/agent-server:d407be8-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:d407be8-golang-arm64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-golang-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-golang-arm64
ghcr.io/openhands/agent-server:d407be8-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:d407be8-java-amd64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-java-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-java-amd64
ghcr.io/openhands/agent-server:d407be8-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:d407be8-java-arm64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-java-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-java-arm64
ghcr.io/openhands/agent-server:d407be8-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:d407be8-python-amd64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-amd64
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-amd64
ghcr.io/openhands/agent-server:d407be8-python-arm64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-arm64
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-arm64
ghcr.io/openhands/agent-server:d407be8-python-minimal-amd64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-minimal-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-minimal-amd64
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:d407be8-python-minimal-arm64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-minimal-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-minimal-arm64
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:d407be8-python-slim-amd64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-slim-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-slim-amd64
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:d407be8-python-slim-arm64
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-slim-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-slim-arm64
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:d407be8-golang
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-golang
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-golang
ghcr.io/openhands/agent-server:d407be8-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:d407be8-java
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-java
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-java
ghcr.io/openhands/agent-server:d407be8-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:d407be8-python-minimal
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-minimal
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-minimal
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-minimal
ghcr.io/openhands/agent-server:d407be8-python-slim
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python-slim
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python-slim
ghcr.io/openhands/agent-server:d407be8-python-node-runtime-slim
ghcr.io/openhands/agent-server:d407be8-python
ghcr.io/openhands/agent-server:d407be8720e678c68e15af99a49ce5319bd47183-python
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-marketplace-public-python
ghcr.io/openhands/agent-server:d407be8-python-node-runtime

About Multi-Architecture Support

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

GET /api/skills/marketplace returned an empty catalog against the real
OpenHands/extensions repository: _fetch_catalog_entries gave up when
marketplaces/default.json was missing, before trying Marketplace.load,
which discovers .plugin/marketplace.json. Try manifest discovery first
and keep the explicit marketplace path as the fallback, as the plugin
catalog already does. Drop the known-bug marker from F25.marketplace-public.

Fixes #4665

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MKndVj7XWvRQmvW9w2bDg
@github-actions

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   skills_service.py2364880%172, 175–178, 181–183, 186–187, 190–194, 197–201, 203, 207–208, 210, 221–222, 302, 309–310, 315, 337–338, 406–407, 521, 544, 561, 578, 595, 612, 629, 721–722, 743–745, 759, 761
TOTAL497291985560% 

@enyst
enyst marked this pull request as ready for review October 13, 2026 21:48
@enyst
enyst requested a review from all-hands-bot October 13, 2026 21:49

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This review was posted by an AI agent (OpenHands).

Review of #5767 — load skills marketplace from .plugin manifest first

Scope/ownership: The change lives in openhands-agent-server (skills catalog), which this repository owns, so it belongs here.

What I verified on head d407be87:

  • skills_service._fetch_catalog_entries now mirrors plugins_service._fetch_plugin_catalog_entries: Marketplace.load(repo_path) first, and the explicit marketplace_path file only as a fallback. The old early return when marketplaces/default.json was missing is gone. This is the correct minimal fix for the reported symptom (empty Marketplace tab against the real OpenHands/extensions layout, which ships .plugin/marketplace.json).
  • The fallback branch preserves explicit-path behavior; plugin-over-skill precedence, plugin filtering, the fresh installed check, and REST schemas are untouched.
  • Focused tests: uv run pytest tests/agent_server/test_skills_service.py tests/agent_server/test_skills_router.py -q -> 90 passed. The new TestFetchCatalogEntries cases cover the .plugin-only layout, the explicit-path fallback, and the empty-repo case.
  • Current-head checks are green (agent-server-tests, sdk-tests, REST API (OpenAPI), security, etc.).

Material finding (scope / acceptance criteria): Linked issue #4665 (ready-for-dev, labeled architecture) lists five acceptance criteria. This head satisfies #1 (.plugin/marketplace.json layout), #2 (explicit path as fallback), and #5 (unchanged precedence/filtering/installed freshness/schemas). It does not satisfy:

  • #3 "Skills and plugins within the TTL use the same fetched/parsed snapshot" — no shared snapshot; skills and plugins still fetch and parse the repository independently.
  • #4 "Transient fetch/parse failures do not cache an empty catalog" — service_get_marketplace_catalog still caches whatever _fetch_catalog_entries returns for 300s, including an empty list (unchanged lines around entries = _fetch_catalog_entries(marketplace_path) / _catalog_cache = (now, entries)), and POST /api/skills/sync does not clear it.

The PR body itself flags this ("Scope: minimal loader-order fix (this PR) or the shared marketplace snapshot the issue suggests… A maintainer should pick one of the two"), while the description still says Fixes #4665. Because the change addresses a narrower scenario than the linked issue, this needs a maintainer scope decision: either confirm the reduced scope and retarget the closing reference (e.g. Refs #4665, with the shared-snapshot / empty-cache work tracked as a follow-up — the author names F25.marketplace-stale-cache), or fold criteria #3/#4 into this PR.

The code change itself is correct and low-risk; I have no correctness, security, or compatibility finding against it. The only open item is the scope/acceptance gap above, which the repository review guide directs to human maintainer review.

🔄 CHANGES REQUESTED

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.

[Bug]: Skills marketplace returns empty against the current extensions manifest layout

3 participants