mirror of
https://github.com/superdesigndev/treg.git
synced 2026-10-02 03:24:35 +08:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBkwAP4DPMJsxBRmG1UGSb
201 lines
8.5 KiB
Python
201 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the MiniMax plugin at `plugins/minimax/` against the marketplace's intake rules, and zip it.
|
|
|
|
The rules are MiniMax's (docs/MINIMAX-PLUGIN.md carries the source): `.minimax-plugin/plugin.json`
|
|
at the package root with no wrapper directory; ASCII-only paths; no symlinks, hardlinks, install
|
|
scripts, executables or native binaries; UTF-8 without BOM; JSON objects without duplicate keys;
|
|
every capability file referenced by the manifest and every referenced file present; no credential
|
|
anywhere in the package. Their validator runs after submission and reports back over Feishu —
|
|
this one runs before, in the test suite.
|
|
|
|
Usage: python3 scripts/minimax_plugin.py --check # validate only (the test)
|
|
python3 scripts/minimax_plugin.py --zip OUT.zip # validate, then write a submission ZIP
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PLUGIN = ROOT / "plugins" / "minimax"
|
|
MANIFEST = PLUGIN / ".minimax-plugin" / "plugin.json"
|
|
|
|
CATEGORIES = {"Office", "Studio", "Design & Sites", "Code", "Business", "Sales", "Productivity",
|
|
"Science & Healthcare", "Education", "Other"}
|
|
NAME_RE = re.compile(r"^[a-z][a-z0-9._-]{0,79}$")
|
|
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
|
PATH_RE = re.compile(r"^[A-Za-z0-9._/-]+$")
|
|
ICON_EXT = {".png", ".jpg", ".jpeg", ".webp"}
|
|
# Things that must never ship. The frontmatter and prose talk ABOUT tokens, so match value shapes.
|
|
SECRET_RE = re.compile(r"(sk-[A-Za-z0-9]{20,}|treg_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|"
|
|
r"gh[pousr]_[A-Za-z0-9]{30,}|xox[baprs]-[A-Za-z0-9-]{20,}|-----BEGIN [A-Z ]*PRIVATE KEY)")
|
|
INSTALL_SCRIPT_EXT = {".sh", ".bash", ".zsh", ".bat", ".cmd", ".ps1", ".exe", ".dll", ".so", ".dylib"}
|
|
|
|
|
|
def package_version() -> str:
|
|
for line in (ROOT / "pyproject.toml").read_text(encoding="utf-8").splitlines():
|
|
if line.startswith("version = "):
|
|
return line.split('"')[1]
|
|
raise SystemExit("pyproject.toml has no version")
|
|
|
|
|
|
def no_dup_keys(pairs):
|
|
seen = set()
|
|
for k, _ in pairs:
|
|
if k in seen:
|
|
raise ValueError(f"duplicate key {k!r}")
|
|
seen.add(k)
|
|
return dict(pairs)
|
|
|
|
|
|
def files() -> list[Path]:
|
|
return sorted(p for p in PLUGIN.rglob("*") if not p.is_dir() or p.is_symlink())
|
|
|
|
|
|
def validate() -> list[str]:
|
|
errors: list[str] = []
|
|
e = errors.append
|
|
if not MANIFEST.is_file():
|
|
return [f"missing {MANIFEST.relative_to(ROOT)}"]
|
|
|
|
try:
|
|
m = json.loads(MANIFEST.read_text(encoding="utf-8"), object_pairs_hook=no_dup_keys)
|
|
except ValueError as exc:
|
|
return [f"plugin.json: {exc}"]
|
|
if not isinstance(m, dict):
|
|
return ["plugin.json must be a JSON object"]
|
|
|
|
if m.get("schemaVersion") != 1:
|
|
e("schemaVersion must be 1")
|
|
if not NAME_RE.match(str(m.get("name", ""))):
|
|
e(f"name {m.get('name')!r} must be lowercase, start with a letter, <=80 chars of [a-z0-9._-]")
|
|
if not SEMVER_RE.match(str(m.get("version", ""))):
|
|
e("version must be SemVer")
|
|
elif m["version"] != package_version():
|
|
e(f"version {m['version']} != pyproject {package_version()}")
|
|
for field in ("description", "author", "icon", "category"):
|
|
if not m.get(field):
|
|
e(f"{field} is required")
|
|
if m.get("category") not in CATEGORIES:
|
|
e(f"category {m.get('category')!r} not one of {sorted(CATEGORIES)}")
|
|
eq = m.get("exampleQueries", [])
|
|
if not isinstance(eq, list) or len(eq) > 3 or any(not isinstance(q, str) or not q.strip()
|
|
or len(q) > 4096 for q in eq):
|
|
e("exampleQueries must be 0-3 non-empty strings of <=4096 chars")
|
|
for cap in ("apps", "mcpServers", "skills"):
|
|
if not isinstance(m.get(cap), list):
|
|
e(f"{cap} must be present (an empty array when unused)")
|
|
if not any(m.get(cap) for cap in ("apps", "mcpServers", "skills")):
|
|
e("a plugin must contain at least one capability")
|
|
|
|
icon = m.get("icon", "")
|
|
if Path(icon).suffix not in ICON_EXT or Path(icon).suffix != Path(icon).suffix.lower():
|
|
e(f"icon {icon!r} must be a lowercase .png/.jpg/.jpeg/.webp")
|
|
|
|
referenced = set()
|
|
for rel in [icon, *m.get("apps", []), *m.get("mcpServers", []), *m.get("skills", [])]:
|
|
if not isinstance(rel, str) or rel.startswith("/") or ".." in rel.split("/"):
|
|
e(f"{rel!r} must be a relative path inside the package")
|
|
continue
|
|
referenced.add(rel)
|
|
if not (PLUGIN / rel).is_file():
|
|
e(f"manifest references {rel}, which does not exist")
|
|
|
|
for rel in m.get("skills", []):
|
|
p = PLUGIN / rel
|
|
if not p.is_file():
|
|
continue
|
|
text = p.read_text(encoding="utf-8")
|
|
parts = text.split("---", 2)
|
|
if not text.startswith("---\n") or len(parts) < 3:
|
|
e(f"{rel}: must begin with YAML frontmatter")
|
|
continue
|
|
fm = {k.strip(): v.strip() for k, _, v in
|
|
(line.partition(":") for line in parts[1].strip().splitlines())}
|
|
if fm.get("name") != p.parent.name:
|
|
e(f"{rel}: frontmatter name {fm.get('name')!r} != directory {p.parent.name!r}")
|
|
if not fm.get("description"):
|
|
e(f"{rel}: frontmatter description is empty")
|
|
if not re.fullmatch(r"skills/[A-Za-z0-9._-]+/SKILL\.md", rel):
|
|
e(f"{rel}: skills live at skills/<name>/SKILL.md")
|
|
|
|
# Every capability-shaped file must be referenced, and every file must obey the package rules.
|
|
for p in files():
|
|
rel = p.relative_to(PLUGIN).as_posix()
|
|
if p.is_symlink():
|
|
e(f"{rel}: symlinks are rejected")
|
|
continue
|
|
if not PATH_RE.match(rel):
|
|
e(f"{rel}: paths may only use ASCII letters, digits, . _ - and /")
|
|
if len(rel.encode()) > 512 or any(len(seg.encode()) > 128 for seg in rel.split("/")) \
|
|
or rel.count("/") + 1 > 16:
|
|
e(f"{rel}: path too long or too deep")
|
|
if p.stat().st_nlink > 1:
|
|
e(f"{rel}: hardlinks are rejected")
|
|
if p.stat().st_mode & 0o111:
|
|
e(f"{rel}: executable bit set")
|
|
if p.suffix.lower() in INSTALL_SCRIPT_EXT:
|
|
e(f"{rel}: install scripts / binaries are rejected")
|
|
if p.stat().st_size > 16 * 1024 * 1024:
|
|
e(f"{rel}: over 16 MiB")
|
|
if p.name.endswith((".mcp.json", ".app.json")) or p.name == "SKILL.md":
|
|
if rel not in referenced:
|
|
e(f"{rel}: capability file not referenced by the manifest")
|
|
if p.suffix in {".md", ".json", ".txt", ".yml", ".yaml", ".py", ".js"}:
|
|
raw = p.read_bytes()
|
|
if raw.startswith(b"\xef\xbb\xbf"):
|
|
e(f"{rel}: UTF-8 BOM")
|
|
try:
|
|
text = raw.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
e(f"{rel}: not UTF-8")
|
|
continue
|
|
if SECRET_RE.search(text):
|
|
e(f"{rel}: looks like it contains a credential")
|
|
if p.suffix == ".json":
|
|
try:
|
|
if not isinstance(json.loads(text, object_pairs_hook=no_dup_keys), dict):
|
|
e(f"{rel}: JSON must be an object")
|
|
except ValueError as exc:
|
|
e(f"{rel}: {exc}")
|
|
|
|
regular = [p for p in files() if not p.is_symlink()]
|
|
if len(regular) > 1024:
|
|
e("more than 1,024 files")
|
|
if sum(p.stat().st_size for p in regular) > 64 * 1024 * 1024:
|
|
e("more than 64 MiB uncompressed")
|
|
return errors
|
|
|
|
|
|
def write_zip(out: Path) -> None:
|
|
# No wrapper directory: entries are relative to the plugin root, so
|
|
# `.minimax-plugin/plugin.json` is a top-level entry. Deterministic order and timestamps.
|
|
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
|
for p in files():
|
|
rel = p.relative_to(PLUGIN).as_posix()
|
|
info = zipfile.ZipInfo(rel, date_time=(2020, 1, 1, 0, 0, 0))
|
|
info.compress_type = zipfile.ZIP_DEFLATED
|
|
info.external_attr = 0o644 << 16
|
|
z.writestr(info, p.read_bytes())
|
|
print(f"wrote {out} ({out.stat().st_size} bytes, {len(files())} files)")
|
|
|
|
|
|
def main() -> int:
|
|
errors = validate()
|
|
for err in errors:
|
|
print(f"FAIL — {err}", file=sys.stderr)
|
|
if errors:
|
|
return 1
|
|
print(f"OK — {PLUGIN.relative_to(ROOT)}/ passes the MiniMax package rules")
|
|
if "--zip" in sys.argv:
|
|
write_zip(Path(sys.argv[sys.argv.index("--zip") + 1]))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|