Skip to content

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

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

enyst wants to merge 3 commits 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:

Why

The skills catalog returned empty before attempting the .plugin/marketplace.json manifest used by OpenHands/extensions.

Summary

 _fetch_catalog_entries(repo_path)
   Marketplace.load(repo_path)       # normal manifest discovery
     success → catalog entries
     missing / invalid
       explicit marketplace_path exists? → load that fallback
       neither works                    → empty catalog

Try normal marketplace discovery before checking the explicit legacy path, matching the plugin catalog’s ordering.

Source: skills_service.py.

Issue Number

Fixes #4665

How to Test

Previously recorded verification
  • 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

This is the narrow manifest-order fix. It does not implement the issue’s broader shared, success-only snapshot cache: the five-minute cache can still retain an empty result, and sync does not invalidate it (F25.marketplace-stale-cache). Maintainer choice remains whether to take this partial fix or expand the cache design. Schema, plugin-over-skill precedence and installed checks are unchanged. Independent comparison: #4785.

Visual explanation by Codex using HumanLayer show-me.


🐳 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:ae35866-python

Run

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

All tags pushed for this build

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

About Multi-Architecture Support

  • Each variant tag (e.g., ae35866-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., ae35866-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

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   skills_service.py2434581%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, 557, 574, 591, 608, 744–745, 766–768, 782, 784
TOTAL516502044960% 

@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

…-public

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

@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 skills catalog lives in openhands-agent-server, which this repository owns. In scope, no cross-repo move needed.

Code change verified on head 901c3f8b.

  • skills_service._fetch_catalog_entries now calls Marketplace.load(repo_path) first and reads the explicit marketplace_path file only when discovery raises FileNotFoundError/ValueError, returning [] only when neither works (skills_service.py:747-759). This mirrors plugins_service._fetch_plugin_catalog_entries (plugins_service.py:344-376) and removes the early marketplaces/default.json existence gate that blanked the catalog. This is the correct minimal fix for the reported symptom.
  • The fallback branch, plugin-over-skill precedence, plugin filtering, the fresh installed check, and the REST schemas are unchanged.
  • Focused tests pass in the workspace: uv run pytest tests/agent_server/test_skills_service.py tests/agent_server/test_skills_router.py -q -> 95 passed. The new TestFetchCatalogEntries cases cover the .plugin-only layout, the explicit-path fallback, and the empty-repo case.
  • All 51 check runs on 901c3f8b are green.

Acceptance-criteria status against issue #4665 (currently open, ready-for-dev, labeled architecture, priority:low). The issue lists five criteria:

  • #1 .plugin/marketplace.json layout — met.
  • #2 explicit marketplace path as fallback — met.
  • #3 "Skills and plugins within the TTL use the same fetched/parsed snapshot" — not met. Skills and plugins still fetch and parse the repository independently, each with its own cache (_catalog_cache at skills_service.py:673, _plugin_catalog_cache at plugins_service.py:308).
  • #4 "Transient fetch/parse failures do not cache an empty catalog" — already satisfied on this head: the PR base includes merged #5903, so service_get_marketplace_catalog caches only non-empty results (skills_service.py:712-715) and sync_public_skills clears the cache (skills_service.py:491). The earlier review's criterion-#4 concern no longer applies to the current head.
  • #5 precedence, filtering, installed-state freshness, schemas unchanged — met.

So this head delivers a narrower scenario than the linked issue (criterion #3 unmet) while the description still says Fixes #4665. The PR body flags exactly this and asks a maintainer to choose between the minimal loader-order fix and the shared snapshot described in the issue's "Suggested Implementation" (an alternative is open PR #4785). An acceptance gap against the linked issue is a maintainer scope decision under this repository's review guide, so approval is deferred to a human maintainer.

No correctness, security, or compatibility defect was found in the code change itself; the only open item is the acceptance scope above.

Recommendation: confirm the reduced scope and retarget the closing reference (for example Refs #4665, tracking the shared-snapshot criterion #3 as a follow-up), or fold criterion #3 into this PR.

🔄 CHANGES REQUESTED

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

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed after this PR merges to main through a cleanup PR.

@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 touches openhands-agent-server/openhands/agent_server/skills_service.py plus its tests and the verify-agent-server feature map. The skills catalog belongs to the Agent Server, which this repository owns; no cross-repo move is needed.

Code change verified on head ae35866fe (fresh fetch; prior reviews covered d407be8 and 901c3f8b, and this head is new).

  • The diff against main is exactly three files. _fetch_catalog_entries now calls Marketplace.load(repo_path) first — which discovers .plugin/marketplace.json / .claude-plugin/marketplace.json — and reads the explicit marketplace_path file only when discovery raises FileNotFoundError/ValueError and the file exists, returning [] only when neither works. This matches plugins_service._fetch_plugin_catalog_entries line for line and removes main's early marketplaces/default.json existence gate that blanked the catalog on the real OpenHands/extensions layout. This is the correct minimal fix for the reported symptom.
  • Fallback error handling is unchanged from base (same inner json.JSONDecodeError/ValidationError/OSError catch), plugin-over-skill name precedence, the fresh installed check, and all REST schemas are untouched — no OpenAPI drift (Check OpenAPI Schema and REST API (OpenAPI) are green on this head).
  • Cache semantics are unchanged by this PR, and I re-verified them on the base (901c3f8bc, first parent of this merge): service_get_marketplace_catalog already caches only non-empty results and sync_public_skills already invalidates the catalog cache. Criterion #4 of the linked issue ("transient failures do not cache an empty catalog") is already satisfied on this head; the first bot review's concern there is stale for the current code.
  • Focused tests in the workspace: uv run pytest tests/agent_server/test_skills_service.py tests/agent_server/test_skills_router.py -q → 101 passed, including the three new TestFetchCatalogEntries cases (.plugin-only layout, explicit-path fallback, empty repo).
  • All check runs on ae35866fe are green, including agent-server-tests, sdk-tests, integration-test, pre-commit, and security.
  • One deliberate behavior change worth naming (not a defect): for a repository that ships both a manifest and the explicit file, the manifest now wins where main used the explicit file. That is exactly the "manifest discovery first, explicit path only as fallback" ordering the linked issue asks for, and it keeps skills consistent with the plugin catalog.

Acceptance-criteria status against linked issue #4665 (currently open, ready-for-dev, labeled bug/architecture/priority:low):

  • #1 .plugin/marketplace.json layout — met.
  • #2 explicit path as fallback — met.
  • #4 transient failures not cached as empty — already met on this head (see above).
  • #5 precedence/filtering/installed freshness/schemas unchanged — met.
  • #3 "Skills and plugins within the TTL use the same fetched/parsed snapshot" — not met: skills and plugins still fetch and parse independently with separate caches (_catalog_cache, _plugin_catalog_cache).

This head therefore delivers a narrower fix than the issue's suggested shared, success-only snapshot, while the PR description still says Fixes #4665. The gap is transparent — the PR body flags it and asks a maintainer to choose between this minimal loader-order fix and the shared-snapshot design (alternative PR #4785 remains open) — and the issue's author/maintainer notes the same in the issue thread. No one has recorded that maintainer decision on the issue or PR yet.

What's needed to proceed: a maintainer scope decision — either confirm the reduced scope and retarget the closing reference (e.g. Refs #4665, tracking the shared-snapshot criterion #3 and the F25.marketplace-stale-cache follow-up separately), or fold criterion #3 into this PR. No correctness, security, or compatibility defect was found in the changed code; the open item is purely this scope/acceptance decision, so approval is deferred to a human maintainer.

🔄 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