feat: Marketplace integration -- read marketplace.json for plugin discovery + governance#503
Conversation
Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
- Add marketplace/ package: models, errors, registry, client, resolver - Add marketplace CLI commands: add, list, browse, update, remove, search - Add lockfile provenance fields: discovered_via, marketplace_plugin_name - Add install hook for NAME@MARKETPLACE syntax pre-parse intercept - Wire marketplace commands in cli.py Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
- Create guides/marketplaces.md covering marketplace concepts, registration, browsing, search, install syntax, provenance tracking, and cache behavior - Add apm marketplace and apm search command sections to cli-commands.md - Update apm install arguments to include NAME@MARKETPLACE syntax - Update plugins.md Finding Plugins section with marketplace cross-refs Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
…mplementation - Use array-based plugins format matching models.py parser expectations - Use discovered_via and marketplace_plugin_name matching lockfile.py fields - Document both Copilot CLI (repository/ref) and Claude Code (source) formats Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
- git-subdir uses separate repo and subdir fields - Relative string sources resolve to marketplace repo subdirectory Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
- 114 unit tests across 8 test files covering all marketplace modules - New marketplace guide at docs/src/content/docs/guides/marketplaces.md - Updated CLI reference with marketplace and search commands - Updated plugins guide with marketplace integration section - CHANGELOG entry for marketplace feature Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
- Use List[MarketplacePlugin] from typing instead of lowercase generic - Eliminate duplicated condition in install.py marketplace intercept - Restructure control flow for clarity Agent-Logs-Url: https://github.com/microsoft/apm/sessions/12a9b016-7930-41b8-a340-c64f11486b71 Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a first-class "marketplace" feature to APM: it can discover plugins from marketplace.json (Copilot CLI + Claude Code formats), resolve them into Git dependencies, and then apply the existing governance flow (locking, SHA pinning, provenance).
Changes:
- Introduces
src/apm_cli/marketplace/(models/parser, registry persistence, GitHub fetch + caching, and resolver forNAME@MARKETPLACE). - Adds CLI surface area:
apm marketplace add/list/browse/update/removeand top-levelapm search. - Extends install + lockfile to track marketplace provenance (
discovered_via,marketplace_plugin_name) and documents the workflow.
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/marketplace/init.py | Adds marketplace unit test package. |
| tests/unit/marketplace/test_lockfile_provenance.py | Tests new lockfile provenance fields serialization/back-compat. |
| tests/unit/marketplace/test_marketplace_client.py | Tests marketplace fetch/caching behavior and auto-detection logic. |
| tests/unit/marketplace/test_marketplace_commands.py | Tests new CLI commands using Click CliRunner. |
| tests/unit/marketplace/test_marketplace_errors.py | Tests actionable marketplace error messages. |
| tests/unit/marketplace/test_marketplace_install_integration.py | Tests marketplace ref detection and _ValidationOutcome provenance field presence. |
| tests/unit/marketplace/test_marketplace_models.py | Tests dataclasses and parsing for both marketplace.json formats. |
| tests/unit/marketplace/test_marketplace_registry.py | Tests registry CRUD + persistence and corrupted file handling. |
| tests/unit/marketplace/test_marketplace_resolver.py | Tests NAME@MARKETPLACE regex and source-type resolution. |
| src/apm_cli/cli.py | Registers marketplace command group and top-level search. |
| src/apm_cli/commands/install.py | Adds marketplace pre-parse intercept; threads provenance into the install engine and lockfile generation. |
| src/apm_cli/commands/marketplace.py | Implements apm marketplace ... and apm search commands. |
| src/apm_cli/core/command_logger.py | Extends _ValidationOutcome to carry marketplace_provenance. |
| src/apm_cli/deps/lockfile.py | Adds provenance fields to LockedDependency + (de)serialization. |
| src/apm_cli/marketplace/init.py | Exposes marketplace public API symbols. |
| src/apm_cli/marketplace/client.py | Implements GitHub Contents API fetch, TTL cache, and path auto-detection. |
| src/apm_cli/marketplace/errors.py | Adds marketplace-specific exception hierarchy with next-step guidance. |
| src/apm_cli/marketplace/models.py | Adds frozen dataclasses + parser for Copilot/Claude marketplace.json. |
| src/apm_cli/marketplace/registry.py | Adds persistent registry in ~/.apm/marketplaces.json with caching + atomic writes. |
| src/apm_cli/marketplace/resolver.py | Adds NAME@MARKETPLACE parsing and plugin source-to-canonical resolution. |
| docs/src/content/docs/guides/marketplaces.md | New guide page describing marketplace concepts and workflows. |
| docs/src/content/docs/guides/plugins.md | Updates plugin discovery guidance to include marketplaces. |
| docs/src/content/docs/reference/cli-commands.md | Documents new commands and NAME@MARKETPLACE install syntax. |
| CHANGELOG.md | Adds Unreleased "Added" entries describing the feature. |
- Narrow except Exception to except ImportError for lazy marketplace import (comment #1) - Fix provenance key mismatch: use dep identity instead of canonical for lockfile lookup (comment #2) - Include subdir in git-subdir source resolution with path traversal validation (comment #3) - Include relative path in relative source resolution with traversal validation (comment #4) - Sanitize marketplace name in cache file paths to prevent path traversal (comment #5) - Fix docs: stale-if-error, not stale-while-revalidate (comment #6) - Consolidate CHANGELOG entries into single line with (#503) (comment #7) - Remove unused _SUPPORTED_SOURCE_TYPES set (comment #8) - Let auth errors propagate in _auto_detect_path instead of swallowing (comment #9) - Validate marketplace --name against [a-zA-Z0-9._-]+ charset (comment #10) - Fix doc examples to use identifier-compatible names (comments #11, #12) - Update tests to match corrected resolver behavior, add traversal tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed all 12 review comments in commit e814a88: Code fixes:
Tests: Updated resolver test expectations to match corrected behavior, added path traversal rejection tests. All 3314 unit tests pass. |
Bug #1 - Format incompatibility with awesome-copilot marketplace: - Parser now accepts 'source' key (Copilot CLI) as type discriminator fallback when 'type' key is absent, normalizing to 'type' for resolvers - GitHub source resolver now accepts 'path' field (Copilot CLI) as virtual subdirectory, same as 'subdir' in git-subdir sources - Path traversal validation applied to 'path' field - Fixes: 8 of 62 plugins in awesome-copilot that use github source objects with 'source'+'path' keys instead of 'type'+'subdir' Bug #2 - Lockfile provenance never written: - Root cause: install passed raw marketplace refs (NAME@MARKETPLACE) as only_packages, but DependencyReference.parse() can't parse those, so identity filtering removed all deps -> 'already installed' - Fix: use validated_packages (canonical owner/repo strings) instead of raw click argument for only_pkgs Both bugs verified fixed via E2E tests against real marketplaces: - github/awesome-copilot (62 plugins) - anthropics/skills (3 plugins) - microsoft/azure-skills (1 plugin) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
E2E Testing Against Real Marketplaces (commit ac1afbb)Tested all marketplace commands against 3 real-world marketplaces:
Results: 25/25 PASS (after fixes)
Bugs Found & FixedBug #1 (P1) — Copilot CLI format incompatibility: Bug #2 (P2) — Provenance never written: Verified Post-Fix# apm.lock.yaml now contains:
discovered_via: awesome-copilot
marketplace_plugin_name: azureFull report: |
E2E Marketplace Testing ReportRan 25 test scenarios against 3 official marketplaces using the branch binary built from source. Marketplaces Tested
Commands Tested & ResultsPhase 1: Setup
Phase 2: Registration (
|
| # | Command | Result |
|---|---|---|
| 3 | apm marketplace add awesome-copilot github/awesome-copilot |
PASS -- auto-detected .github/plugin/marketplace.json |
| 4 | apm marketplace add skills anthropics/skills |
PASS -- auto-detected .claude-plugin/marketplace.json |
| 5 | apm marketplace add azure-skills microsoft/azure-skills |
PASS -- auto-detected .claude-plugin/marketplace.json |
| 6 | apm marketplace list (3 registered) |
PASS |
| 7 | apm marketplace add awesome-copilot github/awesome-copilot (duplicate) |
PASS -- clear error |
| 8 | apm marketplace add bad ../traversal (invalid name) |
PASS -- rejected |
Phase 3: Browse & Search (apm marketplace browse/search)
| # | Command | Result |
|---|---|---|
| 9 | apm marketplace browse awesome-copilot |
PASS -- 62 plugins listed |
| 10 | apm marketplace browse skills |
PASS -- 3 plugins listed |
| 11 | apm marketplace browse azure-skills |
PASS -- 1 plugin listed |
| 12 | apm marketplace search azure (cross-marketplace) |
PASS -- results from multiple marketplaces |
| 13 | apm marketplace search nonexistent-xyz |
PASS -- empty results, no crash |
| 14 | apm marketplace browse unknown |
PASS -- clear error message |
Phase 4: Install (apm install NAME@MARKETPLACE)
| # | Command | Result |
|---|---|---|
| 15 | apm install azure@azure-skills |
PASS -- installed, provenance in lockfile |
| 16 | apm install sequential-thinking@skills |
PASS -- installed from Anthropic |
| 17 | apm install azure@awesome-copilot |
PASS -- Copilot CLI format resolved correctly |
| 18 | apm install unknown@skills |
PASS -- clear "not found" error |
| 19 | apm install thing@nonexistent |
PASS -- clear "marketplace not registered" error |
Phase 5: Update & Remove (apm marketplace remove)
| # | Command | Result |
|---|---|---|
| 20 | apm marketplace remove azure-skills |
PASS |
| 21 | apm marketplace list (2 remaining) |
PASS |
| 22 | apm marketplace remove azure-skills (already removed) |
PASS -- clear error |
| 23 | apm marketplace remove skills && apm marketplace remove awesome-copilot |
PASS |
Phase 6: Lockfile & Provenance Verification
| # | Command | Result |
|---|---|---|
| 24 | Verify apm.lock contains discovered_via and marketplace_plugin_name |
PASS |
| 25 | Verify provenance keyed by dependency identity (not canonical) | PASS |
Summary: 25/25 PASS
Two bugs were discovered during initial testing and fixed in commit ac1afbb:
- Copilot CLI format compatibility --
awesome-copilotuses"source": "github"+"path"keys instead of"type"+"subdir". Parser now handles both formats. - Provenance not written to lockfile -- raw
NAME@MARKETPLACEargs could not be parsed byDependencyReference.parse(), causing identity mismatch. Fixed by using validated canonical package strings.
Both fixes have unit tests. Full suite: 3320 tests passing.
Search now requires QUERY@MARKETPLACE (e.g. apm search security@skills) to eliminate name collisions across marketplaces. Added search_marketplace() client function for single-marketplace search. - Rejects bare queries without @ — clear error with usage example - Validates marketplace exists before searching - Updated docs/guides/marketplaces.md with new syntax - 7 test cases: format validation, unknown marketplace, results, no results Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align all documentation with QUERY@MARKETPLACE search format. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace 3 ad-hoc '..' in x.split('/') checks in marketplace/resolver.py
with validate_path_segments() from utils/path_security.py. Add
defense-in-depth validate_path_segments() call to _sanitize_cache_name()
in client.py.
This ensures marketplace code uses the same cross-platform path safety
utilities (backslash normalization, single-dot rejection) as the rest
of APM.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Directs contributors to use validate_path_segments() and ensure_path_within() from utils/path_security.py instead of ad-hoc traversal checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
APM now reads existing
marketplace.jsonfiles (both Copilot CLI and Claude Code formats) for plugin discovery, resolves plugins to Git URLs, then applies its full governance layer (lockfile, SHA pinning, audit trail). Collapses a two-tool workflow into one.New module:
src/apm_cli/marketplace/models.py-- Frozen dataclasses (MarketplaceSource,MarketplacePlugin,MarketplaceManifest) + JSON parser for both Copilot CLI and Claude Code formatserrors.py-- Actionable error hierarchy with next-step commands in messagesregistry.py-- CRUD for~/.apm/marketplaces.jsonwith process-lifetime cache, atomic writesclient.py-- GitHub Contents API fetch viaAuthResolver.try_with_fallback(unauth_first=True), 1h TTL cache, stale-while-revalidate, auto-detect marketplace.json locationresolver.py--NAME@MARKETPLACEregex detection + resolution for 4 source types (github, url, git-subdir, relative; npm rejected with clear message)Install hook (
install.py)DependencyReference.parse()detectsNAME@MARKETPLACEvia^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+$(no/, no:)owner/repo#ref, replaces package variable, stores provenanceValueErrorCLI commands (
commands/marketplace.py+cli.py)apm marketplace add/list/browse/update/removeapm search QUERY(top-level, across all registered marketplaces)Lockfile provenance (
lockfile.py)LockedDependency:discovered_via,marketplace_plugin_nameNoneby default, omitted from YAML when unsetType of change
Testing
114 unit tests across 8 files: models, resolver (20 regex positive/negative cases + 4 source types), client (cache TTL, stale-while-revalidate, auto-detect), registry (CRUD + persistence), CLI commands (CliRunner), lockfile provenance (round-trip + backward compat), install integration, error hierarchy. Full suite: 3295 passed.