fix: 26 kritische + 29 wichtige Sicherheits- und Robustheitsprobleme behoben

- 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
This commit is contained in:
Michael 2026-05-03 14:00:55 +02:00
parent 1845b30992
commit c294336058
9 changed files with 289 additions and 174 deletions

View file

@ -69,8 +69,8 @@ Ein Commit = eine logische Änderung. Commit-Message im Stil der bestehenden His
### Einstieg ### Einstieg
```bash ```bash
python3 web/serve.py # startet Server auf http://localhost:8081 python3 web/serve.py # startet Server auf http://localhost:8081 (Server-Port)
./scripts/smoke_test.sh # temporärer Server + Endpunkt-Check + Cleanup (fuer R4) ./scripts/smoke_test.sh # temporärer Server + Endpunkt-Check + Cleanup (fuer R4, Standard-Port 8082)
./scripts/cleanup_server.sh # beendet hängende Instanzen ./scripts/cleanup_server.sh # beendet hängende Instanzen
python3 scripts/validate.py # validiert Template-Struktur python3 scripts/validate.py # validiert Template-Struktur
``` ```
@ -85,12 +85,14 @@ python3 scripts/validate.py # validiert Template-Struktur
| `templates/system/*.json` | System-Templates | | `templates/system/*.json` | System-Templates |
| `templates/user/*.md` | User-Templates | | `templates/user/*.md` | User-Templates |
| `scripts/validate.py` | Template-Validierung | | `scripts/validate.py` | Template-Validierung |
| `scripts/cleanup_server.sh` | Beendet serve.py auf Port 8081 (Standard) oder angegebenem Port |
| `docs/` | Weitere technische Dokumentation | | `docs/` | Weitere technische Dokumentation |
| `history/CHANGELOG.md` | Änderungs-Chronik |
### Datenschema `templates.json` und Template-Einträge ### Datenschema `templates.json` und Template-Einträge
`web/templates.json` ist eine **Liste** von Einträgen. Schema jedes Eintrags: `web/templates.json` ist eine **Liste** von Einträgen. `path`-Felder sind **relativ zu ROOT** (Projektstamm), OHNE `../` Präfix.
Schema jedes Eintrags:
```json ```json
{ "path": "…", "type": "…", "name": "…", "description": "…", "version": "…", "tags": [], "format": "md|json" } { "path": "…", "type": "…", "name": "…", "description": "…", "version": "…", "tags": [], "format": "md|json" }

View file

@ -13,11 +13,6 @@ prompt_template/
│ ├── user/ # Benutzer-Prompts (Emails, Texte, etc.) │ ├── user/ # Benutzer-Prompts (Emails, Texte, etc.)
│ └── custom/ # Benutzerdefinierte Templates │ └── custom/ # Benutzerdefinierte Templates
├── categories/ # Optional: Kategorisierte Templates
│ ├── marketing/
│ ├── technical/
│ └── creative/
├── scripts/ # Hilfsskripte ├── scripts/ # Hilfsskripte
│ └── validate.py # Template-Validierung │ └── validate.py # Template-Validierung
@ -27,8 +22,33 @@ prompt_template/
--- ---
## Web-Ansicht
Der Server startet mit:
```bash
python3 web/serve.py # http://localhost:8081
```
### API-Endpunkte
| URL | Methode | Beschreibung |
|-----|---------|--------------|
| `/` | GET | Frontend (index.html) |
| `/templates.json` | GET | Katalog (aus web/templates.json) |
| `/templates/system/*.json` | GET | System-Templates |
| `/templates/user/*.md` | GET | User-Templates |
| `/templates/...` | PUT | Template speichern (Content-Type: text/plain) |
### Validierung
```bash
python3 scripts/validate.py --all
```
## Dateiformate ## Dateiformate
#### Einzelnes Template (JSON)
Ein Template-File (z.B. `templates/system/code_reviewer.json`) hat dieses Schema:
### JSON (empfohlen für strukturierte Templates) ### JSON (empfohlen für strukturierte Templates)
```json ```json
{ {
@ -45,6 +65,9 @@ prompt_template/
} }
``` ```
#### Katalog (web/templates.json)
Der Katalog ist eine Liste von Einträgen:
### Markdown (für einfache Templates mit Dokumentation) ### Markdown (für einfache Templates mit Dokumentation)
```markdown ```markdown
# Template Name # Template Name

View file

@ -2,16 +2,17 @@
# Verifikations-Hook fuer dual_agent-Pipeline. # Verifikations-Hook fuer dual_agent-Pipeline.
# Wird nach dem Executor-Lauf aufgerufen; Exit 0 = gruen. # Wird nach dem Executor-Lauf aufgerufen; Exit 0 = gruen.
# stdout/stderr landen bei Retry als Kontext beim Planner. # stdout/stderr landen bei Retry als Kontext beim Planner.
set -u set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FAIL=0 FAIL=0
# --- 1. Statisch: JS-Syntax in index.html ------------------------------ # --- 1. Statisch: JS-Syntax in index.html ------------------------------
if [ -f web/index.html ]; then if [ -f "$ROOT/web/index.html" ]; then
python3 - <<'PY' || FAIL=1 python3 - <<'PY' || FAIL=1
import re, subprocess, tempfile, sys import re, subprocess, tempfile, sys
html = open('web/index.html').read() html = open(sys.argv[1] if len(sys.argv) > 1 else 'web/index.html').read()
for i, s in enumerate(re.findall(r'<script[^>]*>([\s\S]*?)</script>', html)): for i, s in enumerate(re.findall(r'<script[^>]*>([\s\S]*?)</script>', html)):
with tempfile.NamedTemporaryFile('w', suffix='.js', delete=False) as f: with tempfile.NamedTemporaryFile('w', suffix='.js', delete=True) as f:
f.write(s); p = f.name f.write(s); p = f.name
r = subprocess.run(['node', '--check', p], capture_output=True, text=True) r = subprocess.run(['node', '--check', p], capture_output=True, text=True)
if r.returncode != 0: if r.returncode != 0:
@ -19,41 +20,44 @@ for i, s in enumerate(re.findall(r'<script[^>]*>([\s\S]*?)</script>', html)):
sys.exit(1) sys.exit(1)
print('JS syntax OK') print('JS syntax OK')
PY PY
else
echo "FAIL: web/index.html fehlt"
FAIL=1
fi fi
# --- 2. Dynamisch: Smoke-Test (alle Endpunkte) ------------------------- # --- 2. Dynamisch: Smoke-Test (alle Endpunkte) -------------------------
if [ -x scripts/smoke_test.sh ]; then if [ -x "$ROOT/scripts/smoke_test.sh" ]; then
./scripts/smoke_test.sh 8088 || FAIL=1 SMOKE_PORT=$(( $$ % 1000 + 9000 ))
"$ROOT/scripts/smoke_test.sh" "$SMOKE_PORT" || FAIL=1
fi fi
# --- 3. Semantisch: Pseudo-Fix-Muster verbieten ------------------------ # --- 3. Semantisch: Pseudo-Fix-Muster verbieten ------------------------
# Diese Patterns haben wir als No-Op-Fixes im JSON-Editor erlebt. # Diese Patterns haben wir als No-Op-Fixes im JSON-Editor erlebt.
# Ein neuer Bugfix darf sie nicht wieder einfuehren. # Ein neuer Bugfix darf sie nicht wieder einfuehren.
if [ -f web/index.html ]; then if [ -f "$ROOT/web/index.html" ]; then
# No-Op-Pattern: leerer Container + while(firstChild) -> nichts zu bewegen
if grep -qE 'document\.createElement\([^)]+\)[^;]*;[[:space:]]*[^;]*\.appendChild\([^)]*firstChild\)' web/index.html; then
: # zu grob — deaktiviert
fi
# Doppeldeklaration einer Funktion # Doppeldeklaration einer Funktion
for fn in buildJsonForm extractJsonFromForm createJsonEditUI createTextEditUI applyFilters; do for fn in buildJsonForm extractJsonFromForm createJsonEditUI createTextEditUI applyFilters; do
count=$(grep -cE "function[[:space:]]+${fn}\\b" web/index.html || true) count=$(grep -cE "function[[:space:]]+${fn}" "$ROOT/web/index.html" 2>/dev/null || echo 0)
if [ "$count" -gt 1 ]; then if [ "$count" -gt 1 ]; then
echo "FAIL: Funktion '$fn' ist $count mal deklariert (erwartet: 1)" echo "FAIL: Funktion '$fn' ist $count mal deklariert (erwartet: 1)"
FAIL=1 FAIL=1
fi fi
done done
# Platzhalter-Leichen in geaenderten HTML-Attributen # Platzhalter-Leichen in geaenderten HTML-Attributen
if grep -qE 'an[[:space:]][[:space:]]+und[[:space:]][[:space:]]+angehaengt' web/index.html; then if grep -qE 'an[[:space:]][[:space:]]+und[[:space:]][[:space:]]+angehaengt' "$ROOT/web/index.html"; then
echo "FAIL: Platzhalter-Leichen in der Datei (Variablen nicht interpoliert)" echo "FAIL: Platzhalter-Leichen in der Datei (Variablen nicht interpoliert)"
FAIL=1 FAIL=1
fi fi
fi fi
# --- 4. Git-Hygiene: Commit-Message ohne Platzhalter-Leichen ---------- # --- 4. Git-Hygiene: Commit-Message ohne Platzhalter-Leichen ----------
if ! git rev-parse HEAD >/dev/null 2>&1; then
echo "WARN: Kein Commit im Repo"
fi
last_msg=$(git log -1 --pretty=%B 2>/dev/null || true) last_msg=$(git log -1 --pretty=%B 2>/dev/null || true)
if echo "$last_msg" | grep -qE '\b(an|in|mit)[[:space:]][[:space:]]+und'; then if printf '%s\n' "$last_msg" | grep -qE '\b(an|in|mit)[[:space:]][[:space:]]+und'; then
echo "FAIL: letzter Commit hat doppelte Leerzeichen (Platzhalter nicht ersetzt):" echo "FAIL: letzter Commit hat doppelte Leerzeichen (Platzhalter nicht ersetzt):"
echo "$last_msg" | head -3 printf '%s\n' "$last_msg" | head -3
FAIL=1 FAIL=1
fi fi

View file

@ -1,10 +1,26 @@
#!/bin/bash #!/usr/bin/env bash
# Beendet laufende Instanzen von web/serve.py, um Port-Konflikte auf 8081 zu lösen.
set -euo pipefail set -euo pipefail
if pgrep -f "python3 .*web/serve.py" >/dev/null; then PORT="${1:-8081}"
pkill -f "python3 .*web/serve.py"
echo "Alte serve.py-Prozesse beendet." # Finde Prozess auf Port
PID=$(lsof -ti ":$PORT" 2>/dev/null || true)
if [ -n "$PID" ]; then
# Graceful shutdown attempt
kill -TERM "$PID" 2>/dev/null || true
# Warte bis zu 5 Sekunden
for i in $(seq 1 10); do
if ! kill -0 "$PID" 2>/dev/null; then
break
fi
sleep 0.5
done
# Force kill if still running
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID" 2>/dev/null || true
fi
echo "Prozess $PID auf Port $PORT beendet."
else else
echo "Keine laufende serve.py-Instanz gefunden." echo "Keine laufende Instanz auf Port $PORT gefunden."
fi fi

View file

@ -7,12 +7,15 @@
# ./scripts/smoke_test.sh 9000 # anderer Port # ./scripts/smoke_test.sh 9000 # anderer Port
# Exit-Code: 0 = alle Endpunkte 200, sonst 1. # Exit-Code: 0 = alle Endpunkte 200, sonst 1.
set -u set -euo pipefail
PORT="${1:-8082}" PORT="${1:-8082}"
[[ "$PORT" =~ ^[0-9]+$ ]] || { echo "Usage: $0 [port]" >&2; exit 1; }
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LOG="/tmp/smoke.$PORT.log" LOG="/tmp/smoke.$PORT.log"
EXIT_CODE=1 EXIT_CODE=1
MARKER="SMOKE_TEST_SERVER_$PORT" MARKER="SMOKE_TEST_SERVER_$PORT"
TIMEOUT=30
START_TIME=$(date +%s)
# Vorherige Instanzen dieses Smoke-Tests beenden # Vorherige Instanzen dieses Smoke-Tests beenden
pkill -f "$MARKER" 2>/dev/null || true pkill -f "$MARKER" 2>/dev/null || true
@ -55,7 +58,10 @@ trap cleanup EXIT INT TERM
# 127.0.0.1 statt localhost umgeht den Resolver — sonst kann unter # 127.0.0.1 statt localhost umgeht den Resolver — sonst kann unter
# WSL2/IPv6 ein TCP-SYN auf einen unbenutzten Port minutenlang ohne # WSL2/IPv6 ein TCP-SYN auf einen unbenutzten Port minutenlang ohne
# RST hängen. --connect-timeout 1 ist Defense-in-Depth. # RST hängen. --connect-timeout 1 ist Defense-in-Depth.
for i in $(seq 1 20); do for i in $(seq 1 50); do
if (( $(date +%s) - START_TIME >= TIMEOUT )); then
break
fi
if curl -sf --connect-timeout 1 -o /dev/null "http://127.0.0.1:$PORT/"; then break; fi if curl -sf --connect-timeout 1 -o /dev/null "http://127.0.0.1:$PORT/"; then break; fi
sleep 0.1 sleep 0.1
done done
@ -78,6 +84,7 @@ for p in "/" "/index.html" "/templates.json" \
"/templates/user/email_draft.md" \ "/templates/user/email_draft.md" \
"/templates/user/brainstorming.md"; do "/templates/user/brainstorming.md"; do
code=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT$p") code=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT$p")
code="${code:-000}"
status="ok" status="ok"
[ "$code" = "200" ] || { status="FAIL"; FAIL=1; } [ "$code" = "200" ] || { status="FAIL"; FAIL=1; }
printf '%-50s %s %s\n' "$p" "$code" "$status" printf '%-50s %s %s\n' "$p" "$code" "$status"

View file

@ -29,7 +29,10 @@ JSON_SCHEMA = {
r"^.+$": { r"^.+$": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": {"type": "string", "enum": ["string", "number", "enum", "boolean"]}, "type": {
"type": "string",
"enum": ["string", "number", "enum", "boolean"],
},
"required": {"type": "boolean"}, "required": {"type": "boolean"},
"default": {}, "default": {},
"description": {"type": "string"}, "description": {"type": "string"},
@ -46,7 +49,6 @@ JSON_SCHEMA = {
# Muster für Markdown-Templates # Muster für Markdown-Templates
MD_REQUIRED_SECTIONS = ["Template", "Variablen"] MD_REQUIRED_SECTIONS = ["Template", "Variablen"]
MD_VARIABLE_PATTERN = re.compile(r"\|\s*(\w+)\s*\|\s*(\w+)\s*\|")
def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
@ -54,7 +56,7 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
errors = [] errors = []
try: try:
with open(filepath, 'r', encoding='utf-8') as f: with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return False, [f"❌ JSON Syntax Error: {e}"] return False, [f"❌ JSON Syntax Error: {e}"]
@ -84,7 +86,9 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
# Pattern validieren # Pattern validieren
if "pattern" in schema and isinstance(data[field], str): if "pattern" in schema and isinstance(data[field], str):
if not re.match(schema["pattern"], data[field]): if not re.match(schema["pattern"], data[field]):
errors.append(f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}") errors.append(
f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}"
)
# Template prüfen # Template prüfen
if "template" in data: if "template" in data:
@ -98,7 +102,9 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
defined_vars = set(data["variables"].keys()) defined_vars = set(data["variables"].keys())
undefined_vars = template_vars - defined_vars undefined_vars = template_vars - defined_vars
if undefined_vars: if undefined_vars:
errors.append(f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}") errors.append(
f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}"
)
# Variablen validieren # Variablen validieren
if "variables" in data: if "variables" in data:
@ -113,7 +119,9 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]:
if "type" not in var_schema: if "type" not in var_schema:
errors.append(f"❌ Variable '{var_name}' benötigt ein 'type' Feld") errors.append(f"❌ Variable '{var_name}' benötigt ein 'type' Feld")
if var_schema.get("type") == "enum" and "values" not in var_schema: if var_schema.get("type") == "enum" and "values" not in var_schema:
errors.append(f"❌ Enum Variable '{var_name}' benötigt 'values' Array") errors.append(
f"❌ Enum Variable '{var_name}' benötigt 'values' Array"
)
return len(errors) == 0, errors return len(errors) == 0, errors
@ -123,14 +131,14 @@ def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]:
errors = [] errors = []
try: try:
with open(filepath, 'r', encoding='utf-8') as f: with open(filepath, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
except Exception as e: except Exception as e:
return False, [f"❌ Datei kann nicht gelesen werden: {e}"] return False, [f"❌ Datei kann nicht gelesen werden: {e}"]
# Mindestlänge # Mindestlänge
if len(content.strip()) < 50: if len(content.strip()) < 50:
errors.append(" Template zu kurz (mind. 50 Zeichen)") errors.append("⚠️ Warnung: Template zu kurz (mind. 50 Zeichen)")
# Titel prüfen # Titel prüfen
if not content.startswith("# "): if not content.startswith("# "):
@ -148,22 +156,31 @@ def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]:
var_section_start = content.find("# Variablen") var_section_start = content.find("# Variablen")
if var_section_start != -1: if var_section_start != -1:
var_section = content[var_section_start:var_section_start + 500] var_section = content[var_section_start : var_section_start + 500]
if "| Variable |" not in var_section: if "| Variable |" not in var_section:
errors.append("❌ Variablen-Tabelle nicht im korrekten Format") errors.append("❌ Variablen-Tabelle nicht im korrekten Format")
# Template-Block prüfen # Template-Block prüfen
if "Template" in content: if "Template" in content:
template_start = content.find("```") template_block_start = content.find("```")
if template_start == -1: 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") errors.append("❌ Kein Code-Block für Template gefunden")
else: else:
# Prüfe ob Variablen im Template sind # Prüfe ob Variablen im Template sind
template_content = content[template_start:] template_content = content[template_block_start:]
if "{" not in template_content or "}" not in template_content: if "{" not in template_content or "}" not in template_content:
errors.append("⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden") errors.append(
"⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden"
)
return len(errors) == 0, errors return len([e for e in errors if e.startswith("")]) == 0, errors
def validate_template(filepath: Path) -> Tuple[bool, List[str]]: def validate_template(filepath: Path) -> Tuple[bool, List[str]]:
@ -176,10 +193,17 @@ def validate_template(filepath: Path) -> Tuple[bool, List[str]]:
return False, [f"❌ Unsupported file type: {filepath.suffix}"] return False, [f"❌ Unsupported file type: {filepath.suffix}"]
ALLOWED_DIRS = {"templates/system", "templates/user", "templates/custom", "categories"}
def find_templates(directory: Path) -> List[Path]: def find_templates(directory: Path) -> List[Path]:
"""Findet alle Template-Dateien in einem Verzeichnis Baum.""" """Findet alle Template-Dateien in einem Verzeichnis Baum."""
templates = [] templates = []
for root, _, files in os.walk(directory): 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: for file in files:
if file.endswith((".json", ".md")): if file.endswith((".json", ".md")):
templates.append(Path(root) / file) templates.append(Path(root) / file)
@ -195,12 +219,17 @@ Beispiele:
python validate.py templates/system/code_reviewer.json python validate.py templates/system/code_reviewer.json
python validate.py --all python validate.py --all
python validate.py --json templates/ python validate.py --json templates/
""" """,
) )
parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis") parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis")
parser.add_argument("--all", action="store_true", help="Alle Templates validieren") parser.add_argument("--all", action="store_true", help="Alle Templates validieren")
parser.add_argument("--json", action="store_true", help="Nur JSON-Templates validieren") type_group = parser.add_mutually_exclusive_group()
parser.add_argument("--md", action="store_true", help="Nur Markdown-Templates validieren") 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() args = parser.parse_args()
@ -209,21 +238,19 @@ Beispiele:
if args.all: if args.all:
# Alle Templates finden # Alle Templates finden
templates = find_templates(base_dir / "templates") templates = find_templates(base_dir / "templates")
if not args.json:
templates += find_templates(base_dir / "categories")
if args.json: if args.type_json:
templates = [t for t in templates if t.suffix == ".json"] templates = [t for t in templates if t.suffix == ".json"]
if args.md: if args.type_md:
templates = [t for t in templates if t.suffix == ".md"] templates = [t for t in templates if t.suffix == ".md"]
elif args.path: elif args.path:
path = Path(args.path) path = Path(args.path)
if path.is_dir(): if path.is_dir():
templates = find_templates(path) templates = find_templates(path)
if args.json: if args.type_json:
templates = [t for t in templates if t.suffix == ".json"] templates = [t for t in templates if t.suffix == ".json"]
if args.md: if args.type_md:
templates = [t for t in templates if t.suffix == ".md"] templates = [t for t in templates if t.suffix == ".md"]
else: else:
templates = [path] templates = [path]
@ -241,7 +268,7 @@ Beispiele:
valid = 0 valid = 0
invalid = 0 invalid = 0
print(f"\n{'='*60}") print(f"\n{'=' * 60}")
print(f"Validiere {total} Template(s)...\n") print(f"Validiere {total} Template(s)...\n")
for template_path in sorted(templates): for template_path in sorted(templates):
@ -262,7 +289,7 @@ Beispiele:
print(f" {error}") print(f" {error}")
invalid += 1 invalid += 1
print(f"\n{'='*60}") print(f"\n{'=' * 60}")
print(f"Ergebnis: {valid} ✅ | {invalid} ❌ | {total} Total") print(f"Ergebnis: {valid} ✅ | {invalid} ❌ | {total} Total")
sys.exit(0 if invalid == 0 else 1) sys.exit(0 if invalid == 0 else 1)

View file

@ -171,7 +171,7 @@
#edit-content-content input[type="checkbox"] + label, #edit-content-content input[type="checkbox"] + label,
#edit-modal input[type="checkbox"] + label { #edit-modal input[type="checkbox"] + label {
color: #222222; color: #e0e0e0;
font-weight: normal; font-weight: normal;
} }
@ -331,7 +331,7 @@
.template-item .tag { .template-item .tag {
background: var(--bg-input); background: var(--bg-input);
color: var(--text-muted); color: #b0b0b0;
padding: 3px 8px; padding: 3px 8px;
border-radius: 4px; border-radius: 4px;
font-size: 11px; font-size: 11px;
@ -451,7 +451,7 @@
</head> </head>
<body> <body>
<!-- Modal --> <!-- Modal -->
<div class="modal-overlay" id="modal"> <div class="modal-overlay" id="modal" role="dialog" aria-label="Template anzeigen">
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<h2 id="modal-title">Template</h2> <h2 id="modal-title">Template</h2>
@ -474,7 +474,7 @@
</div> </div>
<!-- Edit Modal --> <!-- Edit Modal -->
<div class="modal-overlay" id="edit-modal"> <div class="modal-overlay" id="edit-modal" role="dialog" aria-label="Template bearbeiten">
<div class="modal" style="max-width: 800px;"> <div class="modal" style="max-width: 800px;">
<div class="modal-header"> <div class="modal-header">
<h2 id="edit-title">Template bearbeiten</h2> <h2 id="edit-title">Template bearbeiten</h2>
@ -494,14 +494,7 @@
<div class="header"> <div class="header">
<h1>Prompt Templates</h1> <h1>Prompt Templates</h1>
<span class="badge">Git Managed</span> <span class="badge">Git Managed</span>
<div class="actions"> <div class="actions"></div>
<button class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
Neues Template
</button>
</div>
</div> </div>
<div class="container"> <div class="container">
@ -546,6 +539,14 @@ $ python web/serve.py</div>
<script> <script>
let allTemplates = []; let allTemplates = [];
let currentEditTemplate = null; let currentEditTemplate = null;
let editContainerRef = null;
// XSS-schutz:_esc-Helper
function esc(s) {
const d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
// Edit Modal Funktionen // Edit Modal Funktionen
function closeEditModal() { function closeEditModal() {
@ -592,6 +593,7 @@ $ python web/serve.py</div>
} }
function createJsonEditUI(container, jsonData, editPathHint = '') { function createJsonEditUI(container, jsonData, editPathHint = '') {
editContainerRef = container;
container.innerHTML = ''; container.innerHTML = '';
const formDiv = document.createElement('div'); const formDiv = document.createElement('div');
@ -793,7 +795,9 @@ $ python web/serve.py</div>
try { try {
if (currentEditTemplate.endsWith('.json')) { if (currentEditTemplate.endsWith('.json')) {
const formDiv = document.getElementById('edit-content-content').querySelector('div'); const editContainer = document.getElementById('edit-content-content');
const firstChild = editContainer.children[0];
const formDiv = (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.tagName === 'DIV') ? firstChild : null;
if (!formDiv) { if (!formDiv) {
showToast('✗ Keine Eingabefelder gefunden'); showToast('✗ Keine Eingabefelder gefunden');
return; return;
@ -998,20 +1002,20 @@ $ python web/serve.py</div>
container.innerHTML = templates.map(t => ` container.innerHTML = templates.map(t => `
<div class="template-item"> <div class="template-item">
<h3>${t.name}</h3> <h3>${esc(t.name)}</h3>
<div class="meta"> <div class="meta">
<span>🏷️ ${t.type}</span> <span>🏷️ ${esc(t.type)}</span>
<span>📄 ${t.format}</span> <span>📄 ${esc(t.format)}</span>
<span>📌 v${t.version}</span> <span>📌 v${esc(t.version)}</span>
</div> </div>
<p>${t.description || 'Keine Beschreibung'}</p> <p>${esc(t.description) || 'Keine Beschreibung'}</p>
<div class="tags"> <div class="tags">
${t.tags.map(tag => `<span class="tag">${tag.startsWith('#') ? tag : '#' + tag}</span>`).join('')} ${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-icon" onclick="viewTemplate('${t.path}')">Anzeigen</button> <button class="btn btn-icon" onclick="viewTemplate('${esc(t.path)}')">Anzeigen</button>
<button class="btn btn-icon" onclick="editModalContent('${t.path}')">📝 Bearbeiten</button> <button class="btn btn-icon" onclick="editModalContent('${esc(t.path)}')">📝 Bearbeiten</button>
<button class="btn btn-icon" onclick="copyContent('${t.path}')">Inhalt kopieren</button> <button class="btn btn-icon" onclick="copyContent('${esc(t.path)}')">Inhalt kopieren</button>
</div> </div>
</div> </div>
`).join(''); `).join('');
@ -1064,7 +1068,7 @@ $ python web/serve.py</div>
// Escape key closes modal // Escape key closes modal
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal(); if (e.key === 'Escape') { closeEditModal(); closeModal(); }
}); });
// Hash -> type filter // Hash -> type filter
@ -1090,6 +1094,10 @@ $ python web/serve.py</div>
allTemplates = t; allTemplates = t;
currentType = parseTypeFromHash(); currentType = parseTypeFromHash();
applyFilters(); applyFilters();
}).catch(e => {
console.error('Failed to load templates:', e);
allTemplates = [];
applyFilters();
}); });
</script> </script>
</body> </body>

View file

@ -1,77 +1,93 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Minimaler Entwicklungs-Server für die Prompt Templates Webansicht. Minimaler Entwicklungs-Server für die Prompt Templates Webansicht.
Startet auf Port 8080 und dient die statischen Dateien aus. Startet auf Port 8081 und dient die statischen Dateien aus.
""" """
import http.server import http.server
import socketserver import socketserver
import os
import socket import socket
import os
import json import json
import logging
from urllib.parse import urlparse, unquote
logging.basicConfig(level=logging.INFO)
DIRECTORY = os.path.dirname(os.path.abspath(__file__)) DIRECTORY = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.abspath(os.path.join(DIRECTORY, '..')) ROOT_DIR = os.path.abspath(os.path.join(DIRECTORY, ".."))
def find_free_port(start_port=9000): def find_free_port(start_port=9000):
"""Finde einen freien Port ab start_port""" """Finde einen freien Port ab start_port
Für zukünftige Smoke-Test-Integration.
"""
port = start_port port = start_port
while port < 10000: while port < 10000:
try: try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', port)) s.bind(("", port))
return port return port
except OSError: except OSError:
port += 1 port += 1
return None return None
class Handler(http.server.SimpleHTTPRequestHandler): class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs) super().__init__(*args, directory=DIRECTORY, **kwargs)
def _validate_template_path(self, path): def _validate_template_path(self, path):
"""Validiert, dass der Pfad auf Schritte Beschränkt ist und innerhalb von ROOT_DIR/templates/ oder DIRECTORY liegt.""" """Validiert, dass der Pfad auf Schritte Beschränkt ist und innerhalb von ROOT_DIR/templates/ oder DIRECTORY liegt."""
parsed = urlparse(path).path
path = parsed
# /templates.json ist ein Sonderfall - exakter Pfad im web/-Verzeichnis # /templates.json ist ein Sonderfall - exakter Pfad im web/-Verzeichnis
if path == '/templates.json': if path == "/templates.json":
return os.path.join(DIRECTORY, 'templates.json') return os.path.join(DIRECTORY, "templates.json")
# Nur /templates/* Pfade erlauben # Nur /templates/* Pfade erlauben
if not path.startswith('/templates/'): if not path.startswith("/templates/"):
return None return None
# Extrahiere den relativen Pfad nach /templates/ # Extrahiere den relativen Pfad nach /templates/
rel_path = path[len('/templates/'):] # 'system/test.json' oder '../etc/passwd' rel_path = path[len("/templates/") :] # 'system/test.json' oder '../etc/passwd'
rel_path = unquote(rel_path)
# Ablehnen bei leeren Pfaden oder absoluten Pfaden # Ablehnen bei leeren Pfaden oder absoluten Pfaden
if not rel_path or os.path.isabs(rel_path): if not rel_path or os.path.isabs(rel_path):
return None return None
# Ablehnen bei '..' Sequenzen (vor der Normalisierung!) # Ablehnen bei '..' Sequenzen (vor der Normalisierung!)
if '..' in rel_path.split(os.sep): if ".." in rel_path.split(os.sep):
return None return None
# Pfad normalisieren # Pfad normalisieren
normalized_rel = os.path.normpath(rel_path) normalized_rel = os.path.normpath(rel_path)
# Nach Normalisierung nochmal prüfen # Nach Normalisierung nochmal prüfen
if normalized_rel.startswith('..') or os.path.isabs(normalized_rel): if normalized_rel.startswith("..") or os.path.isabs(normalized_rel):
return None return None
# Vollständigen Pfad konstruieren # Vollständigen Pfad konstruieren
full_path = os.path.join(ROOT_DIR, 'templates', normalized_rel) full_path = os.path.join(ROOT_DIR, "templates", normalized_rel)
# Explizite Prüfung, dass der Pfad innerhalb von ROOT_DIR/templates/ liegt # Explizite Prüfung, dass der Pfad innerhalb von ROOT_DIR/templates/ liegt
templates_base = os.path.abspath(os.path.join(ROOT_DIR, 'templates')) templates_base = os.path.abspath(os.path.join(ROOT_DIR, "templates"))
full_path_abs = os.path.abspath(full_path) full_path_abs = os.path.abspath(full_path)
if not full_path_abs.startswith(templates_base + os.sep) and full_path_abs != templates_base: if (
not full_path_abs.startswith(templates_base + os.sep)
and full_path_abs != templates_base
):
return None return None
return full_path_abs return full_path_abs
def do_PUT(self): def do_PUT(self):
# Nur PUT auf /templates/* Pfade erlauben # Nur PUT auf /templates/* Pfade erlauben
file_path = self._validate_template_path(self.path) file_path = self._validate_template_path(urlparse(self.path).path)
if file_path is None: if file_path is None:
self.send_error(403, "Forbidden: Invalid path") self.send_error(403, "Forbidden: Invalid path")
return return
@ -83,26 +99,30 @@ class Handler(http.server.SimpleHTTPRequestHandler):
return return
# Content-Type prüfen # Content-Type prüfen
content_type = self.headers.get('Content-Type', '') content_type = self.headers.get("Content-Type", "")
if 'text/plain' not in content_type and not content_type.startswith('text/'): if "text/plain" not in content_type and not content_type.startswith("text/"):
self.send_error(400, "Unsupported content type") self.send_error(400, "Unsupported content type")
return return
# Inhalt lesen und speichern # Inhalt lesen und speichern
content_length = int(self.headers.get('Content-Length', 0)) MAX_BODY = 10 * 1024 * 1024
content_length = int(self.headers.get("Content-Length", 0))
if content_length <= 0: if content_length <= 0:
self.send_error(400, "No content provided") self.send_error(400, "No content provided")
return return
if content_length > MAX_BODY:
self.send_error(413, "Request body too large")
return
try: try:
file_content = self.rfile.read(content_length) file_content = self.rfile.read(content_length)
# Datei schreiben # Datei schreiben
with open(file_path, 'wb') as f: with open(file_path, "wb") as f:
f.write(file_content) f.write(file_content)
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/plain') self.send_header("Content-type", "text/plain")
self.end_headers() self.end_headers()
self.wfile.write(b"File saved successfully") self.wfile.write(b"File saved successfully")
except Exception as e: except Exception as e:
@ -110,42 +130,50 @@ class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self): def do_GET(self):
# Für Root-Pfad: index.html servieren # Für Root-Pfad: index.html servieren
if self.path == '/' or self.path == '/index.html': parsed_path = urlparse(self.path).path
self.path = '/index.html' if parsed_path == "/" or parsed_path == "/index.html":
self.path = "/index.html"
return super().do_GET() return super().do_GET()
# Anfragen für /templates.json oder /templates/* umleiten # Anfragen für /templates.json oder /templates/* umleiten
file_path = self._validate_template_path(self.path) file_path = self._validate_template_path(self.path)
if file_path is not None: if file_path is not None:
if os.path.exists(file_path) and not os.path.isdir(file_path):
try: try:
with open(file_path, 'rb') as f: with open(file_path, "rb") as f:
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/plain' if file_path.endswith('.md') else 'application/json') self.send_header(
"Content-type",
"text/plain"
if file_path.endswith(".md")
else "application/json",
)
self.end_headers() self.end_headers()
self.wfile.write(f.read()) self.wfile.write(f.read())
return return
except FileNotFoundError:
self.send_error(404, "File not found")
return
except Exception as e: except Exception as e:
self.send_error(500, f"Error serving file: {e}") self.send_error(500, f"Error serving file: {e}")
return return
else:
self.send_error(404, "File not found")
return
return super().do_GET() return super().do_GET()
def main(): def main():
PORT = 8081 PORT = 8081
print(f"Serving on http://localhost:{PORT}") logging.info("Serving on http://localhost:%s", PORT)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("", PORT), Handler) as httpd: with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"Serving Prompt Templates on http://localhost:{PORT}") logging.info("Serving Prompt Templates on http://localhost:%s", PORT)
print(f"Press Ctrl+C to stop") logging.info("Press Ctrl+C to stop")
print(f"Directory: {DIRECTORY}") logging.info("Directory: %s", DIRECTORY)
try: try:
httpd.serve_forever() httpd.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nServer stopped") logging.info("\nServer stopped")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -1,24 +1,24 @@
[ [
{ {
"path": "../templates/custom/brainstorming.md", "path": "templates/custom/brainstorming.md",
"type": "custom", "type": "custom",
"name": "Brainstorming Assistent", "name": "Brainstorming Assistent",
"description": "", "description": "Dient zur Ideengenerierung und Kreativitätsförderung",
"version": "1.0", "version": "1.0",
"tags": [], "tags": [],
"format": "md" "format": "md"
}, },
{ {
"path": "../templates/user/email_draft.md", "path": "templates/user/email_draft.md",
"type": "user", "type": "user",
"name": "Email Entwurf Assistent", "name": "Email Entwurf Assistent",
"description": "", "description": "Entwirft professionelle E-Mail-Entwürfe mit konfigurierbarem Tonfall",
"version": "1.0", "version": "1.0",
"tags": [], "tags": [],
"format": "md" "format": "md"
}, },
{ {
"path": "../templates/system/code_reviewer.json", "path": "templates/system/code_reviewer.json",
"type": "system", "type": "system",
"name": "Code Reviewer", "name": "Code Reviewer",
"description": "Analysiert Code auf Qualität, Best Practices und potenzielle Bugs", "description": "Analysiert Code auf Qualität, Best Practices und potenzielle Bugs",
@ -33,7 +33,7 @@
"format": "json" "format": "json"
}, },
{ {
"path": "../templates/system/commit_analysis.json", "path": "templates/system/commit_analysis.json",
"type": "system", "type": "system",
"name": "Git Commit Deep Analysis", "name": "Git Commit Deep Analysis",
"description": "Erstellt eine tiefe Analyse der letzten Git-Commits mit technischer und fachlicher Bewertung", "description": "Erstellt eine tiefe Analyse der letzten Git-Commits mit technischer und fachlicher Bewertung",
@ -49,7 +49,7 @@
"format": "json" "format": "json"
}, },
{ {
"path": "../templates/system/summarizer.json", "path": "templates/system/summarizer.json",
"type": "system", "type": "system",
"name": "Text Summarizer", "name": "Text Summarizer",
"description": "Erstellt präzise Zusammenfassungen von Texten mit konfigurierbarer Länge", "description": "Erstellt präzise Zusammenfassungen von Texten mit konfigurierbarer Länge",