# Deploymentcenter 2.2 – 2.4: Was noch zu tun ist Stand: 2026-08-13. Ergänzt [Deploymentcenter-Integration](Deploymentcenter-Integration.md) (dort steht der Stand nach 2.1) um die drei neuen Ausbaustufen. | Fassung | Was dazukam | Betrifft uns | |---|---|---| | **2.2** | Plattform-Dimension, signierte Releases, Anwenden mit Rollback, `preservePatterns` | Release-Strecke, Update-Anwendung | | **2.3** | Erstinstallation über `update-agent --action install`, `setup.json`, Installationskonto | Neu, siehe §4 | | **2.4** | Release-Ablage hinter HTTP-Basic-Auth, Zugang über den Lizenzschlüssel | **Erledigt**, siehe §1 | --- ## 1. Zugangsschutz (2.4) — erledigt `CheckForUpdateAsync` übergibt jetzt `ReleaseCredentials.FromLicenseKey(...)`, und `UpdateCheckResult.Unauthorized` wird getrennt von einem Netzfehler behandelt. **Warum das nicht warten konnte:** UPGRADE §16.1 empfiehlt „erst ausliefern, dann scharfschalten". Für ein Produkt, das noch nie veröffentlicht hat, geht diese Reihenfolge nicht auf. `ReleaseGuard::regenerateForProject` überspringt Verzeichnisse, die es nicht gibt — `/releases/clawddotnet/` liefert derzeit 404, es ist also nichts geschützt. Sobald wir das **erste** Release hochladen, entsteht das Verzeichnis, und der nächste `tick.php`-Lauf legt den Schutz an. Der erste ausgelieferte Build muss die Zugangsdaten also bereits mitbringen, sonst schließt sich die Tür hinter dem ersten Release. --- ## 2. Plattform (2.2) — Release-Strecke steht Erstes Paket veröffentlicht: **0.1.0, Kanal `dev`, Plattform `win-x64`**, 116 Dateien, 25 MB, Rückgabewert 0. Die Update-Prüfung antwortet korrekt (`0.0.9` → Update, `0.1.0` → keins). ```bash pack-and-deploy --config deploy/packager.config.json \ --project clawddotnet --version 0.1.0 \ --channel dev --platform win-x64 \ --publish-dir ``` - Zugangsdaten in `deploy/packager.config.json` (per `.gitignore` ausgeschlossen), Vorlage ohne Werte in [`packager.config.example.json`](../deploy/packager.config.example.json). - **`ftpRemoteBaseDir` ist `/releases`**, nicht `/public_html/releases` wie in der Packager-Vorlage: Auf diesem Server liegt die Release-Ablage auf der FTP-Wurzel. - Der Packager veröffentlicht mit einem **Sub-Token**, das nur `updateservice:publish` trägt — gezogen über `/api/tokens/v1/provision`. Das Master-Token gehört nicht in eine Konfigurationsdatei. - **`deploy.py` ist dafür das falsche Werkzeug.** Es spiegelt den Deploymentcenter-Projektbaum in die FTP-Wurzel und hat mit dem Veröffentlichen eines Anwendungspakets nichts zu tun. - Die Versionsgegenprobe des Packagers greift und passt: `` aus [Directory.Build.props](../Directory.Build.props) stimmt mit `clawddotnet.dll` überein. Offen: `linux-x64` (erst nach der Avalonia-Portierung) und `prod`. Clientseitig ist nichts zu tun: Das SDK schickt die Kennung des laufenden Systems von selbst. ### `preservePatterns` betrifft uns kaum Unsere Konfiguration liegt seit der Linux-Portierung in `AppPaths.ConfigDirectory` (`%APPDATA%` bzw. XDG), **nicht** neben der Programmdatei. Ein Update kann sie also gar nicht überschreiben. Zu prüfen bleibt nur, dass keine leeren Arbeitsordner ins Paket wandern — die `CreateWorkingDirectories`-Targets in [ClawdDotNet.csproj](../ClawdDotNet.csproj) legen `tools/`, `Logs/` und `Instances/` unter `OutputPath` an, und die sind mit AppPaths ohnehin überholt. --- ## 3. Update anwenden — erledigt Aus dem Hinweis ist eine Rückfrage geworden („Jetzt installieren" / „Später"), die den `update-agent` startet. Umgesetzt in [`DeploymentcenterService.StartUpdate`](../src/ClawdDotNet.App/Services/DeploymentcenterService.cs) und `App.StartUpdateAsync`. ### Der Agent wird mitgeliefert — er muss es Die Erstinstallation legt den Agenten **nicht** ins Zielverzeichnis: Sie läuft von dort, wo der Benutzer sie hingelegt hat. `ResolveAgentPath()` sucht ihn aber neben der Anwendung. Ohne Mitliefern fände die Anwendung nie einen Agenten und könnte sich nicht aktualisieren. [`deploy/publish.py`](../deploy/publish.py) holt das ausgelieferte Binary von `/installer/`, **prüft die SHA256 gegen `installer.json`** und legt es plattformrichtig ab (`update-agent.exe` bzw. `update-agent`). Bewusst das offizielle statt eines selbst gebauten: Es ist dasselbe, das die Erstinstallation verwendet, und wird zentral gepflegt. Ein ungeprüfter Download wäre ausgerechnet auf dem Pfad, der später fremden Code ausführt, die falsche Sparsamkeit. Kosten: rund 28 MB im gepackten Paket (25 → 53 MB). ### Die Reihenfolge ist der eigentliche Inhalt ``` 1. AnnounceUpdate(version) → Watchdog meldet beim Beenden "maintenance" 2. AppHost.DisposeAsync() → Datenbank, Scanner, Telegram, Abmeldung 3. StartUpdate(...) → Agent starten, exitCurrentApp: false 4. desktop.Shutdown() → wir beenden uns selbst ``` Die Verlockung wäre, `LaunchUpdateAgent` das Beenden zu überlassen. Das tut es aber über `Environment.Exit` und übergeht damit Schritt 2 vollständig: keine Abmeldung, keine geschlossene Instanzdatenbank. Deshalb `exitCurrentApp: false` und `waitForCurrentProcess: true` — der Agent bekommt unsere Prozesskennung und wartet, bis wir wirklich weg sind, statt über gesperrte Dateien zu kopieren. `maintenance` statt `stopped` ist kein Schönheitsfehler: `stopped` heißt „bewusst beendet" und lässt den Monitor liegen, bis jemand ihn anfasst. Beim Update kommt die Instanz aber wieder. **Doppeltes Aufräumen** war die Falle dabei: Nach Schritt 2 ruft `desktop.Shutdown()` die Behandlung, die erneut aufräumt — und dabei den gerade gesetzten Wartungszustand mit einer zweiten Abmeldung überschrieben hätte. `AppHost.DisposeAsync` sperrt sich jetzt selbst gegen den zweiten Durchlauf. --- ## 4. Erstinstallation (2.3) — `setup.json` steht Der Konfigurationsort war der Blocker: `setup.json`-Ziele waren „relativ zum Installationsverzeichnis", unsere Konfiguration liegt aber in `%APPDATA%` bzw. `$XDG_CONFIG_HOME` — weil `/opt/clawddotnet` unter Linux für den Dienstbenutzer nicht schreibbar ist ([Linux-Analyse](Linux-Portierung-Analyse.md)). Das Deploymentcenter hat daraufhin `location` am Ziel ergänzt (`install`, `config`, `data`, `home`) samt Variablenersetzung in `file`. Damit ist der Weg frei; [`setup.json`](../src/ClawdDotNet.Desktop/setup.json) liegt im Projekt und wird ins Ausgabeverzeichnis kopiert, landet also im Paket neben der `manifest.json`. ### Der Ordnername ist bewusst kleingeschrieben `AppPaths` legt das Verzeichnis plattformabhängig unterschiedlich an: | Plattform | Pfad | |---|---| | Windows | `%APPDATA%\ClawdDotNet` | | Linux | `$XDG_CONFIG_HOME/clawddotnet` (klein, Konvention) | Eine `setup.json` kennt nur **eine** Schreibweise. `clawddotnet/Settings.json` trifft unter Linux exakt und unter Windows ebenfalls, weil NTFS Groß- und Kleinschreibung nicht unterscheidet. Andersherum ginge es nicht: `ClawdDotNet` wäre unter Linux ein zweites, leeres Verzeichnis neben dem, aus dem die Anwendung liest. ### Was dabei abfällt Das Token stellt der Server aus (`source: "provision"`), die Server-Adresse kommt aus dem Installer (`detect:baseurl`). Damit entfällt der Absatz „bis die Avalonia-Einstellungs- ansicht steht, von Hand in `Settings.json`" aus der [Integrationsbeschreibung](Deploymentcenter-Integration.md) — jedenfalls für frisch installierte Systeme. Der Installer schreibt Lizenzschlüssel und Token **im Klartext**; er kennt unsere DPAPI-Hülle nicht. Das ist in Ordnung und abgesichert: `SecretProtector.Unprotect` gibt Klartext unverändert zurück, beim ersten Speichern wird verschlüsselt. Der Test dazu steht in `SecretProtectorTests` und nennt jetzt beide Gründe, damit ihn niemand als Altlast entfernt. ### Zwei Grenzen bleiben - **`CLAWD_CONFIG_DIR` kennt der Installer nicht.** Wer den Ort per Umgebungsvariable verlegt, muss die Datei selbst verschieben. - **Wer installiert, entscheidet mit** (SETUP warnt selbst davor): `config` bezieht sich auf das Konto, unter dem der Installer läuft. Für einen systemd-Dienst mit eigenem Benutzer heißt das: als dieser Benutzer installieren, sonst landet die Konfiguration im falschen Profil. ### Durchgespielt (2026-08-15) Anmeldung mit dem Installationskonto und der gesamte Ablauf gegen den echten Server: | Schritt | Ergebnis | |---|---| | `POST /api/setup/v1/login` | 201, Rolle `installer`, Recht `setup:install`, Token 30 min gültig | | `GET /api/setup/v1/catalog?platform=win-x64` | `clawddotnet` (dev=0.1.2) erscheint. **Ohne `platform` leer** — wie die Update-Prüfung, der Agent schickt `PlatformId.Current` | | `POST /api/setup/v1/token` | Anwendungstoken mit genau `watchdog:ping` + `bugtracker:report` | | Rechteschranke | Das ausgestellte Token kann **kein** `updateservice:publish` nachziehen (403 `provision_denied`) | | `SetupPaths.Resolve` gegen unsere `setup.json` | löst unter Windows auf `%APPDATA%\ClawdDotNet\Settings.json` auf (`fileWindows` greift) | | **Round-Trip** SDK schreibt → `SettingsManager` liest | trägt: camelCase-Keys treffen, der Klartext-Lizenzschlüssel geht durch den Entschlüsselungspfad (der Klartext unverändert durchreicht) | Damit ist der Weg vollständig: Ein frisch aufgesetztes System bekommt über den Installer Server-Adresse, Lizenzschlüssel und ein vom Server ausgestelltes Instanz-Token in die `Settings.json` geschrieben, die ClawdDotNet dann ohne Zutun lädt. ### Befund am Rande: alte Felder bleiben stehen Der `SetupWriter` merged in eine vorhandene `Settings.json`, statt sie zu ersetzen — richtig so, sonst gingen Logging-Einstellungen und Ähnliches verloren. Auf einem System mit einer **alten** Datei bleiben dabei Felder stehen, die es in der aktuellen `AppSettings` nicht mehr gibt (`watchdogServerUrl`, `licensePublicKeyBase64` aus der LicenseLabrador-Zeit). Harmlos — `SettingsManager` ignoriert unbekannte Felder beim Laden —, aber tote Einträge in der Datei. Kein Handlungsbedarf; beim ersten `Save` der laufenden App verschwinden sie. **Nicht enthalten** (SETUP §7): systemd-Unit und Windows-Dienst legt der Installer nicht an. Für den kopflosen Betrieb bleibt das unsere Aufgabe. --- ## 4a. Der Update-Weg ist durchgespielt Am 2026-08-14 gegen den echten Server geprüft, nicht nur gebaut. Ausgangslage: das 0.1.0-Paket mit Lizenzschlüssel geladen und entpackt — also eine Installation, wie sie beim Kunden aussieht — plus eine selbst angelegte Datei, die in keinem Manifest steht. | Fall | Ergebnis | |---|---| | **0.1.0 → 0.1.1** | RC 0. SHA256 des Pakets und 117 Manifest-Hashes geprüft. Fremde Datei unangetastet, `setup.json` da, `update-agent.exe` neu im Ziel | | **Rücksprung 0.1.1 → 0.1.0** | RC 0. `update-agent.exe` als nicht mehr zum Release gehörig **entfernt** — und nur die, die fremde Datei blieb liegen | | **Ohne Lizenzschlüssel** | `UNAUTHORIZED: … Erwartet wird der Lizenzschluessel dieser Installation`, RC 2. Sauber von einem Netzfehler unterschieden | | **Abbruch mitten im Schreiben** (Datei exklusiv gesperrt) | RC 1, „Vorheriger Stand wurde wiederhergestellt". Version, Dateizahl und Inhalt unverändert — die Installation blieb lauffähig | Damit trägt die Zusage aus §4B des UpdateService-Handbuchs: Ein Abbruch hinterlässt keine halbe Installation, und verwaiste Dateien werden aufgeräumt, ohne fremde anzufassen. Zwei Kleinigkeiten am Rand: - Nach dem gescheiterten Lauf blieb ein **leeres** `.dc-update-backup/` zurück. Kein Speicherverlust — der Rollback hatte alles zurückgeholt —, und der nächste erfolgreiche Lauf hat es entfernt. Ein leeres Verzeichnis dieses Namens sieht für einen Betreiber aber nach „Update hängt" aus. - Der Agent weist bei **jedem** Lauf auf das unsignierte Release hin. Das ist richtig so und wird erst still, wenn §5 erledigt ist. --- ## 5. Signierte Releases (2.2) — Schlüssel steht, Prüfung getestet Der Signierschlüssel ist seit dem 2026-08-14 serverseitig hinterlegt (RSA-SHA256, `canonical-line-v1`). **0.1.2 ist das erste signierte Release**; 0.1.0 und 0.1.1 bleiben unsigniert, weil serverseitig beim Veröffentlichen signiert wird. Am Testsystem durchgespielt: | Fall | Ergebnis | |---|---| | Signiertes 0.1.2 mit `--require-signature` | RC 0, kein Unsigniert-Hinweis mehr | | Unsigniertes 0.1.1 mit `--require-signature` | **RC 1, Abbruch vor dem Herunterladen** | | Unsigniertes 0.1.1 ohne die Pflicht | RC 0 mit Hinweis — wie dokumentiert | Der öffentliche Schlüssel wird beim ersten Lauf geholt und als `dc-release-pubkey.pem` neben dem Agenten festgehalten. Ein später abweichender Schlüssel fällt damit auf. ### Offen: die Pflicht ist aus der Anwendung heraus nicht erreichbar `--require-signature` gibt es **nur als Kommandozeilenschalter**. `UpdateClient.LaunchUpdateAgent` — der vom Handbuch empfohlene Weg, den auch wir benutzen — hat dafür keinen Parameter, und der Agent liest keine Umgebungsvariable dafür (`Program.cs:66` liest ausschließlich `HasFlag(args, "--require-signature")`). Damit läuft jede Anwendung, die den empfohlenen Weg geht, ohne Signaturprüfung, während derselbe Vorgang von Hand auf der Kommandozeile geschützt wäre. Wir könnten den Start selbst nachbauen — dann verlieren wir aber `--restart`, `--wait-for-pid` und `--wait-timeout`, also genau die Handgriffe, für die es die Hilfsmethode gibt. **Gemeldet.** Sobald `LaunchUpdateAgent` einen Parameter dafür hat, setzen wir ihn: Alle unsere Releases ab 0.1.2 sind signiert, ein Rückschritt auf unsignierte Stände wäre danach kein Verlust. --- ## 6. Reihenfolge 1. **Zugangsschutz** (§1) — erledigt, muss im ersten Release drin sein. 2. **`setup.json`** (§4) — erledigt, wird mit dem ersten Paket ausgeliefert. 3. **Release-Strecke** (§2): `packager.config.json`, erster Testlauf nach `dev`. 4. **Update anwenden** (§3): Agent mitliefern, `maintenance` beim Update melden. 5. **Signatur scharf** (§5), sobald der Serverschlüssel steht. Schritt 3 ist die Voraussetzung für alles Weitere: Solange kein Release veröffentlicht ist, lässt sich weder Update noch Erstinstallation erproben — und die `setup.json` wirkt erst, wenn sie in einem Paket steckt. --- ## 7. Befunde vom 2026-08-13 — alle behoben Zur Nachvollziehbarkeit, weil einige unsere Umsetzung geformt haben: | Befund | Behoben durch | |---|---| | `.htpasswd` enthielt alle Lizenzschlüssel im Klartext (Benutzernamenspalte wird nicht gehasht) | `ReleaseGuard::licenseUsername()` leitet `lic_` ab; `ReleaseCredentials.UsernameForLicenseKey` bildet dieselbe Ableitung nach. Die Datei enthält jetzt nur noch bcrypt über einen hochentropen Schlüssel | | Doku beschrieb Nginx, der Schutz greift nur unter Apache; WebUI meldete „GESCHÜTZT" allein anhand vorhandener Dateien | Echter HTTP-Selbsttest (`ReleaseGuard::selfTest`, erwartet 401), Warnhinweis und eigener Nginx-Abschnitt in der Doku | | UPDATESERVICE §7 dokumentierte `{"status":"ok"}`, der Code liefert `"success"` | Doku berichtigt | | `setup.json` schrieb nur ins Installationsverzeichnis | `location`-Angabe am Ziel plus Variablenersetzung in `file` | ### Offen aus dem ersten Release (2026-08-13) | Befund | Wirkung | |---|---| | **Veröffentlichen löst `ReleaseGuard` nicht aus.** `regenerateForProject` läuft nur bei Lizenzänderungen, Projektlöschung, Kontoänderungen und im Sechs-Stunden-Turnus von `cli/tick.php`. Ein Produktverzeichnis entsteht aber erst beim ersten Upload | `/releases/clawddotnet/` war nach dem Upload **ohne `.htaccess`** — das frische Paket bis zum nächsten Turnuslauf für jeden ladbar. Der Turnus hat es inzwischen geschlossen (401 bestätigt). Ein Aufruf am Ende von `/api/updateservice/v1/publish` würde das Fenster ganz vermeiden; das Verzeichnis existiert dort bereits | | **Die Prüfvorschrift aus UPGRADE §16.4 meldet falsch grün.** `curl -I …/.htpasswd → 403` trifft auch dann zu, wenn die Datei gar nicht existiert: Apache sperrt `.ht*` global | Wir hatten 403 auf `.htpasswd` **und** 200 auf `package.tar.gz`. Aussagekräftig ist nur der Paket-Abruf ohne Zugangsdaten | | **Kein Signierschlüssel auf dem Server.** `security.release_private_key` ist nicht gesetzt (UPGRADE §15.2) | Releases sind unsigniert, der Agent kann die Herkunft nicht prüfen. `--require-signature` ist damit unbenutzbar | Die Ableitung des Benutzernamens muss auf beiden Seiten zeichengenau übereinstimmen — `lic_` plus die ersten 16 Hexzeichen des SHA-256 über den getrimmten Schlüssel. Wer eine Seite ändert, sperrt die gesamte Installationsbasis aus.