From 36f69a8b982e0efa65311354bd6459d8b052cefd Mon Sep 17 00:00:00 2001 From: Dr Alexander Mikhalev Date: Wed, 14 Oct 2026 23:26:38 +0100 Subject: [PATCH 1/2] fix: verify extension catalog admission and trusted publishing Refs #3488 --- .../workflows/vscode-registry-readback.yml | 50 ++++++ .github/workflows/vscode-release.yml | 52 ++++-- vs-code-terraphim-it/README.md | 25 ++- .../scripts/download_registry_release.py | 90 ++++++++++ .../scripts/publish_openvsx.py | 65 ++----- .../scripts/verify_registry.py | 169 ++++++++++++++++-- .../tests/download_registry_release_test.py | 62 +++++++ .../tests/fixtures/registries/README.md | 1 + .../tests/fixtures/registries/cursor.json | 1 + .../tests/fixtures/registries/microsoft.json | 1 + .../tests/fixtures/registries/windsurf.json | 1 + .../tests/publish_openvsx_test.py | 46 ++--- .../tests/verify_registry_test.py | 64 ++++++- 13 files changed, 510 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/vscode-registry-readback.yml create mode 100644 vs-code-terraphim-it/scripts/download_registry_release.py create mode 100644 vs-code-terraphim-it/tests/download_registry_release_test.py create mode 100644 vs-code-terraphim-it/tests/fixtures/registries/README.md create mode 100644 vs-code-terraphim-it/tests/fixtures/registries/cursor.json create mode 100644 vs-code-terraphim-it/tests/fixtures/registries/microsoft.json create mode 100644 vs-code-terraphim-it/tests/fixtures/registries/windsurf.json diff --git a/.github/workflows/vscode-registry-readback.yml b/.github/workflows/vscode-registry-readback.yml new file mode 100644 index 000000000..452954b29 --- /dev/null +++ b/.github/workflows/vscode-registry-readback.yml @@ -0,0 +1,50 @@ +name: VS Code registry readback +on: + workflow_dispatch: + inputs: + tag: + description: Existing immutable VS Code release tag (no rebuild or publication) + type: string + required: true + default: terraphim_vscode-v0.1.3 + registry: + description: Catalog to verify; all runs four independent checks + type: choice + required: true + default: all + options: [all, marketplace, openvsx, cursor, windsurf] + +permissions: + contents: read + +jobs: + verify: + strategy: + fail-fast: false + matrix: + registry: [marketplace, openvsx, cursor, windsurf] + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Download and verify frozen released bytes + if: inputs.registry == 'all' || inputs.registry == matrix.registry + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.tag }} + run: python3 vs-code-terraphim-it/scripts/download_registry_release.py --tag "$RELEASE_TAG" --directory registry-input + - name: Verify exact catalog entry and actual package download + if: inputs.registry == 'all' || inputs.registry == matrix.registry + env: + REGISTRY: ${{ matrix.registry }} + run: python3 vs-code-terraphim-it/scripts/verify_registry.py --registry "$REGISTRY" --vsix registry-input/terraphim-it.vsix --manifest registry-input/release-manifest.json --receipt registry-result.json --attempts 20 --delay 30 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() && (inputs.registry == 'all' || inputs.registry == matrix.registry) + with: + name: registry-${{ matrix.registry }}-${{ github.run_id }}-${{ github.run_attempt }} + path: | + registry-result.json + registry-input/release-readback.json + if-no-files-found: error diff --git a/.github/workflows/vscode-release.yml b/.github/workflows/vscode-release.yml index a7d8e343d..af68b1cbd 100644 --- a/.github/workflows/vscode-release.yml +++ b/.github/workflows/vscode-release.yml @@ -161,23 +161,51 @@ jobs: run: npm exec -- vsce publish --azure-credential --packagePath terraphim-it.vsix --pre-release - name: Publish exact VSIX to Open VSX with trusted publishing continue-on-error: true - if: matrix.registry == 'open-vsx' && vars.OVSX_BOOTSTRAP_FIRST_PUBLISH != 'true' + if: matrix.registry == 'open-vsx' run: python3 scripts/publish_openvsx.py --vsix terraphim-it.vsix --manifest release-manifest.json - - name: Bootstrap the first Open VSX version explicitly - continue-on-error: true - if: matrix.registry == 'open-vsx' && vars.OVSX_BOOTSTRAP_FIRST_PUBLISH == 'true' - env: - OVSX_BOOTSTRAP_PAT: ${{ secrets.OVSX_BOOTSTRAP_PAT }} - # Open VSX cannot register a trusted publisher before an active version - # exists. Only this environment setting enables the one-time bootstrap; - # a failed OIDC attempt never falls back to a stored token. - run: python3 scripts/publish_openvsx.py --bootstrap --vsix terraphim-it.vsix --manifest release-manifest.json - name: Verify exact published registry payload env: REGISTRY: ${{ matrix.registry }} run: | - if [[ "$REGISTRY" == visual-studio-marketplace ]]; then registry=marketplace; else registry=openvsx; fi - python3 scripts/verify_registry.py --registry "$registry" --vsix terraphim-it.vsix --manifest release-manifest.json --receipt registry.json + if [[ "$REGISTRY" == visual-studio-marketplace ]]; then + registry=marketplace; extra=() + else + registry=openvsx; extra=(--require-trusted-publishing) + fi + python3 scripts/verify_registry.py --registry "$registry" --vsix terraphim-it.vsix --manifest release-manifest.json --receipt registry.json --attempts 20 --delay 30 "${extra[@]}" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: vscode-registry-${{ matrix.registry }}-${{ github.sha }} + path: vs-code-terraphim-it/registry.json + if-no-files-found: error + + downstream-catalogs: + needs: [release, marketplace-plan, marketplace] + if: ${{ always() && needs.release.result == 'success' && contains(needs.marketplace-plan.outputs.registries, 'open-vsx') }} + strategy: + fail-fast: false + matrix: + registry: [cursor, windsurf] + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + defaults: + run: + working-directory: vs-code-terraphim-it + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: vscode-package-${{ github.sha }} + path: vs-code-terraphim-it + - name: Verify downstream catalog and its actual download separately + env: + REGISTRY: ${{ matrix.registry }} + run: python3 scripts/verify_registry.py --registry "$REGISTRY" --vsix terraphim-it.vsix --manifest release-manifest.json --receipt registry.json --attempts 20 --delay 30 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 if: always() with: diff --git a/vs-code-terraphim-it/README.md b/vs-code-terraphim-it/README.md index d7f0d4429..053685fc5 100644 --- a/vs-code-terraphim-it/README.md +++ b/vs-code-terraphim-it/README.md @@ -102,7 +102,7 @@ Endpoint and agent changes take effect on the next Atomic request, including an ## Getting Started -1. **Install Terraphim It** from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=applied-knowledge-systems.terraphim-it) or [Open VSX](https://open-vsx.org/extension/applied-knowledge-systems/terraphim-it). Compatible Cursor installations use the Open VSX package. +1. **Install Terraphim It** from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=applied-knowledge-systems.terraphim-it) or [Open VSX](https://open-vsx.org/extension/applied-knowledge-systems/terraphim-it). Cursor and Windsurf discover Open VSX packages through their own catalogs; availability and indexing can differ. The [GitHub release](https://github.com/terraphim/terraphim-ai/releases?q=terraphim_vscode) also provides the exact VSIX for a manual install. 2. **Configure your server** settings in VS Code preferences 3. **Start your Terraphim server** (if using local server): ```bash @@ -218,3 +218,26 @@ For issues and questions: ## License Apache License 2.0 — see LICENSE.txt. + +## Release and catalog verification + +Tagged releases build one VSIX, qualify that exact package in native Linux, macOS +and Windows hosts, and publish those frozen bytes. Microsoft publication uses +GitHub-to-Azure federation; Open VSX uses its trusted-publishing OIDC exchange. +The Open VSX release gate also requires its exact-version metadata to attest +`publishedWithTrustedPublishing: true`; a matching manually published package +cannot substitute for that proof. Open VSX publication stays disabled until namespace ownership and its trusted +publisher are configured. There is no PAT bootstrap or fallback. + +CI requires an exact prerelease entry in the selected catalog **and** compares +every downloaded `extension/*` file with the qualified VSIX. Cursor and Windsurf +have independent catalog/download receipts after Open VSX publication; an Open +VSX upload alone does not establish their availability. These receipts verify +catalog/package identity, not native functionality in those two hosts. + +The read-only **VS Code registry readback** workflow can verify an existing +immutable release against any of the four catalogs. It downloads the published +VSIX, checks the release asset digests, checksums, qualification manifest and tag +commit, and performs fresh anonymous catalog/download checks. It never rebuilds, +reuploads or changes a release. A missing version fails the selected check and +retains its failure receipt. Later indexing can be checked by rerunning readback. diff --git a/vs-code-terraphim-it/scripts/download_registry_release.py b/vs-code-terraphim-it/scripts/download_registry_release.py new file mode 100644 index 000000000..c765ff2c6 --- /dev/null +++ b/vs-code-terraphim-it/scripts/download_registry_release.py @@ -0,0 +1,90 @@ +"""Read an immutable public release for catalog checks; never build or publish.""" +import argparse +import json +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +from publish_release import ( + anonymous_download, + api, + checksums, + digest, + verify_remote_tag, +) +from verify_registry import qualify +from verify_vsix import check_tag + +REPOSITORY = "terraphim/terraphim-ai" +ASSETS = {"terraphim-it.vsix", "release-manifest.json", "SHA256SUMS"} + + +def release_assets(metadata, tag): + """Bind anonymous asset URLs to the exact public published preview release.""" + if (not re.fullmatch(r"terraphim_vscode-v\d+\.\d+\.\d+", tag) + or metadata.get("tag_name") != tag or metadata.get("draft") is not False + or metadata.get("prerelease") is not True): + raise ValueError("Expected exact published VS Code prerelease") + assets = metadata["assets"] + if len(assets) != len(ASSETS) or {item["name"] for item in assets} != ASSETS: + raise ValueError("Release must have exactly the three qualified assets") + for item in assets: + url = f"https://github.com/{REPOSITORY}/releases/download/{tag}/{item['name']}" + if (item.get("browser_download_url") != url or item.get("state") != "uploaded" + or type(item.get("size")) is not int or not 0 < item["size"] <= 128 * 1024 * 1024 + or not re.fullmatch(r"sha256:[0-9a-f]{64}", item.get("digest", ""))): + raise ValueError("Release asset URL, state, size or GitHub digest is invalid") + return assets + + +def verify_downloads(root, assets, tag): + """Verify hashes, archive identity and manifest before returning downloaded bytes.""" + for item in assets: + path = root / item["name"] + if path.stat().st_size != item["size"] or "sha256:" + digest(path) != item["digest"]: + raise ValueError("Downloaded release asset differs from GitHub digest") + package, manifest = root / "terraphim-it.vsix", root / "release-manifest.json" + expected, report = qualify(package, manifest) + check_tag(tag, expected["version"]) + found = checksums((root / "SHA256SUMS").read_text()) + wanted = {package.name: digest(package)} + if found not in (wanted, {**wanted, manifest.name: digest(manifest)}): + raise ValueError("Release checksums differ from downloaded package") + return report + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--tag", required=True) + parser.add_argument("--directory", type=Path, required=True) + args = parser.parse_args() + try: + if not re.fullmatch(r"terraphim_vscode-v\d+\.\d+\.\d+", args.tag): + raise ValueError("Invalid immutable VS Code release tag") + if args.directory.exists() or args.directory.is_symlink(): + raise ValueError("Choose a new output directory") + metadata = api(f"repos/{REPOSITORY}/releases/tags/{args.tag}") + assets = release_assets(metadata, args.tag) + with tempfile.TemporaryDirectory(prefix="terraphim-registry-release-") as directory: + root = Path(directory) + for item in assets: + anonymous_download(item["browser_download_url"], root / item["name"]) + report = verify_downloads(root, assets, args.tag) + verify_remote_tag(REPOSITORY, args.tag, report["sourceCommit"]) + args.directory.mkdir(parents=True) + for item in assets: + (args.directory / item["name"]).write_bytes((root / item["name"]).read_bytes()) + receipt = {"schema": 1, "status": "RELEASE_BYTES_VERIFIED", "repository": REPOSITORY, + "tag": args.tag, "sourceCommit": report["sourceCommit"], "assets": assets, + "scope": "Read-only published artifact recovery; no rebuild, publication or new native qualification"} + (args.directory / "release-readback.json").write_text(json.dumps(receipt, indent=2) + "\n") + except (ValueError, KeyError, TypeError, OSError, subprocess.SubprocessError) as error: + print(f"download_registry_release: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vs-code-terraphim-it/scripts/publish_openvsx.py b/vs-code-terraphim-it/scripts/publish_openvsx.py index 5a9271853..3caff5eef 100644 --- a/vs-code-terraphim-it/scripts/publish_openvsx.py +++ b/vs-code-terraphim-it/scripts/publish_openvsx.py @@ -1,75 +1,36 @@ -#!/usr/bin/env python3 -"""Publish qualified bytes using explicit Open VSX first-version or OIDC mode.""" +"""Publish qualified bytes only through Open VSX trusted publishing.""" import argparse -import json import os -from pathlib import Path import subprocess import sys -from urllib.error import HTTPError -from urllib.request import Request, urlopen - -from verify_registry import identity, qualify - +from pathlib import Path -def require_first_publication(status, metadata, manifest): - """Only the registry's exact missing-extension response permits bootstrap.""" - publisher, name, _ = identity(manifest) - expected = f"Extension not found: {publisher}.{name}" - if status != 404 or not isinstance(metadata, dict) or metadata.get("error") != expected: - raise ValueError("Open VSX bootstrap requires an absent extension; use trusted publishing for existing versions") +from verify_registry import qualify -def publication_environment(bootstrap, environment): - """Keep the scoped credential in the child environment, never CLI arguments.""" +def publication_environment(environment): + """Remove PAT overrides: a failed OIDC exchange must fail closed.""" child = dict(environment) - token = child.pop("OVSX_BOOTSTRAP_PAT", "") - # ovsx gives PATs precedence over OIDC; neither inherited credentials nor a - # failed OIDC exchange may silently select a different publication method. child.pop("OVSX_PAT", None) - if bootstrap: - if not token.strip(): - raise ValueError("Explicit bootstrap requires the environment secret OVSX_BOOTSTRAP_PAT") - child["OVSX_PAT"] = token + child.pop("OVSX_BOOTSTRAP_PAT", None) return child -def publication_command(package, bootstrap): - """Construct a shell-free command using only the qualified package path.""" - command = ["npm", "exec", "--", "ovsx", "publish", str(package.resolve()), "--pre-release"] - if not bootstrap: - command.append("--trusted-publishing") - return command - - -def registry_metadata(manifest): - publisher, name, _ = identity(manifest) - request = Request(f"https://open-vsx.org/api/{publisher}/{name}", - headers={"Accept": "application/json", "User-Agent": "Terraphim-release-verifier/1.0"}) - try: - with urlopen(request, timeout=60) as response: - status, body = response.status, response.read() - except HTTPError as error: - status, body = error.code, error.read() - try: - return status, json.loads(body) - except ValueError as error: - raise ValueError("Open VSX returned invalid metadata; publication refused") from error +def publication_command(package): + """Construct a shell-free OIDC command using only qualified package bytes.""" + return ["npm", "exec", "--", "ovsx", "publish", str(package.resolve()), + "--pre-release", "--trusted-publishing"] def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--vsix", type=Path, required=True) parser.add_argument("--manifest", type=Path, required=True) - parser.add_argument("--bootstrap", action="store_true", - help="Explicit first-version PAT bootstrap; never an automatic OIDC fallback") args = parser.parse_args() try: - manifest, _ = qualify(args.vsix, args.manifest) - command = publication_command(args.vsix, args.bootstrap) - environment = publication_environment(args.bootstrap, os.environ) - if args.bootstrap: - require_first_publication(*registry_metadata(manifest), manifest) + qualify(args.vsix, args.manifest) + command = publication_command(args.vsix) + environment = publication_environment(os.environ) # ubs:ignore -- False positive: credential flows to env only; fixed argv and shell=False. subprocess.run(command, env=environment, check=True, shell=False, timeout=180) except (ValueError, OSError, subprocess.SubprocessError) as error: diff --git a/vs-code-terraphim-it/scripts/verify_registry.py b/vs-code-terraphim-it/scripts/verify_registry.py index 2ae7a367a..960738bc5 100644 --- a/vs-code-terraphim-it/scripts/verify_registry.py +++ b/vs-code-terraphim-it/scripts/verify_registry.py @@ -1,20 +1,35 @@ -#!/usr/bin/env python3 """Verify one exact registry version against the qualified extension payload.""" import argparse import hashlib import hmac import json -from pathlib import Path import re import subprocess import sys import tempfile -from urllib.parse import urlparse +import time import zipfile +from datetime import UTC, datetime +from pathlib import Path +from urllib.parse import urlparse from xml.etree.ElementTree import ParseError -from publish_release import SHA_RE, anonymous_download, digest import verify_vsix +from publish_release import SHA_RE, anonymous_download, digest + +GALLERY_URLS = { + "marketplace": "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery", + "openvsx": "https://open-vsx.org/vscode/gallery/extensionquery", + "cursor": "https://marketplace.cursorapi.com/_apis/public/gallery/extensionquery", + "windsurf": "https://marketplace.windsurf.com/vscode/gallery/extensionquery", +} +VSIX_ASSET = "Microsoft.VisualStudio.Services.VSIXPackage" +PRERELEASE_PROPERTY = "Microsoft.VisualStudio.Code.PreRelease" + + +class NotIndexed(ValueError): + """The exact qualified version is not yet offered in this catalog.""" + IDENTIFIER_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*") @@ -79,6 +94,97 @@ def openvsx_download(metadata, manifest): return url +def require_trusted_publication(metadata): + """Open VSX must attest OIDC publication; byte equality alone is insufficient.""" + if metadata.get("publishedWithTrustedPublishing") is not True: + 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.""" + publisher, name, _ = identity(manifest) + return {"filters": [{"criteria": [{"filterType": 7, "value": f"{publisher}.{name}"}], + "pageNumber": 1, "pageSize": 100}], "flags": 147} + + +def catalog_version(metadata, manifest, registry): + """Reject missing, ambiguous, wrong-channel or non-public catalog entries.""" + publisher, name, version = identity(manifest) + matches = [entry for result in metadata["results"] for entry in result["extensions"] + if (entry["publisher"]["publisherName"], entry["extensionName"]) == (publisher, name)] + if not matches: + raise NotIndexed("Exact extension is absent from catalog") + if len(matches) != 1: + raise ValueError("Ambiguous exact extension in catalog") + extension = matches[0] + versions = [item for item in extension["versions"] if item["version"] == version + and item.get("targetPlatform") in (None, "universal")] + if not versions: + raise NotIndexed("Exact universal version is absent from catalog") + if len(versions) != 1: + raise ValueError("Ambiguous exact universal version in catalog") + selected = versions[0] + properties = [item["value"] for item in selected["properties"] if item["key"] == PRERELEASE_PROPERTY] + if properties != ["true"]: + raise ValueError("Catalog version is not the qualified prerelease channel") + if registry == "marketplace": + if not {"public", "validated"} <= {flag.strip() for flag in extension.get("flags", "").split(",")}: + raise NotIndexed("Microsoft extension has not passed public validation") + if "validated" not in {flag.strip() for flag in selected.get("flags", "").split(",")}: + raise NotIndexed("Microsoft version has not passed validation") + return selected + + +def catalog_download(selected, manifest, registry): + """Only admit the registry's actual exact-version VSIX URL on known origins.""" + publisher, name, version = identity(manifest) + sources = [item["source"] for item in selected["files"] if item["assetType"] == VSIX_ASSET] + if len(sources) != 1: + raise ValueError("Catalog must advertise exactly one VSIX download") + url = sources[0] + parsed = urlparse(url) + if (parsed.scheme != "https" or parsed.username or parsed.password or parsed.query or parsed.fragment): + raise ValueError("Unsafe catalog download URL") + prefix = f"/api/{publisher}/{name}/{version}/file/" + origins = { + "openvsx": {"open-vsx.org": [prefix]}, + "cursor": {"marketplace.cursorapi.com": ["/open-vsx-mirror" + prefix]}, + "windsurf": {"open-vsx.org": [prefix], "marketplace.windsurf.com": [prefix, "/open-vsx.org" + prefix]}, + } + if registry == "marketplace": + allowed = (parsed.netloc == f"{publisher}.gallerycdn.vsassets.io" + and re.fullmatch(rf"/extensions/{re.escape(publisher)}/{re.escape(name)}/{re.escape(version)}/[0-9]+/{re.escape(VSIX_ASSET)}", parsed.path)) + else: + allowed = any(parsed.path.startswith(value) and parsed.path[len(value):].endswith(".vsix") + and re.fullmatch(r"[A-Za-z0-9_.-]+", parsed.path[len(value):]) + for value in origins.get(registry, {}).get(parsed.netloc, [])) + if not allowed: + raise ValueError("Unexpected catalog VSIX origin or version path") + return url + + +def read_catalog(registry, manifest, root): + """Anonymous, bounded read-only POST; never inherit curl config or credentials.""" + 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", + "--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", + "--data-binary", "@" + str(request), "--output", str(response), + "--write-out", "%{http_code}", GALLERY_URLS[registry], + ], text=True, capture_output=True, timeout=70, check=False) + 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), + "responseSha256": hashlib.sha256(raw).hexdigest(), "responseBytes": len(raw), + "responseBody": raw.decode("utf-8")} + + def qualify(package, manifest): report = json.loads(manifest.read_text()) expected, actual, _ = payload(package) @@ -91,37 +197,66 @@ def qualify(package, manifest): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--registry", choices=["marketplace", "openvsx"], required=True) + parser.add_argument("--registry", choices=list(GALLERY_URLS), required=True) parser.add_argument("--vsix", type=Path, required=True) parser.add_argument("--manifest", type=Path, required=True) parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--attempts", type=int, default=1, help="Bounded catalog-index attempts (1–20)") + parser.add_argument("--delay", type=int, default=30, help="Seconds between absent-index reads (0–30)") + parser.add_argument("--require-trusted-publishing", action="store_true", + help="Require Open VSX to attest OIDC for this exact version") args = parser.parse_args() - result = {"schema": 1, "registry": args.registry, "status": "FAILED"} + result = {"schema": 2, "registry": args.registry, "status": "FAILED", "attempts": [], + "scope": "Exact catalog version and downloaded payload; no native host/functionality claim"} try: if args.receipt.exists() or args.receipt.is_symlink(): raise ValueError("Choose a new receipt path to retain prior registry evidence") + if not 1 <= args.attempts <= 20 or not 0 <= args.delay <= 30: + raise ValueError("Catalog retry bounds exceeded") + if args.require_trusted_publishing and args.registry != "openvsx": + raise ValueError("Trusted-publication metadata is supported only by Open VSX") expected, report = qualify(args.vsix, args.manifest) - url = registry_url(args.registry, expected) + result.update(extension=report["extension"], version=report["version"], + sourceCommit=report["sourceCommit"], qualifiedSha256=report["sha256"]) with tempfile.TemporaryDirectory(prefix="terraphim-registry-") as folder: root = Path(folder) - # Hold the qualified bytes stable throughout network verification. local = root / "qualified.vsix" local.write_bytes(args.vsix.read_bytes()) if not hmac.compare_digest(digest(local), report["sha256"]): raise ValueError("Qualified package changed before registry verification") + 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)) + 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": - metadata = json.loads(anonymous_download(url, root / "registry.json")) - url = openvsx_download(metadata, expected) + metadata = json.loads(anonymous_download(registry_url(args.registry, expected), root / "registry.json")) + result["registryMetadata"] = metadata + result["publishedWithTrustedPublishing"] = metadata.get("publishedWithTrustedPublishing") + if args.require_trusted_publishing: + require_trusted_publication(metadata) + if openvsx_download(metadata, expected) != url: + raise ValueError("Open VSX catalog and exact metadata downloads differ") remote = root / "published.vsix" anonymous_download(url, remote) files = compare_payloads(local, remote) - result = {"schema": 1, "status": "REGISTRY_PAYLOAD_VERIFIED", "registry": args.registry, - "extension": report["extension"], "version": report["version"], - "sourceCommit": report["sourceCommit"], "downloadUrl": url, - "qualifiedSha256": report["sha256"], "registrySha256": digest(remote), - "comparison": "Every extension/* file; outer signing/container metadata may differ", - "payloadFiles": files} - except (ValueError, KeyError, TypeError, OSError, zipfile.BadZipFile, ParseError, subprocess.CalledProcessError) as error: + result.update(status="REGISTRY_PAYLOAD_VERIFIED", catalogIndexed=True, + downloadUrl=url, registrySha256=digest(remote), + comparison="Every extension/* file; outer signing/container metadata may differ", + payloadFiles=files) + except (ValueError, KeyError, TypeError, AttributeError, IndexError, OSError, zipfile.BadZipFile, ParseError, subprocess.SubprocessError) as error: result["error"] = str(error) print(f"verify_registry: {error}", file=sys.stderr) if not args.receipt.exists() and not args.receipt.is_symlink(): diff --git a/vs-code-terraphim-it/tests/download_registry_release_test.py b/vs-code-terraphim-it/tests/download_registry_release_test.py new file mode 100644 index 000000000..9f3fa7c06 --- /dev/null +++ b/vs-code-terraphim-it/tests/download_registry_release_test.py @@ -0,0 +1,62 @@ +"""Immutable-release admission tests using actual ZIP/files, without mocked services.""" +import copy +import json +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +import download_registry_release as download +import verify_registry +import verify_vsix_test + + +class ReleaseDownloadTest(unittest.TestCase): + tag = "terraphim_vscode-v0.1.0" + + def fixture(self, root): + package, _ = verify_vsix_test.VsixTest().package(root) + package.rename(root / "terraphim-it.vsix") + _, report, _ = verify_registry.payload(root / "terraphim-it.vsix") + report.update(sourceCommit="a" * 40, sourceTreeDirty=False) + (root / "release-manifest.json").write_text(json.dumps(report)) + (root / "SHA256SUMS").write_text(download.digest(root / "terraphim-it.vsix") + " terraphim-it.vsix\n") + assets = [{"name": name, "state": "uploaded", "size": (root / name).stat().st_size, + "digest": "sha256:" + download.digest(root / name), + "browser_download_url": f"https://github.com/{download.REPOSITORY}/releases/download/{self.tag}/{name}"} + for name in sorted(download.ASSETS)] + return {"tag_name": self.tag, "draft": False, "prerelease": True, "assets": assets} + + def test_exact_release_bytes_pass_and_any_asset_mutation_fails(self): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + metadata = self.fixture(root) + assets = download.release_assets(metadata, self.tag) + self.assertEqual(download.verify_downloads(root, assets, self.tag)["version"], "0.1.0") + for name in download.ASSETS: + path = root / name + original = path.read_bytes() + path.write_bytes(original + b"modified") + with self.assertRaises(ValueError): + download.verify_downloads(root, assets, self.tag) + path.write_bytes(original) + + def test_draft_wrong_tag_channel_url_digest_duplicates_fail_closed(self): + with tempfile.TemporaryDirectory() as folder: + original = self.fixture(Path(folder)) + for mutation in ["draft", "tag", "channel", "url", "digest", "duplicate", "size"]: + value = copy.deepcopy(original) + if mutation == "draft": value["draft"] = True + elif mutation == "tag": value["tag_name"] = "other" + elif mutation == "channel": value["prerelease"] = False + elif mutation == "url": value["assets"][0]["browser_download_url"] = "https://other.invalid/a" + elif mutation == "digest": value["assets"][0]["digest"] = None + elif mutation == "duplicate": value["assets"][0] = value["assets"][1] + else: value["assets"][0]["size"] = True + with self.subTest(mutation=mutation), self.assertRaises((ValueError, TypeError)): + download.release_assets(value, self.tag) + + +if __name__ == "__main__": + unittest.main() diff --git a/vs-code-terraphim-it/tests/fixtures/registries/README.md b/vs-code-terraphim-it/tests/fixtures/registries/README.md new file mode 100644 index 000000000..40e7d2d84 --- /dev/null +++ b/vs-code-terraphim-it/tests/fixtures/registries/README.md @@ -0,0 +1 @@ +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. diff --git a/vs-code-terraphim-it/tests/fixtures/registries/cursor.json b/vs-code-terraphim-it/tests/fixtures/registries/cursor.json new file mode 100644 index 000000000..6c86fdbde --- /dev/null +++ b/vs-code-terraphim-it/tests/fixtures/registries/cursor.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.0","lastUpdated":"2026-10-14T08:17:11.286382Z","assetUri":"https://marketplace.cursorapi.com/open-vsx-mirror/vscode/asset/applied-knowledge-systems/terraphim-it/0.1.0","fallbackAssetUri":"https://marketplace.cursorapi.com/open-vsx-mirror/vscode/asset/applied-knowledge-systems/terraphim-it/0.1.0","files":[{"assetType":"Microsoft.VisualStudio.Code.Manifest","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.0/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.0/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.0/file/LICENSE.txt"},{"assetType":"Microsoft.VisualStudio.Services.VSIXPackage","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.0/file/applied-knowledge-systems.terraphim-it-0.1.0.vsix"},{"assetType":"Microsoft.VisualStudio.Services.Content.Changelog","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.0/file/changelog.md"},{"assetType":"Microsoft.VisualStudio.Services.VsixManifest","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.0/file/extension.vsixmanifest"},{"assetType":"Microsoft.VisualStudio.Services.VsixSignature","source":"https://marketplace.cursorapi.com/open-vsx-mirror/api/applied-knowledge-systems/terraphim-it/0.1.0/file/applied-knowledge-systems.terraphim-it-0.1.0.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-14T08:20:45.453003Z","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/fixtures/registries/microsoft.json b/vs-code-terraphim-it/tests/fixtures/registries/microsoft.json new file mode 100644 index 000000000..01b89657d --- /dev/null +++ b/vs-code-terraphim-it/tests/fixtures/registries/microsoft.json @@ -0,0 +1 @@ +{"results":[{"extensions":[{"publisher":{"publisherId":"b5e30463-6dc6-4085-aa7e-284c80a7be4e","publisherName":"applied-knowledge-systems","displayName":"Applied Knowledge Systems","flags":"verified","domain":null,"isDomainVerified":false},"extensionId":"d2100091-b70e-425b-95ad-87796e0e34e3","extensionName":"terraphim-it","displayName":"Terraphim It","flags":"validated, public","lastUpdated":"2026-10-14T19:39:11.587Z","publishedDate":"2026-10-14T15:14:45.46Z","releaseDate":"2026-10-14T15:14:45.46Z","shortDescription":"Intelligent Markdown editor with alternatives, knowledge-graph synonyms and role-based suggestions","versions":[{"version":"0.1.3","flags":"validated","lastUpdated":"2026-10-14T19:39:11.583Z","files":[{"assetType":"Microsoft.VisualStudio.Code.Manifest","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532/Microsoft.VisualStudio.Code.Manifest"},{"assetType":"Microsoft.VisualStudio.Services.Content.Changelog","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532/Microsoft.VisualStudio.Services.Content.Changelog"},{"assetType":"Microsoft.VisualStudio.Services.Content.Details","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532/Microsoft.VisualStudio.Services.Content.Details"},{"assetType":"Microsoft.VisualStudio.Services.Content.License","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532/Microsoft.VisualStudio.Services.Content.License"},{"assetType":"Microsoft.VisualStudio.Services.VsixManifest","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532/Microsoft.VisualStudio.Services.VsixManifest"},{"assetType":"Microsoft.VisualStudio.Services.VSIXPackage","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532/Microsoft.VisualStudio.Services.VSIXPackage"},{"assetType":"Microsoft.VisualStudio.Services.VsixSignature","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532/Microsoft.VisualStudio.Services.VsixSignature"}],"properties":[{"key":"Microsoft.VisualStudio.Services.Links.Getstarted","value":"https://github.com/terraphim/terraphim-ai.git"},{"key":"Microsoft.VisualStudio.Services.Links.Support","value":"https://github.com/terraphim/terraphim-ai/issues"},{"key":"Microsoft.VisualStudio.Services.Links.Learn","value":"https://github.com/terraphim/terraphim-ai#readme"},{"key":"Microsoft.VisualStudio.Services.Links.Source","value":"https://github.com/terraphim/terraphim-ai.git"},{"key":"Microsoft.VisualStudio.Services.Links.GitHub","value":"https://github.com/terraphim/terraphim-ai.git"},{"key":"Microsoft.VisualStudio.Code.Engine","value":"^1.126.0"},{"key":"Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown","value":"true"},{"key":"Microsoft.VisualStudio.Code.ExtensionDependencies","value":""},{"key":"Microsoft.VisualStudio.Code.ExtensionPack","value":""},{"key":"Microsoft.VisualStudio.Code.LocalizedLanguages","value":""},{"key":"Microsoft.VisualStudio.Code.ExtensionKind","value":"workspace"},{"key":"Microsoft.VisualStudio.Code.PreRelease","value":"true"},{"key":"Microsoft.VisualStudio.Services.Content.Pricing","value":"Free"},{"key":"Microsoft.VisualStudio.Code.EnabledApiProposals","value":""},{"key":"Microsoft.VisualStudio.Code.ExecutesCode","value":"true"},{"key":"Microsoft.VisualStudio.Services.VsixSha256","value":"f932b51c304729dfc3603e04cc52c2588a5e83412e7d347033a8957bb9bfd4b7"}],"assetUri":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.3/1792006330532","fallbackAssetUri":"https://applied-knowledge-systems.gallery.vsassets.io/_apis/public/gallery/publisher/applied-knowledge-systems/extension/terraphim-it/0.1.3/assetbyname"},{"version":"0.1.2","flags":"validated","lastUpdated":"2026-10-14T15:24:48.31Z","files":[{"assetType":"Microsoft.VisualStudio.Code.Manifest","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647/Microsoft.VisualStudio.Code.Manifest"},{"assetType":"Microsoft.VisualStudio.Services.Content.Changelog","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647/Microsoft.VisualStudio.Services.Content.Changelog"},{"assetType":"Microsoft.VisualStudio.Services.Content.Details","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647/Microsoft.VisualStudio.Services.Content.Details"},{"assetType":"Microsoft.VisualStudio.Services.Content.License","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647/Microsoft.VisualStudio.Services.Content.License"},{"assetType":"Microsoft.VisualStudio.Services.VsixManifest","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647/Microsoft.VisualStudio.Services.VsixManifest"},{"assetType":"Microsoft.VisualStudio.Services.VSIXPackage","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647/Microsoft.VisualStudio.Services.VSIXPackage"},{"assetType":"Microsoft.VisualStudio.Services.VsixSignature","source":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647/Microsoft.VisualStudio.Services.VsixSignature"}],"properties":[{"key":"Microsoft.VisualStudio.Services.Links.Getstarted","value":"https://github.com/terraphim/terraphim-ai.git"},{"key":"Microsoft.VisualStudio.Services.Links.Support","value":"https://github.com/terraphim/terraphim-ai/issues"},{"key":"Microsoft.VisualStudio.Services.Links.Learn","value":"https://github.com/terraphim/terraphim-ai#readme"},{"key":"Microsoft.VisualStudio.Services.Links.Source","value":"https://github.com/terraphim/terraphim-ai.git"},{"key":"Microsoft.VisualStudio.Services.Links.GitHub","value":"https://github.com/terraphim/terraphim-ai.git"},{"key":"Microsoft.VisualStudio.Code.Engine","value":"^1.126.0"},{"key":"Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown","value":"true"},{"key":"Microsoft.VisualStudio.Code.ExtensionDependencies","value":""},{"key":"Microsoft.VisualStudio.Code.ExtensionPack","value":""},{"key":"Microsoft.VisualStudio.Code.LocalizedLanguages","value":""},{"key":"Microsoft.VisualStudio.Code.ExtensionKind","value":"workspace"},{"key":"Microsoft.VisualStudio.Code.PreRelease","value":"true"},{"key":"Microsoft.VisualStudio.Services.Content.Pricing","value":"Free"},{"key":"Microsoft.VisualStudio.Code.EnabledApiProposals","value":""},{"key":"Microsoft.VisualStudio.Code.ExecutesCode","value":"true"},{"key":"Microsoft.VisualStudio.Services.VsixSha256","value":"e9ed2e2a6a68245e59ffc8ce4db13f3cfcae3443b929fcba1d4384a7b477e403"}],"assetUri":"https://applied-knowledge-systems.gallerycdn.vsassets.io/extensions/applied-knowledge-systems/terraphim-it/0.1.2/1791990885647","fallbackAssetUri":"https://applied-knowledge-systems.gallery.vsassets.io/_apis/public/gallery/publisher/applied-knowledge-systems/extension/terraphim-it/0.1.2/assetbyname"}],"versionsTruncated":false,"deploymentType":0}],"pagingToken":null,"resultMetadata":[{"metadataType":"ResultCount","metadataItems":[{"name":"TotalCount","count":1}]}]}]} \ No newline at end of file diff --git a/vs-code-terraphim-it/tests/fixtures/registries/windsurf.json b/vs-code-terraphim-it/tests/fixtures/registries/windsurf.json new file mode 100644 index 000000000..a65744710 --- /dev/null +++ b/vs-code-terraphim-it/tests/fixtures/registries/windsurf.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":null,"isDomainVerified":null},"versions":[{"version":"0.1.0","lastUpdated":"2026-10-14T08:17:11.286382Z","assetUri":"https://marketplace.windsurf.com/open-vsx.org/vscode/asset/applied-knowledge-systems/terraphim-it/0.1.0","fallbackAssetUri":"https://marketplace.windsurf.com/open-vsx.org/vscode/asset/applied-knowledge-systems/terraphim-it/0.1.0","files":[{"assetType":"Microsoft.VisualStudio.Code.Manifest","source":"https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.0/file/package.json"},{"assetType":"Microsoft.VisualStudio.Services.Content.Details","source":"https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.0/file/readme.md"},{"assetType":"Microsoft.VisualStudio.Services.Content.License","source":"https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.0/file/LICENSE.txt"},{"assetType":"Microsoft.VisualStudio.Services.VSIXPackage","source":"https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.0/file/applied-knowledge-systems.terraphim-it-0.1.0.vsix"},{"assetType":"Microsoft.VisualStudio.Services.Content.Changelog","source":"https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.0/file/changelog.md"},{"assetType":"Microsoft.VisualStudio.Services.VsixManifest","source":"https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.0/file/extension.vsixmanifest"},{"assetType":"Microsoft.VisualStudio.Services.VsixSignature","source":"https://open-vsx.org/api/applied-knowledge-systems/terraphim-it/0.1.0/file/applied-knowledge-systems.terraphim-it-0.1.0.sigzip"},{"assetType":"Microsoft.VisualStudio.Services.PublicKey","source":"https://open-vsx.org/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-14T08:20:45.453003Z","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/publish_openvsx_test.py b/vs-code-terraphim-it/tests/publish_openvsx_test.py index 0b75626c7..e489478a6 100644 --- a/vs-code-terraphim-it/tests/publish_openvsx_test.py +++ b/vs-code-terraphim-it/tests/publish_openvsx_test.py @@ -1,47 +1,27 @@ -"""Direct policy tests: no substitute registry, host, or subprocess implementation.""" -from pathlib import Path +"""Direct OIDC policy tests; no substitute registry or subprocess implementation.""" +import subprocess import sys import unittest +from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) import publish_openvsx as publish class OpenVsxPublicationTest(unittest.TestCase): - manifest = {"publisher": "applied-knowledge-systems", "name": "terraphim-it", "version": "0.1.0"} - - def test_only_exact_missing_extension_allows_bootstrap(self): - missing = {"error": "Extension not found: applied-knowledge-systems.terraphim-it", - "deprecated": False, "downloadable": False} - publish.require_first_publication(404, missing, self.manifest) - for status, body in [(200, {"version": "0.0.4"}), (403, missing), (500, missing), - (404, {"error": "Namespace not found"}), - (404, {"error": "Extension not found: other.name"}), (404, []), - (404, {"error": "Extension not found: applied-knowledge-systems.terraphim-it.extra"})]: - with self.subTest(status=status, body=body), self.assertRaises(ValueError): - publish.require_first_publication(status, body, self.manifest) - - def test_default_is_oidc_and_removes_inherited_pat_override(self): + def test_only_oidc_and_removes_inherited_pat_overrides(self): original = {"PATH": "/bin", "OVSX_PAT": "example-old-token", "OVSX_BOOTSTRAP_PAT": "example-new-token"} - command = publish.publication_command(Path("qualified.vsix"), False) - child = publish.publication_environment(False, original) - self.assertEqual(command, ["npm", "exec", "--", "ovsx", "publish", str(Path("qualified.vsix").resolve()), - "--pre-release", "--trusted-publishing"]) - self.assertEqual(child, {"PATH": "/bin"}) + self.assertEqual(publish.publication_command(Path("qualified.vsix")), + ["npm", "exec", "--", "ovsx", "publish", str(Path("qualified.vsix").resolve()), + "--pre-release", "--trusted-publishing"]) + self.assertEqual(publish.publication_environment(original), {"PATH": "/bin"}) self.assertIn("OVSX_PAT", original) - def test_explicit_bootstrap_uses_only_scoped_secret_without_cli_exposure(self): - command = publish.publication_command(Path("qualified.vsix"), True) - child = publish.publication_environment(True, - {"PATH": "/bin", "OVSX_PAT": "unrelated-token", "OVSX_BOOTSTRAP_PAT": "example-bootstrap-token"}) - self.assertNotIn("--trusted-publishing", command) - self.assertNotIn("example-bootstrap-token", " ".join(command)) - self.assertEqual(child, {"PATH": "/bin", "OVSX_PAT": "example-bootstrap-token"}) - - def test_bootstrap_has_no_fallback_to_ambient_pat_or_oidc(self): - for environment in [{}, {"OVSX_PAT": "unrelated-token"}, {"OVSX_BOOTSTRAP_PAT": " "}]: - with self.subTest(environment=environment), self.assertRaisesRegex(ValueError, "OVSX_BOOTSTRAP_PAT"): - publish.publication_environment(True, environment) + def test_bootstrap_cli_option_is_refused_before_network(self): + result = subprocess.run([sys.executable, publish.__file__, "--bootstrap", "--vsix", "absent", + "--manifest", "absent"], capture_output=True, text=True, timeout=10, check=False) + self.assertEqual(result.returncode, 2) + self.assertIn("unrecognized arguments: --bootstrap", result.stderr) if __name__ == "__main__": diff --git a/vs-code-terraphim-it/tests/verify_registry_test.py b/vs-code-terraphim-it/tests/verify_registry_test.py index 80de7ee3a..8839cc3b7 100644 --- a/vs-code-terraphim-it/tests/verify_registry_test.py +++ b/vs-code-terraphim-it/tests/verify_registry_test.py @@ -1,11 +1,12 @@ """Registry comparison uses real ZIP payloads; no network substitutes or remote writes.""" +import copy import json -from pathlib import Path import subprocess import sys import tempfile import unittest import zipfile +from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) import verify_registry as registry @@ -13,6 +14,65 @@ class RegistryTest(unittest.TestCase): + def fixture(self, registry): + name = "microsoft" if registry == "marketplace" else registry + return json.loads((Path(__file__).parent / "fixtures/registries" / (name + ".json")).read_text()) + + def test_real_catalog_responses_bind_exact_version_channel_and_download(self): + for name in ["marketplace", "cursor", "windsurf"]: + manifest = {**verify_vsix_test.VsixTest.manifest, "version": "0.1.3" if name == "marketplace" else "0.1.0"} + selected = registry.catalog_version(self.fixture(name), manifest, name) + url = registry.catalog_download(selected, manifest, name) + self.assertIn("/" + manifest["version"] + "/", url) + # A directly available 0.1.3 asset cannot qualify an older indexed catalog. + for name in ["cursor", "windsurf"]: + with self.assertRaises(registry.NotIndexed): + registry.catalog_version(self.fixture(name), {**manifest, "version": "0.1.3"}, name) + + def test_catalog_rejects_ambiguous_channel_platform_and_validation(self): + original = self.fixture("marketplace") + manifest = {**verify_vsix_test.VsixTest.manifest, "version": "0.1.3"} + for mutation in ["duplicate-extension", "duplicate-version", "stable", "platform", "private", "unvalidated"]: + data = copy.deepcopy(original) + extension = data["results"][0]["extensions"][0] + version = extension["versions"][0] + if mutation == "duplicate-extension": + data["results"][0]["extensions"].append(copy.deepcopy(extension)) + elif mutation == "duplicate-version": + extension["versions"].append(copy.deepcopy(version)) + elif mutation == "stable": + version["properties"] = [{"key": registry.PRERELEASE_PROPERTY, "value": "false"}] + elif mutation == "platform": + version["targetPlatform"] = "linux-x64" + elif mutation == "private": + extension["flags"] = "validated" + else: + version["flags"] = "" + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + registry.catalog_version(data, manifest, "marketplace") + + def test_openvsx_oidc_attestation_cannot_be_replaced_by_matching_bytes(self): + registry.require_trusted_publication({"publishedWithTrustedPublishing": True}) + for value in [False, None, "true", 1]: + with self.subTest(value=value), self.assertRaisesRegex(ValueError, "attest trusted publishing"): + registry.require_trusted_publication({"publishedWithTrustedPublishing": value}) + with self.assertRaises(ValueError): + registry.require_trusted_publication({}) + + def test_catalog_asset_url_and_duplicates_fail_closed(self): + for name in ["marketplace", "cursor", "windsurf"]: + manifest = {**verify_vsix_test.VsixTest.manifest, "version": "0.1.3" if name == "marketplace" else "0.1.0"} + selected = registry.catalog_version(self.fixture(name), manifest, name) + original = registry.catalog_download(selected, manifest, name) + for url in [original + "?token=no", original.replace("https:", "http:"), + original.replace("/" + manifest["version"] + "/", "/9.9.9/"), + "https://attacker.invalid/package.vsix", original.replace("https://", "https://user@")]: + changed = {**selected, "files": [{"assetType": registry.VSIX_ASSET, "source": url}]} + with self.subTest(name=name, url=url), self.assertRaises(ValueError): + registry.catalog_download(changed, manifest, name) + with self.assertRaises(ValueError): + registry.catalog_download({**selected, "files": selected["files"] * 2}, manifest, name) + def test_outer_signing_and_zip_compression_can_differ_with_exact_payload(self): with tempfile.TemporaryDirectory() as folder: root = Path(folder) @@ -95,7 +155,7 @@ def test_cli_dirty_qualification_fails_with_receipt_without_network(self): result = subprocess.run([sys.executable, str(Path(registry.__file__)), "--registry", "marketplace", "--vsix", str(original), "--manifest", str(manifest), "--receipt", str(receipt)], - text=True, capture_output=True) + text=True, capture_output=True, timeout=10, check=False) self.assertEqual(result.returncode, 1) self.assertEqual(json.loads(receipt.read_text())["status"], "FAILED") self.assertIn("clean source", result.stderr) From acd50dbddd8ab0e4a8bb4ba3b6e89f1d1ce1ab6c Mon Sep 17 00:00:00 2001 From: Dr Alexander Mikhalev Date: Wed, 14 Oct 2026 23:26:43 +0100 Subject: [PATCH 2/2] fix: bound existing-release catalog audits to one read Refs #3488 --- .github/workflows/vscode-registry-readback.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/vscode-registry-readback.yml b/.github/workflows/vscode-registry-readback.yml index 452954b29..5cb913417 100644 --- a/.github/workflows/vscode-registry-readback.yml +++ b/.github/workflows/vscode-registry-readback.yml @@ -39,7 +39,7 @@ jobs: if: inputs.registry == 'all' || inputs.registry == matrix.registry env: REGISTRY: ${{ matrix.registry }} - run: python3 vs-code-terraphim-it/scripts/verify_registry.py --registry "$REGISTRY" --vsix registry-input/terraphim-it.vsix --manifest registry-input/release-manifest.json --receipt registry-result.json --attempts 20 --delay 30 + run: python3 vs-code-terraphim-it/scripts/verify_registry.py --registry "$REGISTRY" --vsix registry-input/terraphim-it.vsix --manifest registry-input/release-manifest.json --receipt registry-result.json --attempts 1 --delay 0 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 if: always() && (inputs.registry == 'all' || inputs.registry == matrix.registry) with: