Feature/676 url based marketplace#691
Conversation
…skills_index - TestMarketplaceSourceURL: covers source_type='url' creation, immutability, is_url_source helper, to_dict/from_dict serialization, and roundtrip for both URL and GitHub sources (backward-compat preservation) - TestParseAgentSkillsIndex: covers happy-path parsing of skill-md and archive entries, multi-skill indexes, empty lists, $schema version enforcement, skill name validation (RFC: 1-64 chars, lowercase alnum + hyphens), and mixed valid/invalid entry filtering Part of issue microsoft#676 (URL-based marketplace sources). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…crosoft#676) Implements RFC v0.2.0 Agent Skills discovery for arbitrary HTTPS endpoints alongside the existing GitHub-hosted marketplace.json sources. Core changes: - models.py: extend MarketplaceSource with source_type='url' and url field; add parse_agent_skills_index() for RFC v0.2.0 index format with strict $schema enforcement, skill-name validation, and sha256 digest verification - client.py: add _fetch_url_direct() (HTTPS-only, conditional GET, ETag/ Last-Modified caching, digest mismatch detection); add _detect_index_format() and _parse_manifest() dispatcher; on_stale_warning callback on network errors - commands/marketplace.py: add URL branch to 'marketplace add'; HTTPS enforced at command layer; .well-known auto-resolution via _resolve_index_url() - marketplace/archive.py (new): safe download and extraction of .tar.gz / .zip archive skill packages with path-traversal and decompression-bomb guards - deps/lockfile.py: extend provenance tracking for URL-sourced skills - resolver.py: URL source support in skill resolution path Tests (new files): - test_marketplace_url_client.py — _fetch_url_direct, caching, 304 handling - test_marketplace_url_commands.py — marketplace add URL branch, HTTPS enforcement - test_marketplace_url_resolver.py — resolver URL source path - test_marketplace_archive.py — archive extraction safety cases Tests (extended): - test_marketplace_url_models.py — MarketplaceSource URL fields + round-trips - test_marketplace_client.py — stale-cache / on_stale_warning coverage - test_lockfile_provenance.py — URL provenance fields - test_marketplace_install_integration.py — updated for new source shape - test_marketplace_resolver.py — pruned redundant cases covered elsewhere Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- S1: validate resp.url scheme post-redirect in _fetch_url_direct - DR1: from_dict raises ValueError on unknown source_type - DR3: add Optional[Tuple[Dict, str]] return type to _read_cache - DR4: preserve query string in _resolve_index_url - DR5: document GitHub kwargs behavior in _parse_manifest docstring - DR6: case-insensitive scheme detection in add command - DR7: PEP 8 two blank lines after FetchResult class 364 tests passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@microsoft-github-policy-service agree |
| with patch("apm_cli.commands.marketplace._get_console", return_value=None): | ||
| result = runner.invoke(marketplace, ["list"]) | ||
|
|
||
| assert "https://example.com" in result.output |
There was a problem hiding this comment.
Fixed. Refactored the assertion to use a full URL variable instead of substring matching. The test now compares against the exact expected URL, eliminating the CodeQL false positive. See commit fa8b7a4.
| # Provide "n" so it cancels without actually removing | ||
| result = runner.invoke(marketplace, ["remove", "example-skills"], input="n\n") | ||
|
|
||
| assert "https://example.com" in result.output |
There was a problem hiding this comment.
Fixed. Same approach -- replaced substring URL check with exact variable comparison. See commit fa8b7a4.
There was a problem hiding this comment.
Pull request overview
This PR extends APM's marketplace subsystem to support registering and consuming Agent Skills discovery indexes from arbitrary HTTPS URLs, alongside the existing GitHub-backed marketplace flow.
Changes:
- Add a new URL-based marketplace source type (
source_type="url") with.well-knownindex auto-resolution and updated marketplace list/remove/update behaviors. - Implement direct HTTPS index fetching with SHA-256 digest capture, conditional refresh (ETag/Last-Modified), stale-while-revalidate behavior, and format auto-detection (Agent Skills vs legacy
marketplace.json). - Add provenance fields (
source_url,source_digest) to marketplace manifests and lockfile dependencies, plus new archive download/extraction support and tests.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/apm_cli/commands/marketplace.py | Adds URL detection + .well-known resolution; updates list/remove/update UX for URL sources. |
| src/apm_cli/marketplace/models.py | Extends MarketplaceSource; adds Agent Skills index parser + provenance fields. |
| src/apm_cli/marketplace/client.py | Adds URL fetch path with caching, digest/ETag handling, and format auto-detection. |
| src/apm_cli/marketplace/resolver.py | Allows non-GitHub HTTPS URL plugin sources; supports skill-md and archive source types. |
| src/apm_cli/marketplace/archive.py | New archive download + safe extraction utilities for type: "archive". |
| src/apm_cli/deps/lockfile.py | Adds source_url / source_digest provenance to LockedDependency. |
| tests/unit/marketplace/test_marketplace_url_models.py | New unit tests for URL source model + Agent Skills parser validation. |
| tests/unit/marketplace/test_marketplace_url_client.py | New unit tests for URL fetch, cache behavior, digest, and conditional refresh. |
| tests/unit/marketplace/test_marketplace_url_commands.py | New CLI tests for marketplace add/list/remove/update URL behavior and error messages. |
| tests/unit/marketplace/test_marketplace_url_resolver.py | New resolver tests for URL marketplaces and Agent Skills source types. |
| tests/unit/marketplace/test_marketplace_archive.py | New tests for archive extraction safety (traversal, bombs, links). |
| tests/unit/marketplace/test_marketplace_resolver.py | Removes old URL-source resolver tests superseded by new URL resolver test file. |
| tests/unit/marketplace/test_marketplace_install_integration.py | Refactors marketplace ref parsing tests into a parametric form. |
| tests/unit/marketplace/test_marketplace_client.py | Adapts tests to updated _read_cache() return shape. |
| tests/unit/marketplace/test_lockfile_provenance.py | Adds lockfile provenance field coverage for URL/index digest. |
src/apm_cli/commands/marketplace.py
Outdated
| for s in sources: | ||
| table.add_row(s.name, f"{s.owner}/{s.repo}", s.branch, s.path) | ||
| if s.is_url_source: | ||
| table.add_row(s.name, s.url, "—", "—") |
There was a problem hiding this comment.
The Rich table uses the Unicode em dash character ("—") for URL sources (Branch/Path). Project encoding rules require printable ASCII for all CLI output; this can trigger UnicodeEncodeError on Windows cp1252. Replace these placeholders with ASCII (e.g., "-", "n/a", or empty strings).
| table.add_row(s.name, s.url, "—", "—") | |
| table.add_row(s.name, s.url, "-", "-") |
There was a problem hiding this comment.
Fixed. All em-dashes replaced with ASCII -- or - across all PR files. See commit fa8b7a4.
| from urllib.parse import urlparse | ||
|
|
||
| if urlparse(url).scheme != "https": | ||
| raise MarketplaceFetchError(url, "URL sources must use HTTPS") | ||
| try: | ||
| headers = {"User-Agent": "apm-cli"} | ||
| if etag: | ||
| headers["If-None-Match"] = etag | ||
| if last_modified: | ||
| headers["If-Modified-Since"] = last_modified | ||
| resp = requests.get(url, headers=headers, timeout=30) | ||
| # Guard against HTTPS→HTTP redirect (S1) | ||
| final_url = getattr(resp, "url", None) | ||
| if isinstance(final_url, str) and urlparse(final_url).scheme != "https": | ||
| raise MarketplaceFetchError( | ||
| url, "Redirect to non-HTTPS URL rejected" |
There was a problem hiding this comment.
_fetch_url_direct() enforces HTTPS with urlparse(url).scheme != "https", which is case-sensitive if callers pass mixed/upper-case schemes (e.g. HTTPS://...). To honor RFC 3986 scheme case-insensitivity (and match the CLI tests), compare urlparse(...).scheme.lower() for both the initial URL and the post-redirect resp.url check.
There was a problem hiding this comment.
Fixed. Added .lower() to both scheme checks in _fetch_url_direct() (initial URL and post-redirect resp.url). See commit fa8b7a4.
src/apm_cli/marketplace/resolver.py
Outdated
| # Non-GitHub HTTPS URL — return as-is (CDN, arbitrary HTTPS host, etc.) | ||
| if url.startswith("https://"): | ||
| return url | ||
|
|
There was a problem hiding this comment.
_resolve_url_source() only accepts non-GitHub URLs when url.startswith("https://"), which rejects valid HTTPS URLs with mixed/upper-case schemes. Consider parsing with urlparse and validating scheme.lower() == "https" instead, so URL sources are consistently handled case-insensitively.
There was a problem hiding this comment.
Fixed. Replaced url.startswith("https://") with urlparse(url).scheme.lower() == "https" for RFC 3986 compliance. See commit fa8b7a4.
| def from_dict(cls, data: Dict[str, Any]) -> "MarketplaceSource": | ||
| """Deserialize from JSON dict.""" | ||
| source_type = data.get("source_type", "github") | ||
| if source_type == "url": | ||
| return cls( | ||
| name=data["name"], | ||
| source_type="url", | ||
| url=data.get("url", ""), | ||
| ) | ||
| if source_type != "github": | ||
| raise ValueError(f"Unsupported marketplace source_type: {source_type!r}") | ||
| return cls( | ||
| name=data["name"], | ||
| owner=data["owner"], | ||
| repo=data["repo"], | ||
| owner=data.get("owner", ""), | ||
| repo=data.get("repo", ""), | ||
| host=data.get("host", "github.com"), | ||
| branch=data.get("branch", "main"), | ||
| path=data.get("path", "marketplace.json"), | ||
| source_type="github", | ||
| ) |
There was a problem hiding this comment.
MarketplaceSource.from_dict() allows a URL source to deserialize with a missing/empty url (it falls back to ""). That creates an invalid registered source and can cause cache-key collisions (sha256("")) and confusing runtime errors later. Prefer failing fast by requiring a non-empty url for source_type == "url" and raising ValueError if it's missing.
There was a problem hiding this comment.
Fixed. from_dict() now raises ValueError if source_type="url" with empty/missing url. Added 2 tests. See commit fa8b7a4.
| skill_type = entry.get("type", "") | ||
| url = entry.get("url", "") | ||
| digest = entry.get("digest", "") | ||
| if not _is_valid_digest(digest): | ||
| logger.debug( | ||
| "Skipping Agent Skills entry %r with invalid digest %r in '%s'", | ||
| name, | ||
| digest, | ||
| source_name, | ||
| ) | ||
| continue | ||
| description = entry.get("description", "") | ||
| plugins.append( | ||
| MarketplacePlugin( | ||
| name=name, | ||
| source={"type": skill_type, "url": url, "digest": digest}, | ||
| description=description, | ||
| source_marketplace=source_name, |
There was a problem hiding this comment.
parse_agent_skills_index() does not validate that type, url, and description are strings (or that type is one of the supported artifact types like "skill-md"/"archive"). If these fields are missing or non-strings, invalid sources can flow into MarketplacePlugin and fail later in resolution/install. Add type checks and skip (or warn+skip) entries with unsupported/invalid type or non-string url/description.
There was a problem hiding this comment.
Fixed. Added validation: type must be a string in ("skill-md", "archive"), url must be a non-empty string, description defaults to empty string if non-string. Invalid entries are skipped with a warning log. Added 7 tests. See commit fa8b7a4.
| """Tests for archive download and extraction (Step 7 — gap #14). | ||
|
|
||
| Covers: _check_archive_member safety checks, _detect_archive_format, _extract_tar_gz, | ||
| _extract_zip, and the download_and_extract_archive public API. | ||
| """ | ||
|
|
There was a problem hiding this comment.
This test file includes Unicode em dashes ("—") in docstrings/comments. Repo encoding policy requires printable ASCII in all source files to avoid Windows encoding issues. Replace em dashes with ASCII (e.g., "-" or "--").
|
|
||
| Covers: _cache_key() URL sources, _fetch_url_direct(), fetch_marketplace() | ||
| URL branch, format auto-detection, cache read/write, stale-while-revalidate. | ||
| GitHub paths are not touched — regression tests confirm they still work. |
There was a problem hiding this comment.
This test file includes Unicode em dashes ("—") in docstrings/comments. Repo encoding policy requires printable ASCII in all source files to avoid Windows encoding issues. Replace em dashes with ASCII (e.g., "-" or "--").
| GitHub paths are not touched — regression tests confirm they still work. | |
| GitHub paths are not touched - regression tests confirm they still work. |
| ) | ||
| assert mock_add.called | ||
| source = mock_add.call_args[0][0] | ||
| assert source.source_type == "url" | ||
|
|
||
| def test_http_url_rejected(self, runner): | ||
| """Plain http:// (non-TLS) must be rejected — RFC requires HTTPS.""" | ||
| from apm_cli.commands.marketplace import marketplace | ||
|
|
||
| result = runner.invoke(marketplace, ["add", "http://example.com"]) | ||
| assert result.exit_code != 0 | ||
| assert "https" in result.output.lower() | ||
|
|
||
| @pytest.mark.parametrize("url", [ | ||
| "HTTPS://EXAMPLE.COM", | ||
| "Https://example.com", | ||
| "HTTP://example.com", | ||
| ]) | ||
| def test_mixed_case_scheme_detected_as_url(self, runner, url): | ||
| """URL detection must be case-insensitive per RFC 3986 §3.1.""" | ||
| from apm_cli.commands.marketplace import marketplace | ||
|
|
||
| result = runner.invoke(marketplace, ["add", url]) | ||
| # Should NOT produce "Invalid format ... OWNER/REPO" — | ||
| # it should enter the URL path (may fail on http or succeed on https) | ||
| assert "OWNER/REPO" not in result.output | ||
|
|
There was a problem hiding this comment.
This test file includes Unicode em dashes ("—") in docstrings/comments. Repo encoding policy requires printable ASCII in all source files to avoid Windows encoding issues. Replace em dashes with ASCII (e.g., "-" or "--").
| """Tests for URL-based marketplace source and Agent Skills index parser. | ||
|
|
||
| Covers MarketplaceSource URL fields, serialization round-trips, and the | ||
| parse_agent_skills_index() parser including schema enforcement, skill name | ||
| validation, and source type handling. | ||
| """ |
There was a problem hiding this comment.
This test file includes Unicode em dashes ("—") in docstrings/comments. Repo encoding policy requires printable ASCII in all source files to avoid Windows encoding issues. Replace em dashes with ASCII (e.g., "-" or "--").
There was a problem hiding this comment.
Fixed. All em-dashes replaced with ASCII --. See commit fa8b7a4.
| """Tests for URL-source resolver behaviour. | ||
|
|
||
| Covers: | ||
| - resolve_plugin_source() with skill-md and archive Agent Skills types | ||
| - _resolve_url_source() with non-GitHub HTTPS URLs | ||
| - resolve_marketplace_plugin() end-to-end with a URL marketplace source | ||
| """ |
There was a problem hiding this comment.
This test file includes Unicode em dashes ("—") in docstrings/comments. Repo encoding policy requires printable ASCII in all source files to avoid Windows encoding issues. Replace em dashes with ASCII (e.g., "-" or "--").
There was a problem hiding this comment.
Fixed. All em-dashes replaced with ASCII --. See commit fa8b7a4.
sergio-sisternes-epam
left a comment
There was a problem hiding this comment.
Great work on this feature, @Vicente-Pastor! The implementation quality is high -- strong security posture (HTTPS enforcement at two layers, post-redirect validation, SHA-256 digest integrity), thorough test suite (174 new test functions), and clean backward compatibility. The Agent Skills RFC v0.2.0 parser with strict $schema validation and skill-name rules is well done.
A few items need addressing before we can approve:
Must-fix:
- Non-ASCII em-dash characters in source files (see inline comments) -- the project requires all source code to stay within printable ASCII (U+0020-U+007E) to avoid
charmapcodec errors on Windows cp1252 terminals. - Missing
CHANGELOG.mdentry -- this is a significant user-facing feature (apm marketplace add <URL>). Please add an entry under## [Unreleased]>Added. - Missing documentation updates --
docs/src/content/docs/marketplace pages andpackages/apm-guide/.apm/skills/apm-usage/commands.mdneed to reflect URL-based marketplace support.
Should-fix:
4. archive.py path validation uses custom _check_archive_member() instead of the centralized path_security.py guards (see inline).
5. No response size limit on _fetch_url_direct() -- a malicious server could return a multi-GB JSON payload (see inline).
6. The TestCacheKey tests deleted from test_marketplace_client.py (GitHub-path cache key tests) should be preserved alongside the new URL cache key tests -- those cover the existing GitHub/GHE cache key behavior.
Note: This PR will have merge conflicts with #677 (marketplace versioning) which touches the same files (models.py, client.py, resolver.py). Recommend rebasing after #677 lands.
Also, the PR body mentions "marketplace browse -- handles URL sources in display" but there is no diff for the browse command. Could you verify whether browse actually works for URL sources or update the PR description?
sergio-sisternes-epam
left a comment
There was a problem hiding this comment.
Inline comments for the items referenced in the main review above.
src/apm_cli/marketplace/models.py
Outdated
| # Agent Skills Discovery RFC v0.2.0 — the only schema version we accept. | ||
| _AGENT_SKILLS_SCHEMA = "https://schemas.agentskills.io/discovery/0.2.0/schema.json" | ||
|
|
||
| # RFC skill-name rule: 1-64 chars, lowercase alphanumeric + hyphens, | ||
| # no leading/trailing/consecutive hyphens. | ||
| _SKILL_NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$") | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class MarketplaceSource: | ||
| """A registered marketplace repository. | ||
|
|
||
| Stored in ``~/.apm/marketplaces.json``. | ||
|
|
||
| Two source types are supported: | ||
| - ``"github"`` (default) — a GitHub-hosted marketplace.json index. | ||
| - ``"url"`` — an arbitrary HTTPS Agent Skills discovery endpoint. |
There was a problem hiding this comment.
[must-fix] Em-dashes (U+2014) appear in several lines in this range:
- Line 15:
-- the only schema version - Line 30:
(default) -- a GitHub-hosted - Line 31:
-- an arbitrary HTTPS
Replace all em-dashes with -- throughout. The encoding convention applies to all .py source files, including comments and docstrings. Please also check the rest of the PR for other occurrences (e.g. client.py docstrings).
| """Raised when an archive cannot be downloaded or extracted safely.""" | ||
|
|
||
|
|
||
| def _check_archive_member(member_path: str) -> None: |
There was a problem hiding this comment.
[should-fix] The project convention requires using the centralized path security guards from src/apm_cli/utils/path_security.py (validate_path_segments() and ensure_path_within()) for any filesystem path built from external data. This custom _check_archive_member() is functionally equivalent but creates maintenance drift.
Consider importing and reusing the centralized utilities, or if the archive use case needs additional checks (null bytes, Windows UNC), extending path_security.py so all integrators benefit.
There was a problem hiding this comment.
Fixed. Refactored archive.py to import and use validate_path_segments() and ensure_path_within() from path_security.py. Archive-specific guards (null bytes, Windows UNC/drive-letter paths) are kept as additional checks on top of the centralized ones. See commit fa8b7a4.
| if resp.status_code == 404: | ||
| raise MarketplaceFetchError(url, "404 Not Found") | ||
| resp.raise_for_status() | ||
| raw = resp.content |
There was a problem hiding this comment.
[should-fix] resp.content loads the entire response into memory with no size guard. A malicious or misconfigured server could return a multi-GB JSON payload. Consider adding a Content-Length check before reading, e.g.:
_MAX_INDEX_BYTES = 10 * 1024 * 1024 # 10 MB
content_length = resp.headers.get("Content-Length")
if content_length and int(content_length) > _MAX_INDEX_BYTES:
raise MarketplaceFetchError(url, f"Index exceeds size limit ({content_length} bytes)")Archives already have the 512 MB decompression bomb guard -- the index fetch deserves a similar safety net (10 MB would be generous for any realistic skill index).
There was a problem hiding this comment.
Fixed. Added _MAX_INDEX_BYTES = 10 * 1024 * 1024 (10 MB) constant. Checks both Content-Length header before reading and actual body size after reading. Added 3 tests. See commit fa8b7a4.
src/apm_cli/commands/marketplace.py
Outdated
| for s in sources: | ||
| table.add_row(s.name, f"{s.owner}/{s.repo}", s.branch, s.path) | ||
| if s.is_url_source: | ||
| table.add_row(s.name, s.url, "—", "—") |
There was a problem hiding this comment.
[must-fix] This uses Unicode em-dash (U+2014). The project encoding convention requires all source files stay within printable ASCII (U+0020-U+007E) to avoid charmap codec errors on Windows cp1252 terminals.
Replace with "--" or "n/a".
See: STATUS_SYMBOLS in src/apm_cli/utils/console.py for the project's ASCII-only pattern.
There was a problem hiding this comment.
Fixed. Replaced Unicode em-dash with ASCII - in the Rich table output. See commit fa8b7a4.
- C01/C02: Fix CodeQL URL substring sanitization in test assertions - C03/C11-C16/C19: Replace all Unicode em-dashes with ASCII in PR files - C04/C05: Case-insensitive HTTPS scheme validation (urlparse-based) - C06: Reject empty URL in MarketplaceSource.from_dict() - C07: Strict field validation in parse_agent_skills_index() - C08: HTTPS enforcement + post-redirect check in archive download - C09: Reject non-regular tar members (device files, FIFOs, symlinks) - C10/C17: Centralize path security using path_security.py guards - C18: 10 MB response size limit on _fetch_url_direct() - C20: Add CHANGELOG.md entries for URL marketplace feature - C21: Update marketplace docs and command reference for URL support - C22: Restore deleted TestCacheKey tests - C23: Verified browse command works for URL sources 387 tests passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace non-ASCII arrows (U+2192) and section sign (U+00A7) with ASCII - Update cli-commands.md with URL marketplace add syntax and examples - Update marketplace group description to mention URL sources - Add PR number (microsoft#691) alongside issue number (microsoft#676) in CHANGELOG Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addressing review body items@sergio-sisternes-epam Thanks for the thorough review! All items have been addressed: Inline comments (C01-C19)All 19 inline review comments have been fixed and replied to individually. Summary:
Review body items
Test status387 tests passing (up from 384 before review fixes). All new tests follow AAA pattern with descriptive names. |
Description
Adds support for URL-based marketplace registration, enabling
apm marketplace add <URL>to register Agent Skills discovery indexes served from arbitrary HTTPS endpoints (per the Agent Skills Discovery RFC v0.2.0).This extends the existing GitHub-based marketplace system to support any HTTPS-hosted index, with full caching, conditional refresh (ETag/Last-Modified), digest integrity verification, and provenance tracking.
Fixes #676
Type of change
What's New
Core Features
apm marketplace add <URL>— register an Agent Skills discovery index from any HTTPS URLhttps://example.com→https://example.com/.well-known/agent-skills/index.json$schemavalidation, name validation (1-64 chars, lowercase alphanumeric + hyphens), and digest format enforcement"skills"key) vs legacy marketplace.json ("plugins"key)skill-mdandarchivesource types — resolver handles both RFC-defined artifact typesSecurity
sha256:{64 hex chars})Caching & Performance
Provenance
source_urlandsource_digestfields onMarketplaceManifestandLockedDependencyCommand Updates
marketplace list— shows URL instead of owner/repo for URL sourcesmarketplace remove— correct cache key derivation for URL sourcesmarketplace update— uses source-aware cache clearingmarketplace browse— handles URL sources in displayFiles Changed
src/apm_cli/marketplace/models.pyMarketplaceSourceURL fields,parse_agent_skills_index(), name/digest validatorssrc/apm_cli/commands/marketplace.py_resolve_index_url(), list/remove/update URL display, error handlingsrc/apm_cli/marketplace/client.py_fetch_url_direct(),FetchResult,_detect_index_format(),_parse_manifest(), SHA-256 cache keys, ETag/conditionalsrc/apm_cli/marketplace/resolver.pyskill-md/archivehandling, empty URL guardsrc/apm_cli/marketplace/archive.pysrc/apm_cli/deps/lockfile.pysource_url/source_digestonLockedDependencytests/.../test_marketplace_url_models.pytests/.../test_marketplace_url_client.pytests/.../test_marketplace_url_commands.pytests/.../test_marketplace_url_resolver.pytests/.../test_marketplace_archive.pyTesting
Backward Compatibility
from_dict()handles legacy dicts withoutsource_typekeyto_dict()for GitHub sources omitssource_type(preserves existing format)_read_cache)