← Files PineconeARCHIVED FILE

scripts/check_manifest.py

3.4 KB · Oct 2, 2026 · 00:35 UTC

↓ Download file

#!/usr/bin/env python3
"""Validate .codex-plugin/plugin.json against the Codex plugin spec.

Run from repo root:
    python3 scripts/check_manifest.py
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
MANIFEST = REPO / ".codex-plugin" / "plugin.json"

KEBAB = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
SEMVER = re.compile(r"^\d+\.\d+\.\d+$")
HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$")


def fail(msg: str, errors: list[str]) -> None:
    errors.append(msg)


def check_path(value: str, field: str, errors: list[str]) -> None:
    if not value.startswith("./"):
        fail(f"{field}: path {value!r} must start with './'", errors)
        return
    resolved = (REPO / value.removeprefix("./")).resolve()
    try:
        resolved.relative_to(REPO)
    except ValueError:
        fail(f"{field}: path {value!r} escapes the plugin root", errors)
        return
    if not resolved.exists():
        fail(f"{field}: path {value!r} does not resolve to an existing file or directory", errors)


def main() -> int:
    errors: list[str] = []

    if not MANIFEST.exists():
        print(f"FAIL: {MANIFEST} does not exist", file=sys.stderr)
        return 1

    try:
        data = json.loads(MANIFEST.read_text())
    except json.JSONDecodeError as e:
        print(f"FAIL: {MANIFEST} is not valid JSON: {e}", file=sys.stderr)
        return 1

    for required in ("name", "version", "description", "skills"):
        if required not in data:
            fail(f"missing required field: {required}", errors)

    name = data.get("name", "")
    if name and not KEBAB.match(name):
        fail(f"name {name!r} is not lowercase kebab-case", errors)

    version = data.get("version", "")
    if version and not SEMVER.match(version):
        fail(f"version {version!r} does not match semver X.Y.Z", errors)

    # Required per OpenAI support (2026-09-21). Without it the plugin's skills and
    # MCP server do not get a local executor, so the bundled scripts cannot run.
    # It must be the boolean true, not the string "true".
    if data.get("requires_local_executor") is not True:
        fail(
            "requires_local_executor must be present at the top level and set to true, "
            f"got {data.get('requires_local_executor')!r}",
            errors,
        )

    for field in ("skills", "mcpServers", "apps", "hooks"):
        value = data.get(field)
        if isinstance(value, str):
            check_path(value, field, errors)
        elif isinstance(value, list):
            for i, item in enumerate(value):
                if isinstance(item, str):
                    check_path(item, f"{field}[{i}]", errors)

    interface = data.get("interface", {})
    for field in ("composerIcon", "logo"):
        if field in interface:
            check_path(interface[field], f"interface.{field}", errors)
    for i, shot in enumerate(interface.get("screenshots", [])):
        check_path(shot, f"interface.screenshots[{i}]", errors)

    brand = interface.get("brandColor")
    if brand and not HEX_COLOR.match(brand):
        fail(f"interface.brandColor {brand!r} must be #RRGGBB", errors)

    if errors:
        print("plugin.json validation failed:", file=sys.stderr)
        for e in errors:
            print(f"  - {e}", file=sys.stderr)
        return 1

    print(f"OK: {MANIFEST.relative_to(REPO)}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

SHA-256: 7205df1455d428da34032dd67e511c8c64a6cb9071ccb251d7afa9fd60da6993