Files
ClawdDotNet/docs/archiv/Deploymentcenter-2.4-Integrationsplan.md
T
RichardandClaude Opus 5 740649789e Eine Roadmap statt neun verteilter Listen
Der Stand lag ueber eine Bestandsaufnahme, drei Konzeptpapiere, vier
Umsetzungsplaene und zwei Deploymentcenter-Dokumente verteilt - jedes mit
eigener Reihenfolge, teils widersprueglich. Alle offenen Punkte daraus sind
in docs/Roadmap.md zusammengefuehrt.

Aufbau der neuen Roadmap
- Statusvokabular: erledigt / beschlossen-offen / Entscheidung noetig /
  zurueckgestellt / Idee. Damit steht das Geparkte sichtbar drin, statt in
  einem Konzeptpapier zu verschwinden.
- Herkunft-Spalte traegt das alte Kuerzel (S4, K2, B9, T4, F-A1, C1, DC1),
  damit die archivierten Papiere auffindbar bleiben, ohne sie zu lesen.
- Abschnitt 1 Reihenfolge, 2 offene Entscheidungen, 3 die Vorhaben nach
  Bereich, 4 Ideenspeicher, 5 Chronik des Erledigten, 6 Modell-Einstufung,
  7 Herkunftskarte.

Was dabei sichtbar wurde
- Neun Entscheidungen blockieren Arbeit, ohne dass sie Aufwand kosten -
  allen voran Matrix oder Rocket.Chat. Sie stehen jetzt gesammelt in
  Abschnitt 2 statt verstreut in den Diskussionsteilen der Konzepte.
- B8 (keine Tiefenbegrenzung bei AgentComm) ist unveraendert offen. Das ist
  keine Theorie: A haelt sein Gate, waehrend es auf B wartet - ruft B nun A,
  warten beide bis zum Timeout. Der Testfall A13 dafuer fehlt bis heute.
- Die vier Umsetzungsplaene vom 2026-08-05 sind alle unumgesetzt und waren
  in keiner Roadmap verzeichnet.

Archiv
Sechs Dokumente ziehen nach docs/archiv/: Bestandsaufnahme, Konzepte
Backup/Finanz/Analyse, Linux-Portierung-Analyse, Lizenz-HardwareId-v2
(gegenstandslos - LicenseLabrador ist ersetzt), Deploymentcenter-Review und
der 2.4-Integrationsplan. Sie bleiben als Begruendung lesbar, werden aber
nicht mehr fortgeschrieben; das README ordnet jedes einzeln ein und warnt,
dass ihre Quelltext-Verweise ins Leere gehen koennen.

Bauplan bleibt Bauplan
Taskboard, Audit, Staging, Memory, Agentenkommunikation, RocketChat und
Deploymentcenter-Integration bleiben in docs/ - sie sind die Detailvorgabe
fuer die Umsetzung, nicht Vorhabenlisten. Jedes bekommt oben eine Zeile,
die seine Rolle und den Umsetzungsstand nennt und auf die Roadmap zeigt.
Dasselbe fuer die vier Umsetzungsplaene: die Reihenfolge gilt in der
Roadmap, nicht im Plan.

Nebenbei repariert: Taskboard-Konzept und Teststrategie verwiesen auf
AgentScheduler und ToolJobScheduler, die seit der Scanner-Konsolidierung
geloescht sind. Alle Dokument-Verweise in docs/ sind geprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:31:06 +02:00

300 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.