Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag. Oberflaeche - Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml (eigene ControlThemes statt Fluent umzufaerben). - Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite statt eigenem Fenster, Texteditor-Fenster. - ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt. Rocket.Chat - Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird nur, wenn wirklich etwas anliegt. - send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist. - Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei Annahmen waren falsch und sind korrigiert. Deploymentcenter - DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json) erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke. - AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
300 lines
16 KiB
Markdown
300 lines
16 KiB
Markdown
# 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 <dotnet-publish-Ausgabe>
|
||
```
|
||
|
||
- 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: `<Version>` 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_<sha256[0..16]>` 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.
|