Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/vscode-registry-readback.yml
Original file line number Diff line number Diff line change
@@ -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 1 --delay 0
- 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
52 changes: 40 additions & 12 deletions .github/workflows/vscode-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 24 additions & 1 deletion vs-code-terraphim-it/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
90 changes: 90 additions & 0 deletions vs-code-terraphim-it/scripts/download_registry_release.py
Original file line number Diff line number Diff line change
@@ -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())
65 changes: 13 additions & 52 deletions vs-code-terraphim-it/scripts/publish_openvsx.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Loading
Loading