Skip to content

fix: make root marketplace catalog real files (not symlinks) - #395

Open
jpelletier1 wants to merge 3 commits into
mainfrom
openhands/fix-symlinked-marketplace-catalog
Open

fix: make root marketplace catalog real files (not symlinks)#395
jpelletier1 wants to merge 3 commits into
mainfrom
openhands/fix-symlinked-marketplace-catalog

Conversation

@jpelletier1

@jpelletier1 jpelletier1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
  • A human has tested these changes.

Why

When the plugin-directory is configured with MARKETPLACE_SOURCE=github://openhands/extensions, the marketplace catalog does not render (empty Plugin Directory).

Root cause: the catalog was exposed through a double symlink chain, and raw.githubusercontent.com does not dereference git symlinks (it serves the literal symlink target text; for symlinked directory paths it returns 404):

  • .claude-plugin (symlink) -> .plugin
  • .plugin/marketplace.json (symlink) -> ../marketplaces/openhands-extensions.json

Verified empirically against main: GET .plugin/marketplace.json returns HTTP 200 but the body is the literal string ../marketplaces/openhands-extensions.json (not JSON), and GET .claude-plugin/marketplace.json returns 404 (symlinked directory not traversable). So consumers doing GET + json.loads() get a 404 or a parse error and an empty catalog. This is a server-side (GitHub raw) limitation the plugin-directory client cannot work around, so the catalog simply never arrives.

Summary

  • Replaced the root vendor symlinks (.claude-plugin, .codex-plugin, .plugin/marketplace.json) with real directories + real marketplace.json files that each mirror the canonical marketplaces/openhands-extensions.json byte-for-byte (canonicalized JSON). Git mode changed from 120000 (symlink) to 100644 (regular file). The catalog is now reachable at every standard .claude-plugin/marketplace.json / .codex-plugin/marketplace.json / .plugin/marketplace.json path over the raw URL.
  • Added a root-marketplace sync task to scripts/sync_extensions.py that keeps all three root copies in sync with the canonical file and reports drift in --check mode. Since CI runs python scripts/sync_extensions.py --check with no task filter, the new task is automatically enforced on every PR.
  • Added tests in tests/test_sync_extensions.py (TestRootMarketplace) verifying the real-repo files are non-symlinks, match the canonical catalog, are valid JSON, and that the sync task detects/fixes both the original symlink chain and stale copies.
  • Updated AGENTS.md with the invariant (root catalog copies are real files, never symlinks).

Issue Number

Closes #394

How to Test

  1. pip install pyyaml pytest
  2. python scripts/sync_extensions.py --check - exits 0 (only the pre-existing, non-blocking coverage warning for plugins/issue-duplicate-checker remains).
  3. python -m pytest tests/test_sync_extensions.py -q - 40 passed.
  4. Confirm the files are real (mode 100644, not 120000): git ls-files -s -- .claude-plugin .codex-plugin .plugin
  5. (Optional, network) After merge to main, curl -fsSL https://raw.githubusercontent.com/OpenHands/extensions/main/.claude-plugin/marketplace.json returns the catalog JSON (HTTP 200) instead of 404. (Cannot verify pre-merge from this branch since the raw URL serves main.)

Video/Screenshots

N/A - file-system / raw-URL fix with no UI surface.

Notes

  • The .claude-plugin/marketplace.json, .codex-plugin/marketplace.json, and .plugin/marketplace.json files are now generated copies; edit the canonical marketplaces/openhands-extensions.json and run python scripts/sync_extensions.py (no args) to refresh all three. Do not reintroduce symlinks at these paths.
  • Per-skill/per-plugin .claude-plugin and .codex-plugin vendor symlinks within skills/ and plugins/ are unrelated and unchanged (managed by the existing symlinks sync task).
  • JSON is canonicalized (sorted keys, 2-space indent) for stable, minimal diffs and reliable byte-for-byte equality checks.

This PR was created by an AI agent (OpenHands) on behalf of a user.

raw.githubusercontent.com does not dereference git symlinks through
directory paths, so the double symlink chain
(.claude-plugin -> .plugin -> ../marketplaces/openhands-extensions.json)
made the catalog unreachable at the standard
.claude-plugin/marketplace.json path. Consumers using
github://openhands/extensions (e.g. openhands/plugin-directory) got a 404
or the literal symlink body and saw an empty Plugin Directory.

Replace the root vendor symlinks with real marketplace.json copies that
mirror the canonical marketplaces/openhands-extensions.json byte-for-byte.
Add a root-marketplace sync task to scripts/sync_extensions.py to keep
them in sync (and CI --check enforces it), plus tests.

Closes #394

Co-authored-by: openhands <openhands@all-hands.dev>
Collapse the three root marketplace copies into a single canonical one at
.plugin/marketplace.json (this repo's canonical manifest dir). Drop the
root .claude-plugin/ and .codex-plugin/ mirrors entirely - they duplicated
the catalog with no raw-URL benefit, since raw.githubusercontent.com
cannot dereference symlinks and the real file already lives at .plugin/.

Per-skill/per-plugin .claude-plugin and .codex-plugin vendor symlinks
(within skills/ and plugins/) are unrelated and remain.

Note: openhands/plugin-directory hardcodes
.claude-plugin/marketplace.json as its fetch path, so it must be updated
to fetch .plugin/marketplace.json for this to resolve #394 on its side.

Co-authored-by: openhands <openhands@all-hands.dev>
@jpelletier1 jpelletier1 changed the title fix: make root marketplace catalog real files (not symlinks) fix: publish marketplace catalog as real file at .plugin/marketplace.json Jul 8, 2026
@jpelletier1 jpelletier1 changed the title fix: publish marketplace catalog as real file at .plugin/marketplace.json fix: make root marketplace catalog real files (not symlinks) Jul 8, 2026
@jpelletier1
jpelletier1 marked this pull request as ready for review July 8, 2026 14:25
@jpelletier1

Copy link
Copy Markdown
Contributor Author

@jpshackelford As someone passionate about Plugins, I wanted to see if you had a chance to review this PR. Specifically, the symlinks we use in the OpenHands/extensions repo makes it difficult to render the full plugin marketplace in the Plugin Directory -- this attempts to fix that by creating an actual marketplace.json in the respective folders and keeping them in sync.

@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

3 similar comments
@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Marketplace catalog unreachable via symlinked .claude-plugin path (breaks plugin-directory / github://openhands/extensions)

3 participants