from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any
VALID_PLUGIN_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,63}$')
VALID_VERSION_RE = re.compile(r'^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$')
REQUIRED_MANIFEST_FIELDS = ('name',)
@dataclass
class ManifestValidationError:
field: str
message: str
def validate_manifest(manifest: dict[str, Any]) -> list[ManifestValidationError]:
errors: list[ManifestValidationError] = []
if not isinstance(manifest, dict):
errors.append(ManifestValidationError('root', 'Manifest must be a JSON object'))
return errors
for f in REQUIRED_MANIFEST_FIELDS:
if f not in manifest:
errors.append(ManifestValidationError(f, f"Required field '{f}' is missing"))
name = manifest.get('name')
if isinstance(name, str):
if not VALID_PLUGIN_NAME_RE.match(name):
errors.append(
ManifestValidationError(
'name',
f"Plugin name '{name}' is invalid. "
f'Must start with a letter, contain only [a-zA-Z0-9_-], max 64 chars.',
)
)
elif name is not None:
errors.append(ManifestValidationError('name', 'Plugin name must be a string'))
version = manifest.get('version')
if version is not None:
if not isinstance(version, str):
errors.append(ManifestValidationError('version', 'Version must be a string'))
elif not VALID_VERSION_RE.match(version):
errors.append(
ManifestValidationError(
'version', f"Version '{version}' is not valid semver (expected X.Y.Z)"
)
)
description = manifest.get('description')
if description is not None and not isinstance(description, str):
errors.append(ManifestValidationError('description', 'Description must be a string'))
hooks = manifest.get('hooks')
if hooks is not None and not isinstance(hooks, dict):
errors.append(ManifestValidationError('hooks', 'Hooks must be a JSON object'))
mcp_servers = manifest.get('mcp_servers')
if mcp_servers is not None and not isinstance(mcp_servers, dict):
errors.append(ManifestValidationError('mcp_servers', 'mcp_servers must be a JSON object'))
permissions = manifest.get('permissions')
if permissions is not None:
if not isinstance(permissions, list):
errors.append(ManifestValidationError('permissions', 'Permissions must be an array'))
else:
valid_perms = {'read', 'write', 'execute', 'network', 'mcp'}
for perm in permissions:
if perm not in valid_perms:
errors.append(
ManifestValidationError('permissions', f"Unknown permission '{perm}'")
)
dependencies = manifest.get('dependencies')
if dependencies is not None:
if not isinstance(dependencies, dict):
errors.append(
ManifestValidationError('dependencies', 'Dependencies must be a JSON object')
)
else:
for dep_name, dep_version in dependencies.items():
if not isinstance(dep_name, str) or not isinstance(dep_version, str):
errors.append(
ManifestValidationError(
'dependencies',
f"Dependency '{dep_name}' must have string name and version",
)
)
return errors
def is_valid_manifest(manifest: dict[str, Any]) -> bool:
return len(validate_manifest(manifest)) == 0