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>
16 KiB
Deploymentcenter 2.2 – 2.4: Was noch zu tun ist
Stand: 2026-08-13. Ergänzt Deploymentcenter-Integration (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).
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.gitignoreausgeschlossen), Vorlage ohne Werte inpackager.config.example.json. ftpRemoteBaseDirist/releases, nicht/public_html/releaseswie 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:publishträgt — gezogen über/api/tokens/v1/provision. Das Master-Token gehört nicht in eine Konfigurationsdatei. deploy.pyist 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 stimmt mitclawddotnet.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 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
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 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).
Das Deploymentcenter hat daraufhin location am Ziel ergänzt (install, config,
data, home) samt Variablenersetzung in file. Damit ist der Weg frei;
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 — 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_DIRkennt der Installer nicht. Wer den Ort per Umgebungsvariable verlegt, muss die Datei selbst verschieben.- Wer installiert, entscheidet mit (SETUP warnt selbst davor):
configbezieht 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
- Zugangsschutz (§1) — erledigt, muss im ersten Release drin sein.
setup.json(§4) — erledigt, wird mit dem ersten Paket ausgeliefert.- Release-Strecke (§2):
packager.config.json, erster Testlauf nachdev. - Update anwenden (§3): Agent mitliefern,
maintenancebeim Update melden. - 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.