95 lines
4.0 KiB
Python
95 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import yaml
|
|
|
|
|
|
VALID_EXECUTION_CONTEXTS = {"inline", "fork"}
|
|
VALID_SHELLS = {"bash", "powershell"}
|
|
VALID_SOURCE_TIERS = {"local", "managed", "plugin", "remote"}
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Validate a skill package.")
|
|
parser.add_argument("skill_dir")
|
|
args = parser.parse_args()
|
|
|
|
root = Path(args.skill_dir).resolve()
|
|
failures = []
|
|
warnings = []
|
|
skill_md = root / "SKILL.md"
|
|
interface = root / "agents" / "interface.yaml"
|
|
manifest = root / "manifest.json"
|
|
|
|
if not skill_md.exists():
|
|
failures.append("Missing SKILL.md")
|
|
if not interface.exists():
|
|
failures.append("Missing agents/interface.yaml")
|
|
|
|
if skill_md.exists():
|
|
text = skill_md.read_text(encoding="utf-8")
|
|
if not text.startswith("---"):
|
|
failures.append("SKILL.md missing frontmatter")
|
|
else:
|
|
parts = text.split("---", 2)
|
|
data = yaml.safe_load(parts[1]) or {}
|
|
for field in ("name", "description"):
|
|
if not data.get(field):
|
|
failures.append(f"Missing frontmatter field: {field}")
|
|
|
|
if interface.exists():
|
|
data = yaml.safe_load(interface.read_text(encoding="utf-8")) or {}
|
|
meta = data.get("interface", {})
|
|
compatibility = data.get("compatibility", {})
|
|
activation = compatibility.get("activation", {})
|
|
execution = compatibility.get("execution", {})
|
|
trust = compatibility.get("trust", {})
|
|
degradation = compatibility.get("degradation", {})
|
|
for field in ("display_name", "short_description", "default_prompt"):
|
|
if not meta.get(field):
|
|
failures.append(f"Missing interface field: {field}")
|
|
if not compatibility.get("canonical_format"):
|
|
failures.append("Missing compatibility field: canonical_format")
|
|
adapter_targets = compatibility.get("adapter_targets")
|
|
if not adapter_targets or not isinstance(adapter_targets, list):
|
|
failures.append("Missing compatibility field: adapter_targets")
|
|
adapter_targets = []
|
|
if not activation.get("mode"):
|
|
failures.append("Missing compatibility.activation.mode")
|
|
if activation.get("mode") == "path_scoped" and not activation.get("paths"):
|
|
failures.append("path_scoped activation requires compatibility.activation.paths")
|
|
if execution.get("context") not in VALID_EXECUTION_CONTEXTS:
|
|
failures.append("Invalid compatibility.execution.context")
|
|
if execution.get("shell") not in VALID_SHELLS:
|
|
failures.append("Invalid compatibility.execution.shell")
|
|
if trust.get("source_tier") not in VALID_SOURCE_TIERS:
|
|
failures.append("Invalid compatibility.trust.source_tier")
|
|
if trust.get("remote_inline_execution") not in {"forbid", "allow"}:
|
|
failures.append("Invalid compatibility.trust.remote_inline_execution")
|
|
if not trust.get("remote_metadata_policy"):
|
|
failures.append("Missing compatibility.trust.remote_metadata_policy")
|
|
for target in adapter_targets:
|
|
if target not in degradation:
|
|
failures.append(f"Missing compatibility.degradation entry for target: {target}")
|
|
|
|
if manifest.exists():
|
|
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
for field in ("name", "version", "owner", "updated_at"):
|
|
if not data.get(field):
|
|
failures.append(f"Missing manifest field: {field}")
|
|
if not data.get("review_cadence"):
|
|
warnings.append("Manifest exists without review_cadence.")
|
|
if not data.get("status"):
|
|
warnings.append("Manifest exists without status.")
|
|
if not data.get("maturity_tier"):
|
|
warnings.append("Manifest exists without maturity_tier.")
|
|
|
|
print(json.dumps({"ok": not failures, "failures": failures, "warnings": warnings}, ensure_ascii=False, indent=2))
|
|
if failures:
|
|
raise SystemExit(2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|