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
13 changes: 13 additions & 0 deletions .github/workflows/vscode-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ jobs:
client-id: ${{ vars.VSCE_AZURE_CLIENT_ID }}
tenant-id: ${{ vars.VSCE_AZURE_TENANT_ID }}
allow-no-subscriptions: true
- name: Confirm the authenticated publisher profile before uploading
if: matrix.registry == 'visual-studio-marketplace'
env:
VSCE_AZURE_CLIENT_ID: ${{ vars.VSCE_AZURE_CLIENT_ID }}
VSCE_AZURE_TENANT_ID: ${{ vars.VSCE_AZURE_TENANT_ID }}
VSCE_MARKETPLACE_PROFILE_ID: ${{ vars.VSCE_MARKETPLACE_PROFILE_ID }}
run: python3 scripts/marketplace_identity.py --receipt marketplace-identity.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always() && matrix.registry == 'visual-studio-marketplace'
with:
name: vscode-marketplace-identity-${{ github.sha }}-${{ github.run_attempt }}
path: vs-code-terraphim-it/marketplace-identity.json
if-no-files-found: warn
# A retry may encounter an already published exact version. Readback is
# the mandatory authority; any absent or different payload fails the job.
- name: Publish exact VSIX to Visual Studio Marketplace
Expand Down
4 changes: 4 additions & 0 deletions vs-code-terraphim-it/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.1.2 (preview)

- Verify the federated service principal and its Azure DevOps profile before Marketplace publication; retain non-secret identity evidence and refuse an unconfirmed profile.

## 0.1.1 (preview)

- Bundle the verified editor v0.1.0-rc.2 release built with public Terraphim crate dependencies.
Expand Down
28 changes: 28 additions & 0 deletions vs-code-terraphim-it/README-alternative-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,32 @@ publisher, protected environment and federated identity are ready. Enabled
targets remain `PENDING_PUBLICATION` until the separate registry readback passes.
These gates do not create access, weaken tag policies or provide a PAT fallback.

Microsoft publication also requires `VSCE_MARKETPLACE_PROFILE_ID` in the existing
`visual-studio-marketplace` environment. After federated login, CI verifies the
service principal client and tenant, then queries its own Azure DevOps profile.
The profile ID is distinct from the Entra application and enterprise object IDs.
An empty or different expected profile ID stops the job before upload and retains
only non-secret metadata in `vscode-marketplace-identity-<commit>-<attempt>`.
That receipt proves identity admission, not publisher membership or publication.

For initial setup, enable the Microsoft readiness gate only after the existing
publisher and its federation are ready. Leave the profile ID unset for the first
identity read. Use the returned profile ID to add that principal as **Contributor**
to `applied-knowledge-systems`; verify the assignment, then configure the expected
profile ID. Rerun only that release's `marketplace-plan` job and its dependent
marketplace job to use the already qualified package. Verify the final registry
receipt against every extension payload file. Do not rerun package qualification
or dispatch the same version afresh: a rebuilt VSIX can have different ZIP
timestamps, and the immutable GitHub publisher will refuse replacement bytes.
The original source tag, package artifact and three native receipts must remain
available; select retained receipts by artifact ID and digest, not name alone.

Version 0.1.2 introduces this CI-only identity step because the immutable 0.1.1
tag cannot be changed. It retains the same released editor inputs and runtime
features. Real Azure identity retrieval remains an integration gate until the
configured principal executes it; local payload fixtures do not qualify login.
This step assigns no Azure resource roles or organization membership and creates
no long-lived secrets. See Microsoft's documented
[principal profile lookup](https://learn.microsoft.com/en-us/azure/devops/extend/publish/command-line?view=azure-devops#publish-with-a-microsoft-entra-token).

Acceptance reports distinguish actual saved files, engine tests and visual UI checks. The original 41 visual requirements are not all accepted. This is a scoped preview; stock Zed visual limitations and the separate Zed fork remain outside this VSIX's claims.
4 changes: 2 additions & 2 deletions vs-code-terraphim-it/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vs-code-terraphim-it/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "terraphim-it",
"displayName": "Terraphim It",
"description": "Intelligent Markdown editor with alternatives, knowledge-graph synonyms and role-based suggestions",
"version": "0.1.1",
"version": "0.1.2",
"publisher": "applied-knowledge-systems",
"private": true,
"license": "Apache-2.0",
Expand Down
149 changes: 149 additions & 0 deletions vs-code-terraphim-it/scripts/marketplace_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Read the federated publisher's identity; refuse an unconfirmed profile.

Only fixed Azure CLI metadata queries are executed. Tokens, full profiles and
CLI error text are never written to stdout, stderr or the retained receipt.
"""
import argparse
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import uuid

from verify_vsix import check_tag

ROOT = Path(__file__).resolve().parents[1]
RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798"
PROFILE_URL = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1"
ACCOUNT_COMMAND = ["az", "account", "show", "--query",
"{tenantId:tenantId,userName:user.name,userType:user.type}",
"--output", "json", "--only-show-errors"]
PROFILE_COMMAND = ["az", "rest", "--method", "get", "--url", PROFILE_URL,
"--resource", RESOURCE, "--query", "{id:id}",
"--output", "json", "--only-show-errors"]


class Refusal(ValueError):
"""A fixed public error code, never an external response or command output."""


def identifier(value):
if not isinstance(value, str) or re.fullmatch(
r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", value) is None:
raise Refusal("INVALID_IDENTITY_IDENTIFIER")
result = uuid.UUID(value)
if result.int == 0:
raise Refusal("INVALID_IDENTITY_IDENTIFIER")
return str(result)


def configuration(environment, version):
"""Admit only this repository's exact release tag and explicit identities."""
if (environment.get("GITHUB_REPOSITORY") != "terraphim/terraphim-ai"
or environment.get("GITHUB_REF_TYPE") != "tag"
or re.fullmatch(r"[0-9a-f]{40}", environment.get("GITHUB_SHA", "")) is None):
raise Refusal("INVALID_RELEASE_CONTEXT")
try:
check_tag(environment.get("GITHUB_REF_NAME", ""), version)
except ValueError:
raise Refusal("INVALID_RELEASE_CONTEXT") from None
client = identifier(environment.get("VSCE_AZURE_CLIENT_ID"))
tenant = identifier(environment.get("VSCE_AZURE_TENANT_ID"))
expected = environment.get("VSCE_MARKETPLACE_PROFILE_ID", "")
return client, tenant, identifier(expected) if expected else None


def parse_metadata(raw):
"""Reject ambiguous or oversized metadata without echoing any input."""
def pairs(items):
result = {}
for key, value in items:
if key in result:
raise Refusal("INVALID_METADATA")
result[key] = value
return result

if not isinstance(raw, bytes) or len(raw) > 65536:
raise Refusal("INVALID_METADATA")
try:
value = json.loads(raw, object_pairs_hook=pairs)
except (ValueError, UnicodeError):
raise Refusal("INVALID_METADATA") from None
if not isinstance(value, dict):
raise Refusal("INVALID_METADATA")
return value


def run_metadata(command, timeout=60):
"""Capture a real child process privately and expose only validated JSON."""
try:
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=timeout, check=False)
except subprocess.TimeoutExpired:
raise Refusal("IDENTITY_QUERY_TIMEOUT") from None
except OSError:
raise Refusal("IDENTITY_QUERY_UNAVAILABLE") from None
if result.returncode:
raise Refusal("IDENTITY_QUERY_FAILED")
return parse_metadata(result.stdout)


def verify_account(account, client, tenant):
if (not isinstance(account, dict) or set(account) != {"tenantId", "userName", "userType"}
or account["userType"] != "servicePrincipal"):
raise Refusal("UNEXPECTED_ACCOUNT_TYPE")
if identifier(account["tenantId"]) != tenant or identifier(account["userName"]) != client:
raise Refusal("AUTHENTICATED_IDENTITY_MISMATCH")


def profile_id(profile):
if not isinstance(profile, dict) or set(profile) != {"id"}:
raise Refusal("INVALID_PROFILE_METADATA")
return identifier(profile["id"])


def confirm_profile(actual, expected):
if expected is None:
raise Refusal("PUBLISHER_MEMBERSHIP_CONFIRMATION_REQUIRED")
if actual != expected:
raise Refusal("PUBLISHER_PROFILE_MISMATCH")


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--receipt", type=Path, required=True)
args = parser.parse_args()
result = {"schema": 1, "status": "NOT_READY",
"scope": "Identity admission only; publisher membership and publication require separate verification"}
try:
# Retain earlier evidence, including symlinks to absent destinations.
with args.receipt.open("x", encoding="utf-8") as output:
try:
version = json.loads((ROOT / "package.json").read_text())["version"]
client, tenant, expected = configuration(os.environ, version)
verify_account(run_metadata(ACCOUNT_COMMAND), client, tenant)
actual = profile_id(run_metadata(PROFILE_COMMAND))
result.update(clientId=client, tenantId=tenant, profileId=actual,
sourceCommit=os.environ["GITHUB_SHA"], tag=os.environ["GITHUB_REF_NAME"])
confirm_profile(actual, expected)
result["status"] = "IDENTITY_VERIFIED_READY"
except Refusal as error:
result["errorCode"] = str(error)
except (OSError, ValueError, KeyError, TypeError):
result["errorCode"] = "INVALID_LOCAL_CONFIGURATION"
output.write(json.dumps(result, indent=2) + "\n")
except OSError:
print("marketplace_identity: RECEIPT_UNAVAILABLE", file=sys.stderr)
return 1
if result["status"] != "IDENTITY_VERIFIED_READY":
print("marketplace_identity: " + result["errorCode"], file=sys.stderr)
return 1
print("marketplace_identity: IDENTITY_VERIFIED_READY (publication not yet verified)")
return 0


if __name__ == "__main__":
sys.exit(main())
148 changes: 148 additions & 0 deletions vs-code-terraphim-it/tests/marketplace_identity_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Pure identity fixtures and real local child processes; no cloud authentication."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import marketplace_identity as identity

CLIENT = "11111111-2222-3333-4444-555555555555"
TENANT = "22222222-3333-4444-5555-666666666666"
PROFILE = "33333333-4444-5555-6666-777777777777"
OTHER = "44444444-5555-6666-7777-888888888888"
CANARY = "PRIVATE_OUTPUT_MUST_NOT_ESCAPE"


class MarketplaceIdentityTests(unittest.TestCase):
def environment(self):
version = json.loads((ROOT / "package.json").read_text())["version"]
return {"GITHUB_REPOSITORY": "terraphim/terraphim-ai", "GITHUB_REF_TYPE": "tag",
"GITHUB_REF_NAME": "terraphim_vscode-v" + version, "GITHUB_SHA": "a" * 40,
"VSCE_AZURE_CLIENT_ID": CLIENT, "VSCE_AZURE_TENANT_ID": TENANT}

def test_only_exact_release_context_and_well_formed_configuration_admitted(self):
env = self.environment()
version = json.loads((ROOT / "package.json").read_text())["version"]
self.assertEqual(identity.configuration(env, version), (CLIENT, TENANT, None))
for key, value in (("GITHUB_REPOSITORY", "elsewhere/repo"), ("GITHUB_REF_TYPE", "branch"),
("GITHUB_REF_NAME", "main"), ("GITHUB_SHA", "b" * 39),
("VSCE_AZURE_CLIENT_ID", ""), ("VSCE_AZURE_TENANT_ID", CANARY),
("VSCE_MARKETPLACE_PROFILE_ID", " ")):
with self.subTest(key=key), self.assertRaises(identity.Refusal) as error:
identity.configuration({**env, key: value}, version)
self.assertNotIn(CANARY, str(error.exception))

def test_identifier_refuses_null_zero_whitespace_and_control_characters(self):
for value in (None, [], {}, 3, "", "0" * 32, "00000000-0000-0000-0000-000000000000",
" " + CLIENT, CLIENT + "\n", CLIENT + "?token=" + CANARY):
with self.subTest(value=type(value).__name__), self.assertRaises(identity.Refusal):
identity.identifier(value)
self.assertEqual(identity.identifier("AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"),
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")

def test_pure_account_fixture_must_match_service_principal_tenant_and_client(self):
account = {"tenantId": TENANT, "userName": CLIENT, "userType": "servicePrincipal"}
identity.verify_account(account, CLIENT, TENANT)
for changed in (None, [], {}, {**account, "userType": "user"},
{**account, "userName": OTHER}, {**account, "tenantId": OTHER},
{**account, "userName": None}, {**account, "accessToken": CANARY}):
with self.subTest(changed=type(changed).__name__), self.assertRaises(identity.Refusal) as error:
identity.verify_account(changed, CLIENT, TENANT)
self.assertNotIn(CANARY, str(error.exception))

def test_profile_fixture_is_not_ready_without_explicit_matching_expected_id(self):
actual = identity.profile_id({"id": PROFILE})
self.assertEqual(actual, PROFILE)
with self.assertRaisesRegex(identity.Refusal, "MEMBERSHIP_CONFIRMATION_REQUIRED"):
identity.confirm_profile(actual, None)
with self.assertRaisesRegex(identity.Refusal, "PROFILE_MISMATCH"):
identity.confirm_profile(actual, OTHER)
identity.confirm_profile(actual, PROFILE)
for value in (None, [], {}, {"id": None}, {"id": PROFILE, "token": CANARY}):
with self.subTest(value=type(value).__name__), self.assertRaises(identity.Refusal):
identity.profile_id(value)

def test_metadata_parser_refuses_duplicate_keys_types_and_oversized_output(self):
for data in (b'{"id":"first","id":"second"}', b'[]', b'null', b'not-json',
b'\xff', b' ' * 65537, CANARY):
with self.subTest(dataType=type(data).__name__), self.assertRaises(identity.Refusal) as error:
identity.parse_metadata(data)
self.assertEqual(str(error.exception), "INVALID_METADATA")

def test_real_child_process_json_transport_does_not_expose_stderr(self):
command = [sys.executable, "-c", "import sys; print('{\"transport\":true}'); "
"print('" + CANARY + "',file=sys.stderr)"]
self.assertEqual(identity.run_metadata(command), {"transport": True})

def test_real_child_process_failures_timeout_and_invalid_json_are_sanitized(self):
cases = [([sys.executable, "-c", "import sys; print('" + CANARY + "'); "
"print('" + CANARY + "',file=sys.stderr); sys.exit(7)"], 5, "IDENTITY_QUERY_FAILED"),
([sys.executable, "-c", "import time; time.sleep(5)"], .02, "IDENTITY_QUERY_TIMEOUT"),
([sys.executable, "-c", "print('" + CANARY + "')"], 5, "INVALID_METADATA"),
(["/nonexistent/metadata-reader"], 5, "IDENTITY_QUERY_UNAVAILABLE")]
for command, timeout, expected in cases:
with self.subTest(expected=expected), self.assertRaises(identity.Refusal) as error:
identity.run_metadata(command, timeout)
self.assertEqual(str(error.exception), expected)

def test_invalid_configuration_cli_writes_only_fixed_failure_without_azure(self):
with tempfile.TemporaryDirectory() as directory:
receipt = Path(directory) / "identity.json"
result = subprocess.run([sys.executable, str(ROOT / "scripts/marketplace_identity.py"),
"--receipt", str(receipt)], capture_output=True, text=True, timeout=10,
env={**os.environ, **self.environment(), "VSCE_AZURE_CLIENT_ID": CANARY})
self.assertEqual(result.returncode, 1)
report = json.loads(receipt.read_text())
self.assertEqual(report["status"], "NOT_READY")
self.assertEqual(report["errorCode"], "INVALID_IDENTITY_IDENTIFIER")
self.assertNotIn(CANARY, result.stdout + result.stderr + receipt.read_text())
self.assertNotIn("profileId", report)

def test_existing_and_symlink_receipts_are_not_overwritten(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
old = root / "old.json"; old.write_text("original receipt")
link = root / "link.json"; link.symlink_to(root / "absent.json")
for receipt in (old, link):
result = subprocess.run([sys.executable, str(ROOT / "scripts/marketplace_identity.py"),
"--receipt", str(receipt)], capture_output=True, text=True, timeout=10,
env={**os.environ, **self.environment()})
self.assertEqual(result.returncode, 1)
self.assertIn("RECEIPT_UNAVAILABLE", result.stderr)
self.assertEqual(old.read_text(), "original receipt")
self.assertTrue(link.is_symlink())
self.assertFalse((root / "absent.json").exists())

def test_fixed_commands_request_only_expected_identity_metadata(self):
self.assertEqual(identity.ACCOUNT_COMMAND[:3], ["az", "account", "show"])
self.assertEqual(identity.PROFILE_COMMAND[:4], ["az", "rest", "--method", "get"])
self.assertIn(identity.PROFILE_URL, identity.PROFILE_COMMAND)
self.assertIn("{id:id}", identity.PROFILE_COMMAND)
self.assertIn("499b84ac-1321-427f-aa17-267ca6975798", identity.PROFILE_COMMAND)
for command in (identity.ACCOUNT_COMMAND, identity.PROFILE_COMMAND):
self.assertIn("--only-show-errors", command)
for forbidden in ("get-access-token", "--debug", "--verbose", "--headers", "--password"):
self.assertNotIn(forbidden, command)

def test_workflow_gate_runs_after_login_before_publish_and_retains_separate_receipt(self):
workflow = (ROOT.parent / ".github/workflows/vscode-release.yml").read_text()
login = workflow.index("uses: azure/login@")
gate = workflow.index("run: python3 scripts/marketplace_identity.py")
publish = workflow.index("run: npm exec -- vsce publish")
self.assertLess(login, gate)
self.assertLess(gate, publish)
block = workflow[workflow.rfind(" - name:", 0, login):workflow.rfind(" - name:", 0, publish)]
self.assertNotIn("continue-on-error: true", block)
self.assertIn("vars.VSCE_MARKETPLACE_PROFILE_ID", block)
self.assertIn("path: vs-code-terraphim-it/marketplace-identity.json", workflow)
self.assertIn("vscode-marketplace-identity-${{ github.sha }}-${{ github.run_attempt }}", workflow)
self.assertNotIn("VSCE_PAT", workflow)


if __name__ == "__main__":
unittest.main()
Loading
Loading