diff --git a/.github/schemas/plugin-manifest.schema.json b/.github/schemas/plugin-manifest.schema.json deleted file mode 100644 index 16d74fa..0000000 --- a/.github/schemas/plugin-manifest.schema.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Claude Code Plugin Manifest", - "description": "Schema for .claude-plugin/plugin.json files, based on https://code.claude.com/docs/en/plugins-reference", - "type": "object", - "required": ["name"], - "additionalProperties": false, - "$defs": { - "stringOrStringArray": { - "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "stringOrArrayOrObject": { - "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } }, - { "type": "object" } - ] - } - }, - "properties": { - "name": { - "type": "string", - "description": "Unique plugin identifier (kebab-case, no spaces)" - }, - "displayName": { - "type": "string", - "description": "Human-readable plugin name for marketplace display" - }, - "version": { - "type": "string", - "description": "Semantic version (MAJOR.MINOR.PATCH)" - }, - "description": { - "type": "string", - "description": "Brief explanation of plugin purpose" - }, - "author": { - "type": "object", - "description": "Author information", - "properties": { - "name": { "type": "string" }, - "email": { "type": "string" }, - "url": { "type": "string" } - }, - "additionalProperties": false - }, - "homepage": { - "type": "string", - "description": "Documentation URL" - }, - "repository": { - "type": "string", - "description": "Source code URL" - }, - "license": { - "type": "string", - "description": "License identifier (e.g. MIT, Apache-2.0)" - }, - "keywords": { - "type": "array", - "items": { "type": "string" }, - "description": "Discovery tags" - }, - "commands": { - "$ref": "#/$defs/stringOrStringArray", - "description": "Additional command files/directories (string or array of strings)" - }, - "agents": { - "$ref": "#/$defs/stringOrStringArray", - "description": "Additional agent files/directories (string or array of strings)" - }, - "skills": { - "$ref": "#/$defs/stringOrStringArray", - "description": "Additional skill directories (string or array of strings)" - }, - "hooks": { - "$ref": "#/$defs/stringOrArrayOrObject", - "description": "Hook config paths or inline hook configuration (string, array, or object)" - }, - "mcpServers": { - "$ref": "#/$defs/stringOrArrayOrObject", - "description": "MCP server config paths or inline configuration (string, array, or object)" - }, - "outputStyles": { - "$ref": "#/$defs/stringOrStringArray", - "description": "Additional output style files/directories (string or array of strings)" - }, - "lspServers": { - "$ref": "#/$defs/stringOrArrayOrObject", - "description": "LSP server config paths or inline configuration (string, array, or object)" - } - } -} diff --git a/.github/scripts/validate-frontmatter.py b/.github/scripts/validate-frontmatter.py deleted file mode 100644 index 16db874..0000000 --- a/.github/scripts/validate-frontmatter.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -"""Validate YAML frontmatter in markdown files and standalone YAML files under plugins/.""" - -import os -import subprocess -import sys -import tempfile - -PLUGINS_DIR = "plugins" -YAMLLINT_CONFIG = ".yamllint.yml" - - -def extract_frontmatter(filepath: str) -> tuple[str | None, int]: - """Extract YAML frontmatter from a markdown file. - - Returns (frontmatter_content, start_line) or (None, 0) if no frontmatter. - """ - with open(filepath, "r") as f: - lines = f.readlines() - - if not lines or lines[0].rstrip("\n") != "---": - return None, 0 - - end = None - for i in range(1, len(lines)): - if lines[i].rstrip("\n") == "---": - end = i - break - - if end is None: - return None, 0 - - # Return frontmatter content (between the two --- delimiters) - return "".join(lines[1:end]), 1 - - -def run_yamllint(content: str, original_file: str, line_offset: int) -> list[str]: - """Run yamllint on content and return error messages with adjusted line numbers.""" - errors = [] - with tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False) as tmp: - tmp.write(content) - tmp_path = tmp.name - - try: - result = subprocess.run( - ["yamllint", "-c", YAMLLINT_CONFIG, tmp_path], - capture_output=True, - text=True, - ) - if result.returncode != 0: - for line in result.stdout.strip().splitlines(): - # yamllint output format: "file:line:col: [level] message" - if tmp_path in line: - # Replace temp path with original file path and adjust line number - rest = line.split(tmp_path, 1)[1] - if rest.startswith(":"): - parts = rest[1:].split(":", 2) - if len(parts) >= 2 and parts[0].strip().isdigit(): - adjusted_line = int(parts[0].strip()) + line_offset - errors.append( - f"{original_file}:{adjusted_line}:{':'.join(parts[1:])}" - ) - continue - errors.append(f"{original_file}{rest}") - finally: - os.unlink(tmp_path) - - return errors - - -def main() -> int: - all_errors: list[str] = [] - - # Find all markdown files with frontmatter - for root, _dirs, files in os.walk(PLUGINS_DIR): - for filename in files: - filepath = os.path.join(root, filename) - - if filename.endswith(".md"): - frontmatter, offset = extract_frontmatter(filepath) - if frontmatter is not None: - errors = run_yamllint(frontmatter, filepath, offset) - all_errors.extend(errors) - - elif filename.endswith((".yml", ".yaml")): - with open(filepath) as f: - content = f.read() - errors = run_yamllint(content, filepath, 0) - all_errors.extend(errors) - - if all_errors: - print("YAML validation errors found:\n") - for error in all_errors: - print(f" {error}") - print(f"\n{len(all_errors)} error(s) found.") - return 1 - - print("All YAML frontmatter and YAML files are valid.") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/scripts/validate-marketplace.py b/.github/scripts/validate-marketplace.py deleted file mode 100644 index aa12918..0000000 --- a/.github/scripts/validate-marketplace.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env python3 -"""Validate marketplace.json registry against the actual plugin directories.""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[2] -MARKETPLACE_JSON = REPO_ROOT / ".claude-plugin" / "marketplace.json" -PLUGINS_DIR = REPO_ROOT / "plugins" - - -def load_marketplace() -> dict | None: - """Load and return the parsed marketplace.json, or None on parse error.""" - with MARKETPLACE_JSON.open() as f: - try: - return json.load(f) - except json.JSONDecodeError as exc: - print(f"\n✗ Failed to parse marketplace.json: {exc}") - return None - - -def _resolve_source(plugin: dict) -> Path: - """Resolve a plugin's source path to an absolute path under REPO_ROOT.""" - return REPO_ROOT / plugin["source"].removeprefix("./") - - -def check_source_paths(plugins: list[dict]) -> list[str]: - """Check that every plugin source path exists as a directory.""" - errors: list[str] = [] - for plugin in plugins: - name = plugin["name"] - source = _resolve_source(plugin) - if not source.is_dir(): - errors.append(f" ✗ Plugin '{name}': source path does not exist: {plugin['source']}") - return errors - - -def check_duplicate_names(plugins: list[dict]) -> list[str]: - """Check for duplicate plugin names in marketplace.json.""" - errors: list[str] = [] - seen: dict[str, int] = {} - for plugin in plugins: - name = plugin["name"] - seen[name] = seen.get(name, 0) + 1 - for name, count in seen.items(): - if count > 1: - errors.append(f" ✗ Plugin name '{name}' appears {count} times") - return errors - - -def check_orphan_directories(plugins: list[dict]) -> list[str]: - """Find plugin directories not registered in marketplace.json.""" - warnings: list[str] = [] - - # Resolve all registered source paths to absolute paths - registered: set[Path] = set() - for plugin in plugins: - registered.add(_resolve_source(plugin).resolve()) - - # Walk immediate children and known subdirectories of plugins/ - plugin_dirs: list[Path] = [] - if PLUGINS_DIR.is_dir(): - for child in sorted(PLUGINS_DIR.iterdir()): - if not child.is_dir(): - continue - # Some plugins live in subdirectories (e.g. plugins/commands/*, plugins/mcps/*) - has_plugin_json = (child / ".claude-plugin" / "plugin.json").exists() - has_subdirs = any(sub.is_dir() for sub in child.iterdir()) - if has_plugin_json: - plugin_dirs.append(child) - elif has_subdirs: - # Check nested directories (e.g. plugins/commands/carta-devtools) - for sub in sorted(child.iterdir()): - if sub.is_dir() and (sub / ".claude-plugin" / "plugin.json").exists(): - plugin_dirs.append(sub) - else: - plugin_dirs.append(child) - - for d in plugin_dirs: - if d.resolve() not in registered: - rel = d.relative_to(REPO_ROOT) - warnings.append(f" ! Directory '{rel}' is not registered in marketplace.json") - - return warnings - - -def check_required_files(plugins: list[dict]) -> list[str]: - """Check that each registered plugin has the required plugin.json.""" - errors: list[str] = [] - for plugin in plugins: - name = plugin["name"] - source = _resolve_source(plugin) - manifest = source / ".claude-plugin" / "plugin.json" - if not manifest.is_file(): - errors.append(f" ✗ Plugin '{name}': missing .claude-plugin/plugin.json") - return errors - - -def main() -> int: - print("=" * 60) - print("Marketplace Validation") - print("=" * 60) - - if not MARKETPLACE_JSON.is_file(): - print(f"\n✗ marketplace.json not found at {MARKETPLACE_JSON}") - return 1 - - data = load_marketplace() - if data is None: - return 1 - plugins = data.get("plugins", []) - - error_count = 0 - warning_count = 0 - - # Check 1: Source paths exist - print("\n--- Check 1: Source paths exist ---") - errors = check_source_paths(plugins) - if errors: - for e in errors: - print(e) - error_count += len(errors) - else: - print(" ✓ All source paths exist") - - # Check 2: No duplicate plugin names - print("\n--- Check 2: No duplicate plugin names ---") - errors = check_duplicate_names(plugins) - if errors: - for e in errors: - print(e) - error_count += len(errors) - else: - print(" ✓ All plugin names are unique") - - # Check 3: Orphan plugin directories - print("\n--- Check 3: Orphan plugin directories ---") - warnings = check_orphan_directories(plugins) - if warnings: - for w in warnings: - print(w) - warning_count += len(warnings) - else: - print(" ✓ No orphan plugin directories found") - - # Check 4: Required files - print("\n--- Check 4: Required files ---") - errors = check_required_files(plugins) - if errors: - for e in errors: - print(e) - error_count += len(errors) - else: - print(" ✓ All plugins have required files") - - # Summary - print("\n" + "=" * 60) - print(f"Summary: {error_count} error(s), {warning_count} warning(s)") - if error_count: - print("FAILED") - else: - print("PASSED") - print("=" * 60) - - return 1 if error_count else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/scripts/verify-provenance.py b/.github/scripts/verify-provenance.py deleted file mode 100755 index d1e0f62..0000000 --- a/.github/scripts/verify-provenance.py +++ /dev/null @@ -1,401 +0,0 @@ -#!/usr/bin/env python3 -"""Verify that plugins in a PR originated from the internal claude-marketplace repo. - -For each plugin name provided, performs three checks: - 1. Existence: plugin directory exists in carta/claude-marketplace main branch. - 2. Security manifest: plugin appears with "status": "passed" in the manifest. - 3. Content integrity: local content hash matches the manifest's content_hash. - -Usage: - python verify-provenance.py "plugin-a,plugin-b,plugin-c" - -Requires: - GH_TOKEN environment variable for GitHub API authentication. -""" - -from __future__ import annotations - -import base64 -import hashlib -import json -import os -import sys -import urllib.error -import urllib.request -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[2] -PLUGINS_DIR = REPO_ROOT / "plugins" - -GITHUB_API = "https://api.github.com" -MARKETPLACE_REPO = os.environ.get("PROVENANCE_REPO", "carta/claude-marketplace") -MARKETPLACE_REF = "main" - -# Possible plugin directory prefixes in the marketplace repo. -# Plugins may live at plugins/ or nested under plugins//. -MARKETPLACE_SEARCH_PREFIXES = [ - "plugins", - "plugins/commands", - "plugins/mcps", -] - - -def _gh_token() -> str: - """Return the GitHub token or exit with an error.""" - token = os.environ.get("GH_TOKEN", "") - if not token: - print("Error: GH_TOKEN environment variable is not set.") - print("Set it to a GitHub personal access token with repo read access.") - sys.exit(1) - return token - - -def _github_get(path: str) -> dict | None: - """Make a GET request to the GitHub API and return parsed JSON. - - Returns None on 404. Raises SystemExit on auth or rate-limit errors. - """ - url = f"{GITHUB_API}{path}" - req = urllib.request.Request(url, headers={ - "Authorization": f"Bearer {_gh_token()}", - "Accept": "application/vnd.github.v3+json", - "X-GitHub-Api-Version": "2022-11-28", - }) - - try: - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read()) - except urllib.error.HTTPError as exc: - if exc.code == 404: - return None - if exc.code == 403: - remaining = exc.headers.get("X-RateLimit-Remaining", "unknown") - if remaining == "0": - print(f"Error: GitHub API rate limit exceeded. Reset at " - f"{exc.headers.get('X-RateLimit-Reset', 'unknown')}.") - print("Wait a few minutes or use a token with higher limits.") - sys.exit(1) - print(f"Error: GitHub API returned 403 Forbidden for {url}.") - print("Ensure GH_TOKEN has read access to carta/claude-marketplace.") - sys.exit(1) - if exc.code == 401: - print("Error: GitHub API returned 401 Unauthorized.") - print("Ensure GH_TOKEN is a valid token with repo read access.") - sys.exit(1) - raise - - -# --------------------------------------------------------------------------- -# Content hash — must exactly match generate-security-manifest.py -# --------------------------------------------------------------------------- - -# Exclusions must stay in sync with generate-security-manifest.py so that -# hashes computed here match hashes recorded in the security manifest. -HASH_EXCLUDE_DIRS = {"__pycache__", "node_modules"} - - -def _should_exclude(path: Path) -> bool: - """Return True if *path* should be excluded from content hashing.""" - return ( - any(part in HASH_EXCLUDE_DIRS for part in path.parts) - or path.suffix == ".pyc" - or path.name == ".DS_Store" - ) - - -def compute_content_hash(plugin_dir: Path) -> str: - """Compute a deterministic SHA-256 hash of all files in a plugin directory. - - Algorithm (must match generate-security-manifest.py exactly): - 1. Collect all files recursively, excluding build artifacts and OS - metadata (__pycache__, .pyc, .DS_Store, node_modules). - 2. Sort by relative POSIX path. - 3. For each file: concatenate relative_path + newline + file bytes. - 4. Hash the full concatenation with SHA-256. - 5. Return "sha256:". - """ - hasher = hashlib.sha256() - - all_files: list[Path] = sorted( - ( - f - for f in plugin_dir.rglob("*") - if f.is_file() and not _should_exclude(f.relative_to(plugin_dir)) - ), - key=lambda f: f.relative_to(plugin_dir).as_posix(), - ) - - for filepath in all_files: - rel_path = filepath.relative_to(plugin_dir).as_posix() - hasher.update(rel_path.encode("utf-8")) - hasher.update(b"\n") - hasher.update(filepath.read_bytes()) - - return f"sha256:{hasher.hexdigest()}" - - -# --------------------------------------------------------------------------- -# Check 1: Existence in marketplace -# --------------------------------------------------------------------------- - -def check_existence(plugin_name: str) -> tuple[bool, str, str | None]: - """Verify the plugin exists in claude-marketplace main branch. - - Returns (passed, message, marketplace_path_prefix_or_none). - """ - for prefix in MARKETPLACE_SEARCH_PREFIXES: - api_path = ( - f"/repos/{MARKETPLACE_REPO}/contents/" - f"{prefix}/{plugin_name}/.claude-plugin/plugin.json" - f"?ref={MARKETPLACE_REF}" - ) - result = _github_get(api_path) - if result is not None: - return ( - True, - f"Found at {prefix}/{plugin_name} in {MARKETPLACE_REPO}", - prefix, - ) - - searched = ", ".join(f"{p}/{plugin_name}" for p in MARKETPLACE_SEARCH_PREFIXES) - return ( - False, - f"Plugin '{plugin_name}' not found in {MARKETPLACE_REPO}. " - f"Searched: {searched}. " - f"Publish it to claude-marketplace first before adding to the public repo.", - None, - ) - - -# --------------------------------------------------------------------------- -# Check 2 & 3: Security manifest -# --------------------------------------------------------------------------- - -def fetch_security_manifest() -> dict | None: - """Fetch and decode security-manifest.json from marketplace main branch.""" - api_path = ( - f"/repos/{MARKETPLACE_REPO}/contents/security-manifest.json" - f"?ref={MARKETPLACE_REF}" - ) - result = _github_get(api_path) - if result is None: - return None - - content_b64 = result.get("content", "") - try: - raw = base64.b64decode(content_b64) - return json.loads(raw) - except (json.JSONDecodeError, ValueError) as exc: - print(f"Error: Failed to parse security-manifest.json: {exc}") - return None - - -def check_security_manifest( - plugin_name: str, - manifest: dict, -) -> tuple[bool, str]: - """Verify the plugin appears in the manifest with status 'passed'.""" - plugins = manifest.get("plugins", {}) - entry = plugins.get(plugin_name) - - if entry is None: - return ( - False, - f"Plugin '{plugin_name}' not found in security-manifest.json. " - f"Run the security scan in claude-marketplace CI before publishing.", - ) - - status = entry.get("status", "unknown") - if status != "passed": - return ( - False, - f"Plugin '{plugin_name}' has status '{status}' in security manifest " - f"(expected 'passed'). Resolve security findings before publishing.", - ) - - return True, f"Security manifest status: passed" - - -def check_content_integrity( - plugin_name: str, - manifest: dict, -) -> tuple[bool, str]: - """Compare local content hash with the manifest's recorded hash.""" - plugins = manifest.get("plugins", {}) - entry = plugins.get(plugin_name) - - if entry is None: - return ( - False, - f"Cannot verify content integrity — plugin '{plugin_name}' " - f"missing from security manifest.", - ) - - # Prefer published_content_hash (accounts for publish transforms like - # URL rewrites and file exclusions). Fall back to content_hash for - # plugins that haven't been updated to include the published hash yet. - expected_hash = entry.get("published_content_hash") or entry.get("content_hash", "") - hash_field = "published_content_hash" if entry.get("published_content_hash") else "content_hash" - if not expected_hash: - return ( - False, - f"No content hash recorded in manifest for '{plugin_name}'.", - ) - - # The local plugin directory is always at plugins/ in this repo, - # regardless of where it lives in the marketplace repo. - local_plugin_dir = PLUGINS_DIR / plugin_name - if not local_plugin_dir.is_dir(): - return ( - False, - f"Local plugin directory not found: {local_plugin_dir.relative_to(REPO_ROOT)}. " - f"Ensure the plugin has been copied into the plugins/ directory.", - ) - - local_hash = compute_content_hash(local_plugin_dir) - - if local_hash != expected_hash: - return ( - False, - f"Content hash mismatch for '{plugin_name}'.\n" - f" Local: {local_hash}\n" - f" Expected: {expected_hash} ({hash_field})\n" - f" The local plugin content differs from what was scanned in " - f"claude-marketplace. Ensure you are syncing the exact same files.", - ) - - return True, f"Content hash verified: {local_hash}" - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -def verify_plugin( - plugin_name: str, - manifest: dict | None, -) -> bool: - """Run all checks for a single plugin. Returns True if all pass.""" - print(f"\n--- Plugin: {plugin_name} ---") - - all_passed = True - - # Check 1: Existence - passed, msg, _ = check_existence(plugin_name) - status = "PASS" if passed else "FAIL" - print(f" [{status}] Existence check: {msg}") - if not passed: - all_passed = False - - # Check 2 & 3 require the manifest - if manifest is None: - print(" [FAIL] Security manifest check: security-manifest.json not found " - f"in {MARKETPLACE_REPO} main branch. Ensure the security scan has run.") - print(" [FAIL] Content integrity check: skipped (no manifest)") - return False - - # Check 2: Security manifest status - passed, msg = check_security_manifest(plugin_name, manifest) - status = "PASS" if passed else "FAIL" - print(f" [{status}] Security manifest check: {msg}") - if not passed: - all_passed = False - - # Check 3: Content integrity - passed, msg = check_content_integrity(plugin_name, manifest) - status = "PASS" if passed else "FAIL" - print(f" [{status}] Content integrity check: {msg}") - if not passed: - all_passed = False - - return all_passed - - -def _load_marketplace_plugin_names() -> set[str]: - """Load the set of plugin names registered in marketplace.json.""" - marketplace_path = REPO_ROOT / ".claude-plugin" / "marketplace.json" - if not marketplace_path.is_file(): - print(f"Warning: marketplace.json not found at {marketplace_path.relative_to(REPO_ROOT)}") - return set() - - try: - data = json.loads(marketplace_path.read_text()) - except (json.JSONDecodeError, OSError) as exc: - print(f"Warning: Failed to parse marketplace.json: {exc}") - return set() - - return { - p["name"] - for p in data.get("plugins", []) - if isinstance(p, dict) and "name" in p - } - - -def main() -> int: - if len(sys.argv) < 2 or not sys.argv[1].strip(): - print("No plugins to verify. Exiting.") - return 0 - - plugin_names = [n.strip() for n in sys.argv[1].split(",") if n.strip()] - - if not plugin_names: - print("No plugins to verify. Exiting.") - return 0 - - print("=" * 60) - print("Plugin Provenance Verification") - print("=" * 60) - print(f"Repo: {MARKETPLACE_REPO} (ref: {MARKETPLACE_REF})") - print(f"Plugins: {', '.join(plugin_names)}") - - # Load registered plugin names from marketplace.json (used for deletion checks) - registered_plugins = _load_marketplace_plugin_names() - - # Fetch the security manifest once (used by checks 2 & 3) - manifest = fetch_security_manifest() - if manifest is None: - print(f"\nWarning: Could not fetch security-manifest.json from " - f"{MARKETPLACE_REPO} main branch.") - - results: dict[str, bool] = {} - for name in plugin_names: - local_plugin_dir = PLUGINS_DIR / name - if not local_plugin_dir.is_dir(): - if name in registered_plugins: - # Directory is missing but plugin is still in marketplace.json - # — this is likely an accidental deletion. - print(f"\n [FAIL] Plugin '{name}' directory is missing but still " - f"listed in marketplace.json — either restore the plugin or " - f"remove it from marketplace.json") - results[name] = False - else: - # Directory is missing AND plugin has been removed from - # marketplace.json — intentional deletion, safe to skip. - print(f"\n [SKIP] Plugin '{name}' is being removed (directory deleted " - f"and removed from marketplace.json) — no provenance check needed.") - results[name] = True - continue - results[name] = verify_plugin(name, manifest) - - # Summary - passed = sum(1 for v in results.values() if v) - failed = len(results) - passed - - print("\n" + "=" * 60) - print(f"Summary: {passed} passed, {failed} failed out of {len(results)} plugin(s)") - - if failed: - print("\nFailed plugins:") - for name, ok in results.items(): - if not ok: - print(f" ✗ {name}") - print("\nFAILED") - return 1 - - print("\nPASSED") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index 91a0943..0000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Dependency Review - -on: - pull_request: - branches: [main] - -permissions: - contents: read - pull-requests: write - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Dependency Review - uses: actions/dependency-review-action@v4 - with: - fail-on-severity: moderate - comment-summary-in-pr: always diff --git a/.github/workflows/provenance-check.yml b/.github/workflows/provenance-check.yml deleted file mode 100644 index 4a0bb3a..0000000 --- a/.github/workflows/provenance-check.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Provenance Check -on: - pull_request: - paths: - - 'plugins/**' - -permissions: - contents: read - pull-requests: write - -jobs: - provenance: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Identify changed plugins - id: changes - run: | - PLUGINS=$(gh pr diff "$PR_NUMBER" --name-only \ - | grep '^plugins/' \ - | cut -d'/' -f2 \ - | sort -u \ - | tr '\n' ',') - echo "changed_plugins=${PLUGINS%,}" >> "$GITHUB_OUTPUT" - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - GH_TOKEN: ${{ github.token }} - - uses: actions/setup-python@v5 - if: steps.changes.outputs.changed_plugins != '' - with: - python-version: '3.12' - - name: Verify provenance - if: steps.changes.outputs.changed_plugins != '' - run: python .github/scripts/verify-provenance.py "$CHANGED_PLUGINS" - env: - GH_TOKEN: ${{ secrets.PLUGIN_PUBLISH_TOKEN || github.token }} - PROVENANCE_REPO: ${{ vars.PROVENANCE_REPO }} - CHANGED_PLUGINS: ${{ steps.changes.outputs.changed_plugins }} - - name: Post failure comment - if: failure() - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - GH_TOKEN: ${{ github.token }} - run: | - gh pr comment "$PR_NUMBER" --body "## Provenance Check Failed - - One or more plugins in this PR could not be verified against the internal marketplace. - - **What this means:** Plugins must first be merged to the internal marketplace repo and pass security scanning before they can be published here. - - **How to fix:** - 1. Ensure your plugin is merged to \`main\` in the internal marketplace repo - 2. Wait for the security scan workflow to pass and generate the manifest - 3. Ensure the plugin content here is an exact copy (no modifications) - - See the workflow logs above for specific details on which checks failed." diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml deleted file mode 100644 index 243237b..0000000 --- a/.github/workflows/validate.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Validate Plugins -on: - pull_request: - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Check for plugin changes - id: filter - run: | - CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep -c '^plugins/\|^\.claude-plugin/marketplace\.json\|^\.github/schemas/\|^\.github/scripts/\|^\.github/workflows/validate\.yml' || true) - echo "has_plugin_changes=$([ "$CHANGED" -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ github.token }} - - uses: actions/setup-python@v5 - if: steps.filter.outputs.has_plugin_changes == 'true' - with: - python-version: '3.12' - - run: pip install yamllint check-jsonschema - if: steps.filter.outputs.has_plugin_changes == 'true' - - name: Validate command frontmatter - if: steps.filter.outputs.has_plugin_changes == 'true' - run: python .github/scripts/validate-frontmatter.py - - name: Validate plugin manifests - if: steps.filter.outputs.has_plugin_changes == 'true' - run: | - manifests=$(find plugins -name "plugin.json" -path "*/.claude-plugin/*") - if [ -z "$manifests" ]; then - echo "ERROR: No plugin manifests found. Check that the script is run from the repo root." - exit 1 - fi - echo "$manifests" | xargs check-jsonschema --schemafile .github/schemas/plugin-manifest.schema.json - - name: Validate marketplace registry - if: steps.filter.outputs.has_plugin_changes == 'true' - run: python .github/scripts/validate-marketplace.py diff --git a/.yamllint.yml b/.yamllint.yml deleted file mode 100644 index 3312f4f..0000000 --- a/.yamllint.yml +++ /dev/null @@ -1,7 +0,0 @@ -extends: default -rules: - line-length: disable - document-start: disable - truthy: disable - comments-indentation: disable - trailing-spaces: disable diff --git a/README.md b/README.md index 735fcc0..55eec86 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # Carta Plugins +> **Read-only mirror.** This repo is automatically published from an internal repository. Do not open pull requests here — all development and review happens internally. + The official repository of Carta plugins for AI Agents, as a [Claude Plugin Marketplace](https://code.claude.com/docs/en/discover-plugins). ## Documentation