Publish lean skill market snapshot
Constraint: This repository is a skill distribution snapshot, so download size matters more than historical auditability. Rejected: Preserve prior Git history | Old large blobs keep clone size high and do not help skill consumers. Confidence: high Scope-risk: broad Directive: Future market updates should be validated, then collapsed to a fresh snapshot commit before push. Tested: python scripts/validate_marketplace.py; validate_plugin.py plugins/codex/plugins/ppt-master; uv run python -m pytest backend/tests/test_skill_market_publish.py backend/tests/test_skill_market_scheduler.py -q; node web/tests/skills-market-ui.test.mjs; git diff --check; Codex temp install/use test for ppt-master Not-tested: PowerShell execution because pwsh/powershell is not installed locally.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate this repository's Codex marketplace layout.
|
||||
|
||||
This is intentionally narrower than Codex's internal plugin validator. It checks
|
||||
the structure this repo generates so sync automation can fail before
|
||||
committing malformed marketplace data.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
MARKETPLACES = [
|
||||
REPO_ROOT / ".agents" / "plugins" / "marketplace.json",
|
||||
REPO_ROOT / "plugins" / "codex" / ".agents" / "plugins" / "marketplace.json",
|
||||
]
|
||||
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$")
|
||||
|
||||
|
||||
def load_json(path: Path) -> object:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except UnicodeDecodeError as exc:
|
||||
raise AssertionError(f"{path} is not valid UTF-8: {exc}") from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AssertionError(f"{path} is not valid JSON: {exc}") from exc
|
||||
|
||||
|
||||
def require(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def validate_manifest(plugin_dir: Path, expected_name: str) -> None:
|
||||
manifest_path = plugin_dir / ".codex-plugin" / "plugin.json"
|
||||
require(manifest_path.exists(), f"Missing manifest: {manifest_path}")
|
||||
manifest = load_json(manifest_path)
|
||||
require(isinstance(manifest, dict), f"Manifest must be an object: {manifest_path}")
|
||||
require(manifest.get("name") == expected_name, f"{manifest_path} name must be {expected_name}")
|
||||
require(SEMVER_RE.match(str(manifest.get("version", ""))) is not None, f"{manifest_path} version must be semver-like")
|
||||
require(bool(manifest.get("description")), f"{manifest_path} missing description")
|
||||
require("hooks" not in manifest, f"{manifest_path} must not declare unsupported hooks field")
|
||||
|
||||
author = manifest.get("author")
|
||||
require(isinstance(author, dict) and bool(author.get("name")), f"{manifest_path} missing author.name")
|
||||
|
||||
skills_rel = manifest.get("skills")
|
||||
require(isinstance(skills_rel, str) and skills_rel.startswith("./"), f"{manifest_path} skills path must be relative ./...")
|
||||
skills_dir = plugin_dir / skills_rel[2:]
|
||||
require(skills_dir.exists(), f"{manifest_path} skills directory does not exist: {skills_dir}")
|
||||
skill_dirs = [path for path in skills_dir.iterdir() if path.is_dir()]
|
||||
require(bool(skill_dirs), f"{manifest_path} must contain at least one skill directory")
|
||||
for skill_dir in skill_dirs:
|
||||
require((skill_dir / "SKILL.md").exists(), f"Missing SKILL.md in {skill_dir}")
|
||||
|
||||
interface = manifest.get("interface")
|
||||
require(isinstance(interface, dict), f"{manifest_path} missing interface object")
|
||||
for key in ["displayName", "shortDescription", "longDescription", "developerName", "category"]:
|
||||
require(bool(interface.get(key)), f"{manifest_path} missing interface.{key}")
|
||||
capabilities = interface.get("capabilities")
|
||||
require(isinstance(capabilities, list), f"{manifest_path} interface.capabilities must be a list")
|
||||
|
||||
|
||||
def resolve_marketplace_plugin_path(marketplace_path: Path, source_path: str) -> Path:
|
||||
require(source_path.startswith("./"), f"{marketplace_path} plugin source.path must start with ./")
|
||||
return (marketplace_path.parent.parent.parent / source_path[2:]).resolve()
|
||||
|
||||
|
||||
def validate_marketplace(marketplace_path: Path) -> None:
|
||||
require(marketplace_path.exists(), f"Missing marketplace: {marketplace_path}")
|
||||
marketplace = load_json(marketplace_path)
|
||||
require(isinstance(marketplace, dict), f"Marketplace must be an object: {marketplace_path}")
|
||||
require(bool(marketplace.get("name")), f"{marketplace_path} missing name")
|
||||
require(isinstance(marketplace.get("plugins"), list), f"{marketplace_path} plugins must be a list")
|
||||
|
||||
seen: set[str] = set()
|
||||
for entry in marketplace["plugins"]:
|
||||
require(isinstance(entry, dict), f"{marketplace_path} plugin entry must be an object")
|
||||
name = entry.get("name")
|
||||
require(isinstance(name, str) and name, f"{marketplace_path} plugin entry missing name")
|
||||
require(name not in seen, f"{marketplace_path} duplicate plugin name: {name}")
|
||||
seen.add(name)
|
||||
|
||||
source = entry.get("source")
|
||||
require(isinstance(source, dict), f"{marketplace_path} {name} missing source")
|
||||
require(source.get("source") == "local", f"{marketplace_path} {name} source.source must be local")
|
||||
plugin_dir = resolve_marketplace_plugin_path(marketplace_path, str(source.get("path", "")))
|
||||
require(plugin_dir.exists(), f"{marketplace_path} {name} path does not exist: {plugin_dir}")
|
||||
|
||||
policy = entry.get("policy")
|
||||
require(isinstance(policy, dict), f"{marketplace_path} {name} missing policy")
|
||||
require(policy.get("installation") in {"AVAILABLE", "INSTALLED_BY_DEFAULT", "NOT_AVAILABLE"}, f"{marketplace_path} {name} invalid installation policy")
|
||||
require(policy.get("authentication") in {"ON_INSTALL", "ON_USE"}, f"{marketplace_path} {name} invalid authentication policy")
|
||||
require(bool(entry.get("category")), f"{marketplace_path} {name} missing category")
|
||||
|
||||
validate_manifest(plugin_dir, name)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
for marketplace in MARKETPLACES:
|
||||
validate_marketplace(marketplace)
|
||||
except AssertionError as exc:
|
||||
print(f"Validation failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print("Marketplace validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user