From bb5186e002a0e64c0ad0840a2665735177e6df0e Mon Sep 17 00:00:00 2001 From: Dr Alexander Mikhalev Date: Thu, 15 Oct 2026 10:00:27 +0100 Subject: [PATCH] fix: verify downstream prerelease catalog routes --- .../scripts/verify_registry.py | 138 ++++++++++++++---- .../tests/fixtures/registries/README.md | 6 + .../cursor-latest-prerelease-014.json | 1 + .../tests/verify_registry_test.py | 57 ++++++++ 4 files changed, 174 insertions(+), 28 deletions(-) create mode 100644 vs-code-terraphim-it/tests/fixtures/registries/cursor-latest-prerelease-014.json diff --git a/vs-code-terraphim-it/scripts/verify_registry.py b/vs-code-terraphim-it/scripts/verify_registry.py index 99cc35785..7f36e39bc 100644 --- a/vs-code-terraphim-it/scripts/verify_registry.py +++ b/vs-code-terraphim-it/scripts/verify_registry.py @@ -31,6 +31,14 @@ class NotIndexed(ValueError): """The exact qualified version is not yet offered in this catalog.""" +class CatalogReadError(ValueError): + """Retain the bounded public response when discovery cannot complete.""" + + def __init__(self, message, evidence): + super().__init__(message) + self.evidence = evidence + + IDENTIFIER_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*") @@ -100,11 +108,44 @@ def require_trusted_publication(metadata): raise ValueError("Open VSX does not attest trusted publishing for this exact version") -def gallery_query(manifest): - """Use the same exact-ID query and version/assets flags as Gallery clients.""" +def gallery_query(manifest, *, latest_prerelease=False): + """Request version/assets metadata, including the documented latest-channel mode.""" publisher, name, _ = identity(manifest) return {"filters": [{"criteria": [{"filterType": 7, "value": f"{publisher}.{name}"}], - "pageNumber": 1, "pageSize": 100}], "flags": 147} + "pageNumber": 1, "pageSize": 100}], + # IncludeLatestPrereleaseAndStableVersionOnly (65536) replaces + # IncludeVersions (1); keep files (2), properties (16), asset URI (128). + "flags": 65682 if latest_prerelease else 147} + + +def catalog_modes(registry): + """Use an official client discovery fallback only after an absent version.""" + return ("query",) + {"cursor": ("latest-prerelease",), + "windsurf": ("latest-resource",)}.get(registry, ()) + + +def catalog_request(registry, manifest, mode): + """Describe anonymous supported client routes; latest is discovery, not admission.""" + if mode not in catalog_modes(registry): + raise ValueError("Unsupported catalog discovery mode") + publisher, name, _ = identity(manifest) + if mode == "latest-resource": + # Released Windsurf/Devin's ExtensionLatestVersionUriTemplate: + # /{publisher}/{name}/latest -> full IRawGalleryExtension. + return {"method": "GET", "url": GALLERY_URLS[registry].removesuffix("/extensionquery") + + f"/{publisher}/{name}/latest"} + return {"method": "POST", "url": GALLERY_URLS[registry], + "request": gallery_query(manifest, latest_prerelease=mode == "latest-prerelease")} + + +def catalog_response(raw, mode): + """Normalize the official resource envelope before the same exact-version checks.""" + metadata = json.loads(raw) + if mode == "latest-resource": + if not isinstance(metadata, dict): + raise ValueError("Latest-resource response must be full extension metadata") + metadata = {"results": [{"extensions": [metadata]}]} + return metadata def catalog_version(metadata, manifest, registry): @@ -163,28 +204,74 @@ def catalog_download(selected, manifest, registry): return url -def read_catalog(registry, manifest, root): - """Anonymous, bounded read-only POST; never inherit curl config or credentials.""" +def read_catalog(registry, manifest, root, mode="query"): + """Bounded anonymous read. HTTP errors, including rate limits, are never retried here.""" + spec = catalog_request(registry, manifest, mode) request = root / "query.json" response = root / "catalog.json" - request.write_text(json.dumps(gallery_query(manifest))) - result = subprocess.run([ - "curl", "--disable", "--fail", "--silent", "--show-error", "--proto", "=https", + headers = root / "headers.txt" + evidence = {**spec, "mode": mode, "requestCacheControl": "no-cache", + "checkedAt": datetime.now(UTC).isoformat()} + command = [ + "curl", "--disable", "--silent", "--show-error", "--proto", "=https", "--connect-timeout", "15", "--max-time", "60", "--max-filesize", str(8 * 1024 * 1024), - "--request", "POST", "--header", "Content-Type: application/json", - "--header", "Accept: application/json;api-version=7.2-preview.1", - "--header", "Cache-Control: no-cache", - "--data-binary", "@" + str(request), "--output", str(response), - "--write-out", "%{http_code}", GALLERY_URLS[registry], - ], text=True, capture_output=True, timeout=70, check=False) + "--request", spec["method"], "--header", "Content-Type: application/json", + "--header", "Accept: application/json;api-version=7.2-preview" + + ("" if mode == "latest-resource" else ".1"), + "--header", "Cache-Control: no-cache", "--dump-header", str(headers), + "--output", str(response), "--write-out", "%{http_code}", + ] + if "request" in spec: + request.write_text(json.dumps(spec["request"])) + command.extend(["--data-binary", "@" + str(request)]) + command.append(spec["url"]) + try: + result = subprocess.run(command, text=True, capture_output=True, timeout=70, check=False) + except subprocess.SubprocessError as error: + raise CatalogReadError("Anonymous catalog request did not complete", evidence) from error + raw = response.read_bytes() if response.exists() else b"" + evidence.update(httpStatus=result.stdout, curlCode=result.returncode, + responseSha256=hashlib.sha256(raw).hexdigest(), responseBytes=len(raw), + responseBody=raw.decode("utf-8", errors="replace")) + # Retain only Retry-After, never cookies or other incidental response headers. + if headers.exists(): + evidence["retryAfter"] = [line.partition(":")[2].strip() + for line in headers.read_text(errors="replace").splitlines() + if line.lower().startswith("retry-after:")] if result.returncode or result.stdout != "200": - raise ValueError(f"Anonymous catalog query failed: HTTP {result.stdout}; curl {result.returncode}") - raw = response.read_bytes() - metadata = json.loads(raw) - return metadata, {"url": GALLERY_URLS[registry], "request": gallery_query(manifest), - "requestCacheControl": "no-cache", - "responseSha256": hashlib.sha256(raw).hexdigest(), "responseBytes": len(raw), - "responseBody": raw.decode("utf-8")} + raise CatalogReadError( + f"Anonymous catalog request failed: HTTP {result.stdout}; curl {result.returncode}; " + "no automatic HTTP retry (honor any recorded Retry-After)", evidence) + try: + metadata = catalog_response(raw, mode) + except (ValueError, TypeError) as error: + raise CatalogReadError("Catalog response is not valid extension metadata", evidence) from error + return metadata, evidence + + +def discover_version(registry, manifest, root, attempts): + """Record every actual response; fallback only for an absent exact version.""" + for mode in catalog_modes(registry): + directory = root / mode + directory.mkdir() + try: + metadata, evidence = read_catalog(registry, manifest, directory, mode) + except CatalogReadError as error: + error.evidence.update(status="FAILED", error=str(error)) + attempts.append(error.evidence) + raise + attempts.append(evidence) + try: + selected = catalog_version(metadata, manifest, registry) + except NotIndexed as error: + evidence.update(status="NOT_INDEXED", error=str(error)) + continue + except (ValueError, KeyError, TypeError, AttributeError, IndexError) as error: + evidence.update(status="FAILED", error=str(error)) + raise + evidence.update(status="INDEXED", version=selected) + return selected + raise NotIndexed("Exact universal version is absent from all supported catalog discovery routes") def qualify(package, manifest): @@ -229,18 +316,13 @@ def main(): for attempt in range(args.attempts): directory = root / str(attempt) directory.mkdir() - metadata, evidence = read_catalog(args.registry, expected, directory) - evidence["checkedAt"] = datetime.now(UTC).isoformat() - result["attempts"].append(evidence) try: - selected = catalog_version(metadata, expected, args.registry) - except NotIndexed as error: - evidence.update(status="NOT_INDEXED", error=str(error)) + selected = discover_version(args.registry, expected, directory, result["attempts"]) + except NotIndexed: if attempt + 1 == args.attempts: raise time.sleep(args.delay) continue - evidence.update(status="INDEXED", version=selected) break url = catalog_download(selected, expected, args.registry) if args.registry == "openvsx": diff --git a/vs-code-terraphim-it/tests/fixtures/registries/README.md b/vs-code-terraphim-it/tests/fixtures/registries/README.md index 40e7d2d84..f3b0e9621 100644 --- a/vs-code-terraphim-it/tests/fixtures/registries/README.md +++ b/vs-code-terraphim-it/tests/fixtures/registries/README.md @@ -1 +1,7 @@ Public anonymous exact-ID catalog responses captured on 14 October 2026. Microsoft offered 0.1.3; Cursor and Windsurf offered 0.1.0. These are parser fixtures, not mocked services or evidence of a later publication. Live CI queries every registry again. + +`cursor-latest-prerelease-014.json` is the anonymous 15 October 2026 response to the same exact-ID query with flags `65682` (files, properties, asset URI and Microsoft's documented `IncludeLatestPrereleaseAndStableVersionOnly`). It offers prerelease 0.1.4 while the original flags-147 query still returned 0.1.0. The verifier retains both responses and still requires the exact qualified version and downloaded payload. + +The Windsurf latest-resource parser test reuses the full extension object from the captured Windsurf catalog response. It tests the official client's `IRawGalleryExtension` envelope and exact-version refusal; it does not claim a successful live latest-resource request. CI performs that request separately. + +Primary contracts: [Microsoft query flags](https://learn.microsoft.com/en-us/javascript/api/azure-devops-extension-api/extensionqueryflags?view=azdevops-ext-latest), and [`getLatestRawGalleryExtension` in the official VS Code client](https://github.com/microsoft/vscode/blob/main/src/vs/platform/extensionManagement/common/extensionGalleryService.ts). The released Windsurf/Devin client advertises `/{publisher}/{name}/latest`; latest metadata is only a discovery candidate, never permission to accept another version. diff --git a/vs-code-terraphim-it/tests/fixtures/registries/cursor-latest-prerelease-014.json b/vs-code-terraphim-it/tests/fixtures/registries/cursor-latest-prerelease-014.json new file mode 100644 index 000000000..7f7d3d8fe --- /dev/null +++ b/vs-code-terraphim-it/tests/fixtures/registries/cursor-latest-prerelease-014.json @@ -0,0 +1 @@ +{"results":[{"extensions":[{"extensionId":"01a1397c-abbb-7196-a7d0-5f21bae87581","extensionName":"terraphim-it","displayName":"Terraphim It","shortDescription":"Intelligent Markdown editor with alternatives, knowledge-graph synonyms and role-based suggestions","publisher":{"displayName":"applied-knowledge-systems","publisherId":"01a1397c-abbf-7660-af01-72a592315dce","publisherName":"applied-knowledge-systems","domain":"","isDomainVerified":false},"versions":[{"version":"0.1.4","lastUpdated":"2026-10-15T07:26:45.552045Z","assetUri":"https://marketplace.cursorapi.com/open-vsx-mirror/vscode/asset/applied-knowledge-systems/terraphim-it/0.1.4","fallbackAssetUri":"https://marketplace.cursorapi.com/open-vsx-mirror/vscode/asset/applied-knowledge-systems/terraphim-it/0.1.4","files":[{"assetType":"Microsoft.VisualStudio.Code.Manifest","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.4/file/package.json"},{"assetType":"Microsoft.VisualStudio.Services.Content.Details","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.4/file/readme.md"},{"assetType":"Microsoft.VisualStudio.Services.Content.License","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.4/file/LICENSE.txt"},{"assetType":"Microsoft.VisualStudio.Services.VSIXPackage","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.4/file/applied-knowledge-systems.terraphim-it-0.1.4.vsix"},{"assetType":"Microsoft.VisualStudio.Services.Content.Changelog","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.4/file/changelog.md"},{"assetType":"Microsoft.VisualStudio.Services.VsixManifest","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.4/file/extension.vsixmanifest"},{"assetType":"Microsoft.VisualStudio.Services.VsixSignature","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.4/file/applied-knowledge-systems.terraphim-it-0.1.4.sigzip"},{"assetType":"Microsoft.VisualStudio.Services.PublicKey","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/-/public-key/14ccb407-4e79-41ed-be5a-6d608325c45a"}],"properties":[{"key":"Microsoft.VisualStudio.Services.Branding.Color","value":""},{"key":"Microsoft.VisualStudio.Services.Branding.Theme","value":""},{"key":"Microsoft.VisualStudio.Services.Links.Source","value":"https://github.com/terraphim/terraphim-ai.git"},{"key":"Microsoft.VisualStudio.Code.SponsorLink","value":""},{"key":"Microsoft.VisualStudio.Code.Engine","value":"^1.126.0"},{"key":"Microsoft.VisualStudio.Code.ExtensionDependencies","value":""},{"key":"Microsoft.VisualStudio.Code.ExtensionPack","value":""},{"key":"Microsoft.VisualStudio.Code.LocalizedLanguages","value":""},{"key":"Microsoft.VisualStudio.Code.PreRelease","value":"true"}],"targetPlatform":"universal"}],"statistics":[],"tags":[],"releaseDate":"2026-10-14T08:17:11.286382Z","publishedDate":"2026-10-14T08:17:11.286382Z","lastUpdated":"2026-10-15T07:28:03.027714Z","categories":["AI","Other","Snippets"],"flags":""}],"resultMetadata":[{"metadataType":"ResultCount","metadataItems":[{"name":"TotalCount","count":1}]}]}]} \ No newline at end of file diff --git a/vs-code-terraphim-it/tests/verify_registry_test.py b/vs-code-terraphim-it/tests/verify_registry_test.py index 5b1c223d0..975329ad4 100644 --- a/vs-code-terraphim-it/tests/verify_registry_test.py +++ b/vs-code-terraphim-it/tests/verify_registry_test.py @@ -40,6 +40,63 @@ def test_real_stale_and_revalidated_catalogs_do_not_conflate_publication_with_in self.assertEqual(registry.catalog_download(selected, manifest, "openvsx"), "https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.4/file/applied-knowledge-systems.terraphim-it-0.1.4.vsix") + def test_captured_cursor_prerelease_query_exposes_exact_released_version(self): + manifest = {**verify_vsix_test.VsixTest.manifest, "version": "0.1.4"} + with self.assertRaises(registry.NotIndexed): + registry.catalog_version(self.fixture("cursor"), manifest, "cursor") + data = self.fixture("cursor-latest-prerelease-014") + selected = registry.catalog_version(data, manifest, "cursor") + self.assertEqual(selected["version"], "0.1.4") + self.assertIn("/0.1.4/", registry.catalog_download(selected, manifest, "cursor")) + # A latest-channel response cannot qualify a different frozen version. + with self.assertRaises(registry.NotIndexed): + registry.catalog_version(data, {**manifest, "version": "0.1.3"}, "cursor") + selected["properties"] = [{"key": registry.PRERELEASE_PROPERTY, "value": "false"}] + with self.assertRaisesRegex(ValueError, "prerelease channel"): + registry.catalog_version(data, manifest, "cursor") + + def test_supported_fallbacks_preserve_exact_identity_and_channel_metadata(self): + manifest = verify_vsix_test.VsixTest.manifest + self.assertEqual(registry.catalog_modes("marketplace"), ("query",)) + self.assertEqual(registry.catalog_modes("openvsx"), ("query",)) + self.assertEqual(registry.catalog_modes("cursor"), ("query", "latest-prerelease")) + self.assertEqual(registry.catalog_modes("windsurf"), ("query", "latest-resource")) + original = registry.catalog_request("cursor", manifest, "query") + latest = registry.catalog_request("cursor", manifest, "latest-prerelease") + self.assertEqual(original["request"]["flags"], 147) + self.assertEqual(latest["request"]["flags"], 65682) + self.assertEqual(original["request"]["filters"], latest["request"]["filters"]) + self.assertEqual(registry.catalog_request("windsurf", manifest, "latest-resource"), { + "method": "GET", + "url": "https://marketplace.windsurf.com/vscode/gallery/applied-knowledge-systems/terraphim-it/latest", + }) + for name, mode in [("marketplace", "latest-resource"), ("openvsx", "latest-prerelease"), + ("cursor", "latest-resource"), ("windsurf", "latest-prerelease")]: + with self.subTest(name=name, mode=mode), self.assertRaises(ValueError): + registry.catalog_request(name, manifest, mode) + for key, value in [("publisher", "../escape"), ("name", "x?token=x"), ("version", "latest")]: + with self.subTest(key=key), self.assertRaises(ValueError): + registry.catalog_request("windsurf", {**manifest, key: value}, "latest-resource") + + def test_resource_envelope_still_requires_full_exact_version_metadata(self): + # Same IRawGalleryExtension contract returned by the official latest + # resource route; use captured catalog metadata, not an invented package. + extension = self.fixture("windsurf")["results"][0]["extensions"][0] + manifest = verify_vsix_test.VsixTest.manifest + data = registry.catalog_response(json.dumps(extension), "latest-resource") + selected = registry.catalog_version(data, manifest, "windsurf") + self.assertIn("/0.1.0/", registry.catalog_download(selected, manifest, "windsurf")) + with self.assertRaises(registry.NotIndexed): + registry.catalog_version(data, {**manifest, "version": "0.1.4"}, "windsurf") + for raw in ["null", "[]", '"asset URL"']: + with self.subTest(raw=raw), self.assertRaises(ValueError): + registry.catalog_response(raw, "latest-resource") + for change in [{"publisher": {"publisherName": "other"}}, {"versions": []}, + {"versions": [{"version": "0.1.0", "files": selected["files"]}]}]: + data = registry.catalog_response(json.dumps({**extension, **change}), "latest-resource") + with self.subTest(change=change), self.assertRaises((ValueError, KeyError)): + registry.catalog_version(data, manifest, "windsurf") + def test_catalog_rejects_ambiguous_channel_platform_and_validation(self): original = self.fixture("marketplace") manifest = {**verify_vsix_test.VsixTest.manifest, "version": "0.1.3"}