- serve.py: URL-Encoding fix, Body-Limit (10MB), Port-Reuse, TOCTOU-Race, Logging - index.html: XSS in renderTemplates geheilt, Escape-Key für Edit-Modal, Accessibility - templates.json: Pfade ohne ../, leere Descriptions ergänzt - validate.py: categories/ entfernt, CLI-Flags umbenannt, zu mutually_exclusive_group - smoke_test.sh: set -euo pipefail, Port-Validation, Timeout 5s, code-Fallback - cleanup_server.sh: lsof statt pgrep, Graceful-Term + SIGKILL-Fallback - agent_verify.sh: set -euo pipefail, ROOT-Pfade, dynamischer Port, grep-Crash fix - AGENTS.md: history/ entfernt, Pfad-Schema präzisiert - README.md: categories/ entfernt, Web-Ansicht + API-Endpunkte hinzugefügt
299 lines
10 KiB
Python
Executable file
299 lines
10 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Prompt Template Validator
|
|
|
|
Validiert JSON- und Markdown-Templates gegen definierte Schemas.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
|
|
# JSON Schema für JSON-Templates
|
|
JSON_SCHEMA = {
|
|
"required": ["name", "template", "variables"],
|
|
"properties": {
|
|
"name": {"type": "string", "minLength": 1},
|
|
"version": {"type": "string", "pattern": r"^\d+\.\d+$"},
|
|
"description": {"type": "string"},
|
|
"role": {"type": "string"},
|
|
"template": {"type": "string", "minLength": 1},
|
|
"variables": {
|
|
"type": "object",
|
|
"patternProperties": {
|
|
r"^.+$": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {
|
|
"type": "string",
|
|
"enum": ["string", "number", "enum", "boolean"],
|
|
},
|
|
"required": {"type": "boolean"},
|
|
"default": {},
|
|
"description": {"type": "string"},
|
|
"values": {"type": "array"},
|
|
},
|
|
"required": ["type"],
|
|
}
|
|
},
|
|
},
|
|
"tags": {"type": "array", "items": {"type": "string"}},
|
|
"language": {"type": "string", "enum": ["de", "en", "fr", "es", "any"]},
|
|
},
|
|
}
|
|
|
|
# Muster für Markdown-Templates
|
|
MD_REQUIRED_SECTIONS = ["Template", "Variablen"]
|
|
|
|
|
|
def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
|
|
"""Validiert ein JSON-Template."""
|
|
errors = []
|
|
|
|
try:
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except json.JSONDecodeError as e:
|
|
return False, [f"❌ JSON Syntax Error: {e}"]
|
|
except Exception as e:
|
|
return False, [f"❌ Datei kann nicht gelesen werden: {e}"]
|
|
|
|
# Schemas validieren
|
|
if not isinstance(data, dict):
|
|
return False, ["❌ Root muss ein Object sein"]
|
|
|
|
# Required Felder
|
|
for field in JSON_SCHEMA.get("required", []):
|
|
if field not in data:
|
|
errors.append(f"❌ Fehlendes Pflichtfeld: '{field}'")
|
|
|
|
# Feld-Typen validieren
|
|
for field, schema in JSON_SCHEMA.get("properties", {}).items():
|
|
if field in data:
|
|
field_type = schema.get("type")
|
|
if field_type == "string" and not isinstance(data[field], str):
|
|
errors.append(f"❌ Feld '{field}' muss ein String sein")
|
|
elif field_type == "object" and not isinstance(data[field], dict):
|
|
errors.append(f"❌ Feld '{field}' muss ein Object sein")
|
|
elif field_type == "array" and not isinstance(data[field], list):
|
|
errors.append(f"❌ Feld '{field}' muss ein Array sein")
|
|
|
|
# Pattern validieren
|
|
if "pattern" in schema and isinstance(data[field], str):
|
|
if not re.match(schema["pattern"], data[field]):
|
|
errors.append(
|
|
f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}"
|
|
)
|
|
|
|
# Template prüfen
|
|
if "template" in data:
|
|
template = data["template"]
|
|
if not isinstance(template, str):
|
|
errors.append("❌ Template muss ein String sein")
|
|
else:
|
|
# Variablen im Template prüfen
|
|
if "variables" in data:
|
|
template_vars = set(re.findall(r"\{(\w+)\}", template))
|
|
defined_vars = set(data["variables"].keys())
|
|
undefined_vars = template_vars - defined_vars
|
|
if undefined_vars:
|
|
errors.append(
|
|
f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}"
|
|
)
|
|
|
|
# Variablen validieren
|
|
if "variables" in data:
|
|
variables = data["variables"]
|
|
if not isinstance(variables, dict):
|
|
errors.append("❌ Variables muss ein Object sein")
|
|
else:
|
|
for var_name, var_schema in variables.items():
|
|
if not isinstance(var_schema, dict):
|
|
errors.append(f"❌ Variable '{var_name}' muss ein Object sein")
|
|
continue
|
|
if "type" not in var_schema:
|
|
errors.append(f"❌ Variable '{var_name}' benötigt ein 'type' Feld")
|
|
if var_schema.get("type") == "enum" and "values" not in var_schema:
|
|
errors.append(
|
|
f"❌ Enum Variable '{var_name}' benötigt 'values' Array"
|
|
)
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]:
|
|
"""Validiert ein Markdown-Template."""
|
|
errors = []
|
|
|
|
try:
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
except Exception as e:
|
|
return False, [f"❌ Datei kann nicht gelesen werden: {e}"]
|
|
|
|
# Mindestlänge
|
|
if len(content.strip()) < 50:
|
|
errors.append("⚠️ Warnung: Template zu kurz (mind. 50 Zeichen)")
|
|
|
|
# Titel prüfen
|
|
if not content.startswith("# "):
|
|
errors.append("❌ Fehlender Titel (erwartet: # Titel)")
|
|
|
|
# Pflichtabschnitte prüfen
|
|
for section in MD_REQUIRED_SECTIONS:
|
|
if f"## {section}" not in content and f"# {section}" not in content:
|
|
errors.append(f"❌ Fehlender Abschnitt: {section}")
|
|
|
|
# Variablen-Tabelle prüfen
|
|
if "Variablen" in content:
|
|
var_section_start = content.find("## Variablen")
|
|
if var_section_start == -1:
|
|
var_section_start = content.find("# Variablen")
|
|
|
|
if var_section_start != -1:
|
|
var_section = content[var_section_start : var_section_start + 500]
|
|
if "| Variable |" not in var_section:
|
|
errors.append("❌ Variablen-Tabelle nicht im korrekten Format")
|
|
|
|
# Template-Block prüfen
|
|
if "Template" in content:
|
|
template_block_start = content.find("```")
|
|
if template_block_start != -1:
|
|
# Finde den Code-Block nach "## Template" oder "# Template"
|
|
template_section = content.find("## Template", content.find("Template"))
|
|
if template_section == -1:
|
|
template_section = content.find("# Template", content.find("Template"))
|
|
if template_section > template_block_start:
|
|
template_block_start = template_section
|
|
if template_block_start == -1:
|
|
errors.append("❌ Kein Code-Block für Template gefunden")
|
|
else:
|
|
# Prüfe ob Variablen im Template sind
|
|
template_content = content[template_block_start:]
|
|
if "{" not in template_content or "}" not in template_content:
|
|
errors.append(
|
|
"⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden"
|
|
)
|
|
|
|
return len([e for e in errors if e.startswith("❌")]) == 0, errors
|
|
|
|
|
|
def validate_template(filepath: Path) -> Tuple[bool, List[str]]:
|
|
"""Validiert ein Template basierend auf der Dateiendung."""
|
|
if filepath.suffix.lower() == ".json":
|
|
return validate_json_template(filepath)
|
|
elif filepath.suffix.lower() == ".md":
|
|
return validate_md_template(filepath)
|
|
else:
|
|
return False, [f"❌ Unsupported file type: {filepath.suffix}"]
|
|
|
|
|
|
ALLOWED_DIRS = {"templates/system", "templates/user", "templates/custom", "categories"}
|
|
|
|
|
|
def find_templates(directory: Path) -> List[Path]:
|
|
"""Findet alle Template-Dateien in einem Verzeichnis Baum."""
|
|
templates = []
|
|
for root, dirs, files in os.walk(directory):
|
|
rel = os.path.relpath(root, directory)
|
|
if rel != "." and rel.split(os.sep)[0] not in ALLOWED_DIRS:
|
|
dirs.clear() # Skip nicht-erlaubte Subdirs
|
|
continue
|
|
for file in files:
|
|
if file.endswith((".json", ".md")):
|
|
templates.append(Path(root) / file)
|
|
return templates
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Validiert Prompt-Templates",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Beispiele:
|
|
python validate.py templates/system/code_reviewer.json
|
|
python validate.py --all
|
|
python validate.py --json templates/
|
|
""",
|
|
)
|
|
parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis")
|
|
parser.add_argument("--all", action="store_true", help="Alle Templates validieren")
|
|
type_group = parser.add_mutually_exclusive_group()
|
|
type_group.add_argument(
|
|
"--type-json", action="store_true", help="Nur JSON-Templates validieren"
|
|
)
|
|
type_group.add_argument(
|
|
"--type-md", action="store_true", help="Nur Markdown-Templates validieren"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
base_dir = Path(__file__).parent.parent
|
|
|
|
if args.all:
|
|
# Alle Templates finden
|
|
templates = find_templates(base_dir / "templates")
|
|
|
|
if args.type_json:
|
|
templates = [t for t in templates if t.suffix == ".json"]
|
|
if args.type_md:
|
|
templates = [t for t in templates if t.suffix == ".md"]
|
|
|
|
elif args.path:
|
|
path = Path(args.path)
|
|
if path.is_dir():
|
|
templates = find_templates(path)
|
|
if args.type_json:
|
|
templates = [t for t in templates if t.suffix == ".json"]
|
|
if args.type_md:
|
|
templates = [t for t in templates if t.suffix == ".md"]
|
|
else:
|
|
templates = [path]
|
|
else:
|
|
print("❌ Bitte Pfad angeben oder --all verwenden")
|
|
print("Beispiel: python validate.py --all")
|
|
sys.exit(1)
|
|
|
|
if not templates:
|
|
print("❌ Keine Templates gefunden")
|
|
sys.exit(1)
|
|
|
|
# Validierung
|
|
total = len(templates)
|
|
valid = 0
|
|
invalid = 0
|
|
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Validiere {total} Template(s)...\n")
|
|
|
|
for template_path in sorted(templates):
|
|
is_valid, errors = validate_template(template_path)
|
|
# Make path relative to base_dir, handling both absolute and relative paths
|
|
try:
|
|
rel_path = str(template_path.relative_to(base_dir))
|
|
except ValueError:
|
|
# If template_path is not under base_dir, use absolute path
|
|
rel_path = str(template_path)
|
|
|
|
if is_valid:
|
|
print(f"✅ {rel_path}")
|
|
valid += 1
|
|
else:
|
|
print(f"❌ {rel_path}")
|
|
for error in errors:
|
|
print(f" {error}")
|
|
invalid += 1
|
|
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Ergebnis: {valid} ✅ | {invalid} ❌ | {total} Total")
|
|
|
|
sys.exit(0 if invalid == 0 else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|