diff --git a/build_archive.py b/build_archive.py index ff5136b..c733e07 100644 --- a/build_archive.py +++ b/build_archive.py @@ -69,12 +69,15 @@ REQUIRED_FILES = [ "tests/test_docker_ops.py", "tests/test_http_ops.py", "tests/test_lock_ops.py", + "tests/test_actions.py", + "docs/How_to_use.md", + "scripts/smoke.sh", ] # Top-level directories to include wholesale (in addition to REQUIRED_FILES), # so that any file added later without updating REQUIRED_FILES is still # packaged. REQUIRED_FILES remains the source of truth for *verification*. -INCLUDE_DIRS = ["src", "tests", "docs", "man"] +INCLUDE_DIRS = ["src", "tests", "docs", "man", "scripts"] INCLUDE_FILES = [ "pyproject.toml", "README.md", diff --git a/docs/Archiv_fertig_-_Check_und_Installation.md b/docs/Archiv_fertig_-_Check_und_Installation.md index 8275a25..089564e 100644 --- a/docs/Archiv_fertig_-_Check_und_Installation.md +++ b/docs/Archiv_fertig_-_Check_und_Installation.md @@ -4,13 +4,13 @@ Was gebaut wurde Statt manuell zu tarballen, gibt es jetzt ein eigenständiges Python-Skript build_archive.py, das: -Prüft, ob alle 27 erforderlichen Projektdateien vorhanden sind (Quellcode, Tests, Doku, Manpage, Config-Beispiel) +Prüft, ob alle erforderlichen Projektdateien vorhanden sind (Quellcode, Tests, Doku, Manpage, Config-Beispiel, scripts/smoke.sh) pyproject.toml-Abhängigkeiten gegen requirements.txt abgleicht Die deklarierte Abhängigkeit (requests>=2.31,<3) in einer frischen, isolierten virtuellen Umgebung installiert, um echte Installierbarkeit zu bestätigen (nicht nur einen Trockenlauf) -Die vollständige Test-Suite (93 Tests, alle grün) laufen lässt +Die vollständige Test-Suite (aktuell 130 Tests, alle grün) laufen lässt Das .tar.gz über Pythons tarfile-Modul baut (nicht rohes Shell-tar) @@ -24,7 +24,7 @@ Enthaltene Projektstruktur src/llamacppctl/ — die vollständige Implementierung: cli.py, config.py, prompt_io.py (SSRF-gehärtete Eingabeschicht), docker_ops.py, http_ops.py, lock_ops.py, actions.py, main.py -tests/ — 93 Tests (Prompt-Quellen, Datei-/URL-Sicherheit mit gemockter DNS-Auflösung, Konfigurationsauflösung, CLI-Validierung, Docker-/HTTP-Operationen mit gemocktem subprocess/requests, File-Locking) +tests/ — 130 Tests (Prompt-Quellen, Datei-/URL-Sicherheit mit gemockter DNS-Auflösung, Konfigurationsauflösung, CLI-Validierung, Docker-/HTTP-Operationen mit gemocktem subprocess/requests, File-Locking) docs/SECURITY_AND_OPERATIONS.md — vollständige Architektur-, Konfigurations- und Sicherheitsdokumentation diff --git a/docs/How_to_use.md b/docs/How_to_use.md index 645db2e..53ab829 100644 --- a/docs/How_to_use.md +++ b/docs/How_to_use.md @@ -18,7 +18,16 @@ Besonderheiten gegenüber den ursprünglichen Shell-Skripten: - `--change` nutzt eine exklusive Dateisperre (`/tmp/llamacppctl..lock`), damit zwei gleichzeitige Modellwechsel für denselben Container sich nicht in die Quere kommen. - Die Start-Bereitschaftsprüfung (`--start`/`--change`) wartet auf eine echte Chat-Completion-Antwort, nicht nur auf einen offenen Port — das ist zuverlässiger als ein reiner Port-Check. -- `--dry-run` zeigt dir den vollständigen `docker run`-Befehl an, ohne ihn auszuführen — praktisch, um vor einer echten Änderung zu prüfen, was passieren würde. +- `--dry-run` zeigt dir den vollständigen `docker run`-Befehl an, ohne ihn auszuführen (reine Vorschau: kein Lock, kein Container-Abriss). +- `--change` validiert den Modellpfad **vor** dem Entfernen des laufenden Containers und läuft — wie `--start` — unter Lock; `--force` umgeht einen hängenden Lock. -Die vollständige Referenz zu allen Optionen steht in der Manpage (`man/llamacppctl.1`) und in `README.md`/`docs/SECURITY_AND_OPERATIONS.md` im Archiv. +Neuere Optionen (Details in Manpage/README): + +- **Antwortsteuerung:** `--max-tokens` (Reasoning-Modelle brauchen viel Budget, sonst leere Antwort), `--chat-temp`, und `--stream` für token-weise Live-Ausgabe bei `--chat`. +- **Netzwerk/Auth:** Port wird per Default nur auf `127.0.0.1` veröffentlicht; `--expose` bindet auf alle Interfaces, `--api-key` schützt die Chat-API (Bearer-Token). +- **Scriptbar:** `--check` liefert Exit-Code 0 (läuft/erreichbar) bzw. 5 — geeignet für Monitoring/Cron. +- **`hf_home`** darf Env-Variablen enthalten, z. B. `hf_home = ${HF_HOME}`. +- **End-to-End-Test:** `scripts/smoke.sh` (opt-in) gegen einen echten Server. + +Die vollständige Referenz zu allen Optionen steht in der Manpage (`man/llamacppctl.1`) und in `README.md`/`docs/SECURITY_AND_OPERATIONS.md`. diff --git a/docs/SECURITY_AND_OPERATIONS.md b/docs/SECURITY_AND_OPERATIONS.md index c9f642e..e4ee455 100644 --- a/docs/SECURITY_AND_OPERATIONS.md +++ b/docs/SECURITY_AND_OPERATIONS.md @@ -9,13 +9,14 @@ Betriebsmodus auf einem dedizierten GPU-Host. ``` src/llamacppctl/ cli.py Argumentparsing (argparse) + semantische Cross-Field-Validierung - schema.py Dataclasses: ServerConfig, PromptConfig, CheckResult + schema.py Dataclasses: ServerConfig, PromptConfig, ChatReply, CheckResult config.py INI-Konfiguration laden/mergen -> ServerConfig/PromptConfig + (inkl. ${ENV}- und ~-Expansion in hf_home) prompt_io.py Sicherheitsgehärtete Eingabeschicht für System-/User-Prompts docker_ops.py Alle Docker-Interaktionen (subprocess mit Argumentlisten) - http_ops.py HTTP-Health-Checks und Chat-Completions gegen die - OpenAI-kompatible llama.cpp-API - lock_ops.py Exklusives, nicht-blockierendes File-Locking für --change + http_ops.py HTTP-Health-Checks, Chat-Completions und SSE-Streaming gegen + die OpenAI-kompatible llama.cpp-API + lock_ops.py Exklusives, nicht-blockierendes File-Locking für --start/--change actions.py Orchestrierung: do_start, do_check, do_stop, do_change, do_chat main.py Dünner Einstiegspunkt: parse -> validate -> resolve -> dispatch ``` @@ -37,13 +38,26 @@ testbar von der Orchestrierung. `container_name=va_llm`, `gpu_device=1`, `jinja/fa/kv_unified/ cont_batching/no_context_shift=true`, `reasoning=on`, `cache_type_k/v=q4_0`, `batch_size=1024`, `ubatch_size=512`, - `timeout=300`, `poll_interval=2`, …). + `timeout=300`, `poll_interval=2`, `expose=false`, `api_key=`, + `max_tokens=2048`, `chat_temperature=`, …). 2. **`[default]`**-Sektion — überschreibt die eingebauten Defaults global. 3. **`[model.]`**-Sektion — ausgewählt über `--profile `, überschreibt `[default]` selektiv (nur gesetzte Schlüssel). 4. **CLI-Overrides** — z. B. `--image`, `--host-port`, `--container-name`, `--ctx-size`, … überschreiben alles Vorherige. +`hf_home` unterstützt Environment-Variablen und `~` (z. B. `hf_home = ${HF_HOME}`); +die Expansion erfolgt in `build_server_config()` über `os.path.expandvars` + +`expanduser`. + +Neben den Container-/Server-Parametern liefert die Auflösung auch die +**Chat-Request-Parameter** (`PromptConfig`, wirken nur auf `--chat` und die +Antwort nach `--start`, nicht auf den Container): `max_tokens` (Default 2048; +Reasoning-Modelle brauchen viel Budget, sonst bricht die Antwort mitten im +„Denken“ ab und `content` ist leer) und `chat_temperature` (leer = die +Server-`temp` gilt). CLI-Overrides: `--max-tokens`, `--chat-temp`. Die Ausgabe +kann mit `--stream` token-weise (SSE) erfolgen. + `[prompt.]`-Sektionen werden **separat** aufgelöst und liefern ausschließlich einen *Fallback*-Systemprompt (`system_prompt = ...`). Ein explizit übergebener `-s/--system`, `--system-file` oder `--system-url` @@ -147,6 +161,13 @@ Nach erfolgreicher Zielvalidierung (`load_text_url()`): einen falschen (zu niedrigen) `Content-Length`-Header sendet, keinen Speicher-Erschöpfungsangriff durchführen kann. - Antwort muss gültiges UTF-8 sein. +- **DNS-Pinning gegen Rebinding:** `validate_url_target()` löst den Hostnamen + auf und klassifiziert die IPs; der eigentliche `requests.get` würde jedoch + **erneut** auflösen (TOCTOU-Fenster). `_pin_dns()` beschränkt `socket.getaddrinfo` + für die Dauer des Requests auf genau die bereits validierten IPs — löst der + Name zwischenzeitlich auf eine andere (z. B. private) Adresse um, wird der + Connect mit `gaierror` abgebrochen. TLS/SNI und Zertifikatsprüfung bleiben + intakt, weil die URL weiterhin den Hostnamen trägt. ### 3.3 Warum das wichtig ist @@ -175,16 +196,42 @@ mehrbenutzerfähigen Wrapper um `llamacppctl`). Profile gezielt auf unterschiedliche GPUs (z. B. RTX 3090 #1/#2) gepinnt werden können. -## 5. Locking (`--change`) +### 4.1 Netzwerk-Exposition und API-Key + +Die llama.cpp-OpenAI-API ist standardmäßig **nicht** authentifiziert. Deshalb: + +- Der Port wird per Default nur auf **`127.0.0.1`** veröffentlicht + (`_port_publish()` -> `-p 127.0.0.1::`), d. h. der + Endpunkt ist **nicht** aus dem LAN erreichbar. `--expose`/`expose = true` + bindet auf alle Interfaces (`-p :`). +- Wird exponiert, sollte per `--api-key`/`api_key` ein Schlüssel gesetzt werden. + Er wird dem Server übergeben (`--api-key`) und vom Tool als + `Authorization: Bearer ` auf jeder Anfrage mitgesendet. +- **Wichtig:** llama.cpp schützt damit `/v1/chat/completions` (ohne Key -> 401), + lässt aber `/health` und `/v1/models` bewusst offen (für Health-Checks). Die + Reachability-Checks des Tools (`check_health`/`check_models`) funktionieren + daher auch ohne Key; nur die Chat-Endpunkte verlangen ihn. + +## 5. Locking (`--start` / `--change`) `lock_ops.FileLock` verwendet `fcntl.flock(LOCK_EX | LOCK_NB)` auf einer Lock-Datei unter `/tmp/llamacppctl..lock` (Default, ableitbar -über `--lock-file`). `--change` hält den Lock über die gesamte -Stop-Reconfigure-Restart-Sequenz. Ein zweiter, gleichzeitiger `--change`-Aufruf -für **denselben** `container_name` schlägt sofort mit `LockError` (Exit-Code -6) fehl, statt zu blockieren oder Race-Conditions am Container zu riskieren. -Da der Lock-Pfad an `container_name` gekoppelt ist, blockieren sich zwei -Profile mit unterschiedlichem `container_name` gegenseitig nicht. +über `--lock-file`). **Sowohl `--start` als auch `--change`** halten den Lock +über die gesamte (Stop-)Reconfigure-Restart-Sequenz. Ein zweiter, gleichzeitiger +Aufruf für **denselben** `container_name` schlägt sofort mit `LockError` +(Exit-Code 6) fehl, statt zu blockieren oder Race-Conditions am Container zu +riskieren. Da der Lock-Pfad an `container_name` gekoppelt ist, blockieren sich +zwei Profile mit unterschiedlichem `container_name` gegenseitig nicht. + +`--force` umgeht einen belegten Lock bewusst (mit Warnung auf stderr) — gedacht +für hängende/verwaiste Locks. Zusätzlich lässt `--force` `--stop` +Inkonsistenzen (z. B. halb erstellte Container) schlucken, statt mit +Docker-Fehler abzubrechen. + +`--dry-run` ist eine reine Vorschau und nimmt **weder** den Lock **noch** +entfernt es den Container; `--change` validiert außerdem den Modellpfad +**vor** dem Entfernen des laufenden Containers, sodass eine Fehlkonfiguration +keinen vermeidbaren Ausfall verursacht. ## 6. `--dry-run` und `--print-effective-config` als Sicherheitswerkzeuge @@ -235,7 +282,11 @@ ctx_size = 131072 dem, was tatsächlich in Docker läuft. - `--check --profile ` ist der empfohlene Weg für Monitoring/Cronjobs, um sowohl Container-Status als auch tatsächliche Inferenzfähigkeit - (`chat_ok`) zu prüfen. + (`chat_ok`) zu prüfen. `--check` ist **scriptbar**: Exit-Code 0, wenn der + Container läuft und HTTP erreichbar ist, sonst 5. +- Für einen End-to-End-Test gegen einen echten Docker + llama.cpp-Server + (`--api-key`-Round-Trip, Streaming, Exit-Codes) gibt es das opt-in Skript + `scripts/smoke.sh` (eigener Container/Port, Cleanup am Ende). ## 8. Installation, Nutzung, Fehlersuche @@ -253,3 +304,6 @@ Häufige Fehlerbilder: | `resolved to blocked IP ...` | URL löst auf eine private/loopback/reserved Adresse auf | Ziel korrigieren oder bewusst `--allow-private-url` setzen (nur für vertrauenswürdige interne Ziele) | | `lock busy: /tmp/llamacppctl..lock` | Ein anderer `--change`-Lauf für denselben `container_name` läuft bereits | Warten, bis der andere Lauf beendet ist, oder Ursache des hängenden Laufs prüfen | | `Server did not become ready within s` | Modell braucht länger zum Laden als `timeout`, oder Startfehler | `--timeout` erhöhen, `--logs` für Container-Log-Tail nutzen | +| `chat completion returned HTTP 401: Invalid API Key` | Server verlangt `api_key`, das Tool sendet keinen/falschen | `--api-key`/`api_key` passend setzen | +| Leere Antwort + `Hinweis: ... abgeschnitten (finish_reason=length)` | Reasoning-Modell hat das `max_tokens`-Budget im „Denken“ verbraucht | `--max-tokens`/`max_tokens` erhöhen | +| `--check` meldet `health=unhealthy`, obwohl der Server antwortet | bildeigener Docker-Healthcheck vs. eigene HTTP-Probe des Tools | unkritisch: `HTTP: OK` + Exit 0 zählt; Docker-Health ist separat |