339 lines
17 KiB
Markdown
339 lines
17 KiB
Markdown
# Deploymentcenter-Integration
|
||
|
||
Stand: 2026-08-08, Deploymentcenter **2.1**. Ersetzt den früheren
|
||
`Integrationsplan-WatchDog-LicenseLabrador.md`.
|
||
|
||
ClawdDotNet spricht das [Deploymentcenter](../../Deploymentcenter/docs/README.md) als
|
||
**eine** Gegenstelle an. Vorher waren es zwei Fremdprojekte mit je eigenem Server,
|
||
eigenem Schlüssel und eigener Anleitung:
|
||
|
||
| Vorher | Jetzt |
|
||
|---|---|
|
||
| WatchDog (`watchdog.mhdf.de`, `X-Watchdog-Key`) | Deploymentcenter-Modul Watchdog, `Authorization: Bearer` |
|
||
| LicenseLabrador (`license.mhdf.de`, Ed25519-Public-Key) | Deploymentcenter-Modul Lizenz |
|
||
| — | Update-Prüfung |
|
||
| — | Fehler-Stream (ungefangene Ausnahmen) |
|
||
| — | Bugtracker |
|
||
|
||
Eine Adresse, ein Token. Beides steht in den Anwendungseinstellungen.
|
||
|
||
---
|
||
|
||
## 1. Was der Betreiber einzutragen hat
|
||
|
||
| Ort | Wert |
|
||
|---|---|
|
||
| Einstellungen → Deploymentcenter → **Server-URL** | `https://dc.mhdf.de` (Vorgabe) |
|
||
| Einstellungen → Deploymentcenter → **Token** | Master-Token mit `watchdog:ping` + `bugtracker:report` |
|
||
| Einstellungen → Lizenz → **Lizenzschlüssel** | Der Schlüssel für das Projekt `clawddotnet` |
|
||
| Worker-Tab → Dienst **Instanz-Watchdog** | einschalten, greift beim nächsten Start der Instanz |
|
||
|
||
Das Token entsteht im WebUI unter **Token-Verwaltung → Master-Token erstellen**. Es
|
||
wird verschlüsselt (DPAPI) in `Settings.json` abgelegt.
|
||
|
||
> **Bis die Avalonia-Einstellungsansicht steht**, gibt es für diese Felder noch keine
|
||
> Oberfläche — die Seite „Einstellungen" ist ein Platzhalter. Die Werte kommen
|
||
> vorläufig von Hand in `Settings.json` (Ort steht beim Start im Protokoll:
|
||
> `%APPDATA%\ClawdDotNet\Settings.json`, unter Linux `$XDG_CONFIG_HOME`) bzw. in die
|
||
> `instance.json` der Instanz. Das Token wird beim ersten Speichern durch die Anwendung
|
||
> verschlüsselt; im Klartext eingetragen funktioniert es ebenfalls, weil der
|
||
> `SecretProtector` beide Richtungen verträgt.
|
||
|
||
**Serverseitig ist eine Sache Pflicht**, sonst ist die Überwachung wertlos: der
|
||
Evaluator-Cron. Ohne ihn ändert sich ein Monitor-Zustand nur beim Eintreffen eines
|
||
Heartbeats — eine abgestürzte Instanz bliebe dauerhaft grün.
|
||
|
||
```bash
|
||
* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Watchdog — ein Monitor je Instanz
|
||
|
||
Das war die Vorgabe und ist jetzt sauber abgedeckt: Der Server führt Monitore über das
|
||
Paar `source` + `instance` (`UNIQUE KEY uq_monitor (source, instance)` in
|
||
`sql/schema.sql`). Alle Instanzen melden unter `source = "clawddotnet"` und tragen ihre
|
||
eigene `instance`. Fällt eine von dreien aus, fällt genau deren Monitor — und nur der
|
||
schlägt Alarm.
|
||
|
||
`instance` ist standardmäßig die `InstanceId` (stabil, aber im Dashboard nichtssagend).
|
||
In den Instanz-Einstellungen lässt sich stattdessen ein Name eintragen
|
||
([`WatchdogConfig.Instance`](../src/ClawdDotNet.Core/Config/WatchdogConfig.cs)); ein
|
||
späterer Wechsel legt allerdings einen neuen Monitor an.
|
||
|
||
**Eine Registrierung vorab gibt es nicht mehr.** Der Monitor entsteht beim ersten
|
||
Heartbeat von selbst (`INSERT … ON DUPLICATE KEY UPDATE`). Der frühere Weg über
|
||
`POST /api/register` hatte im Deploymentcenter nie ein Gegenstück — die alte Anbindung
|
||
lief in dieser Form also gegen einen Endpunkt, den es nicht gibt.
|
||
|
||
### Was der Heartbeat trägt
|
||
|
||
```
|
||
POST /api/watchdog/v1/ping
|
||
Authorization: Bearer <Instanz-Token>
|
||
```
|
||
|
||
| Feld | Inhalt |
|
||
|---|---|
|
||
| `source` / `instance` | `clawddotnet` / InstanceId bzw. eingestellter Name |
|
||
| `status` | `ok`, `warning`, `error` — beim Beenden `stopped` |
|
||
| `interval` | 60 s (Vorgabe). Daraus leitet der Evaluator ab: 2× → `warning`, 4× → `down` |
|
||
| `message` | Instanzname + Kurzbegründung |
|
||
| `os` | Betriebssystem + .NET-Version |
|
||
| `version` | Produktversion (2.1). Landet in `watchdog_monitors.app_version` — bei mehreren Instanzen der Unterschied zwischen „läuft" und „läuft noch auf der alten Fassung" |
|
||
| `checks` | `agents`, `scheduler`, `budget` — siehe unten |
|
||
| `metrics` | `agentCount`, `runningChats`, `todayCostUsd`, `todayTokens` |
|
||
|
||
### `checks` — der eigentliche Gewinn
|
||
|
||
Ein Heartbeat beweist nur, dass ein Faden läuft. Deshalb geht der selbst ermittelte
|
||
Zustand je Teilbereich mit; schlägt eine Prüfung fehl, stuft der Server einen als `ok`
|
||
gemeldeten Beat auf `warning` herab und nennt in der Antwort die betroffene.
|
||
|
||
| Prüfung | Fehlschlag bedeutet |
|
||
|---|---|
|
||
| `agents` | Kein OpenRouter-Key — die Instanz läuft, arbeitet aber nichts ab |
|
||
| `scheduler` | Die Taktschleife des Aufgaben-Scanners ist ausgestiegen |
|
||
| `budget` | Tagesgrenze für Kosten oder Token erreicht |
|
||
|
||
„Scanner noch nicht gestartet" gilt **nicht** als Fehlschlag: Er läuft erst nach der
|
||
Startabgleichung an, der erste Heartbeat geht sofort raus. Sonst gäbe es bei jedem
|
||
Start ein `warning_raised` und kurz darauf ein `recovered` — zwei Einträge im
|
||
Ereignisprotokoll für einen Normalvorgang.
|
||
|
||
### Metriken sind nur Zahlen
|
||
|
||
Der Server legt numerische Werte mit Zeitstempel ab (14 Tage) und vergleicht den
|
||
aktuellen Wert mit dem Sieben-Tage-Schnitt desselben Monitors. Nicht-numerische Werte
|
||
verwirft er dabei stillschweigend — `instanceId`, `instanceName` und `buildVersion`
|
||
standen früher in den Metriken und waren dort wirkungslos. Beschreibendes steht jetzt
|
||
in `message` und `os`.
|
||
|
||
### Angekündigtes Ende
|
||
|
||
Beim Herunterfahren geht ein Heartbeat mit `status: "stopped"` raus, danach das Ereignis
|
||
`stopped_graceful`. Der Evaluator lässt einen so gemeldeten Monitor in Ruhe. Ohne das
|
||
erzeugte jedes geplante Beenden wenige Minuten später einen Fehlalarm.
|
||
|
||
Nebenbei korrigiert: Die alte Anbindung schickte die Ereignisarten `start` und `stop` —
|
||
beide stehen nicht auf der Liste des Servers und landeten stillschweigend als `started`.
|
||
Jetzt sind es `started` und `stopped_graceful`.
|
||
|
||
### Token je Instanz
|
||
|
||
Beim ersten Start tauscht die Instanz das anwendungsweite Token über
|
||
`POST /api/tokens/v1/provision` gegen ein eigenes, auf `watchdog:ping` und
|
||
`bugtracker:report` beschränktes Sub-Token und legt es verschlüsselt in der
|
||
Instanzkonfiguration ab. Danach liegt auf der Instanz nicht mehr das Master-Token, und
|
||
ein einzelner Zugang lässt sich widerrufen, ohne die anderen mitzunehmen.
|
||
|
||
Das ist derselbe Zweck, den die frühere Selbstregistrierung hatte. Scheitert es (etwa
|
||
weil das hinterlegte Token selbst ein Sub-Token ist und keine weiteren ausstellen darf),
|
||
wird mit dem hinterlegten Token gemeldet — Monitoring, das nur bei perfekter Rechtelage
|
||
läuft, ist genau dann still, wenn man es braucht.
|
||
|
||
---
|
||
|
||
## 3. Lizenz
|
||
|
||
Startprüfung in [`LicenseGate`](../src/ClawdDotNet.App/Services/LicenseGate.cs). Die
|
||
Offline-Gnadenfrist steckt im SDK: Es legt nach jeder erfolgreichen Prüfung einen mit
|
||
AES-GCM verschlüsselten, an die Hardware gebundenen Zwischenspeicher an (`LLS2`,
|
||
seit 2.1 Schema 3) und trägt damit über Ausfälle hinweg.
|
||
|
||
### Urteil und Fehlversuch sind zwei verschiedene Dinge
|
||
|
||
Das ist der Kern der 2.1-Anpassung. `LicenseValidationResult.IsTransient` unterscheidet:
|
||
|
||
| | Statuswerte | Folge |
|
||
|---|---|---|
|
||
| **Urteil des Servers** | `revoked`, `expired`, `not_found`, `activation_limit`, `suspended`, `clock_rollback` | Anwendung startet nicht bzw. beendet sich |
|
||
| **Kein Urteil erhalten** | `server_unavailable`, `cache_expired` | Warnung, Betrieb läuft weiter |
|
||
|
||
Nur das Urteil sperrt. Ein Serverausfall darf nicht jede Installation gleichzeitig
|
||
aussperren — und eine Drosselung (`429`) oder ein `500` sind Aussagen über den Server,
|
||
nicht über die Lizenz. Das gilt an beiden Stellen gleich: Startprüfung und laufende
|
||
Nachprüfung fragen dasselbe Merkmal ab.
|
||
|
||
> **Bewusst in Kauf genommen:** Ein Rechner, der die Gegenstelle nie erreicht, läuft
|
||
> damit auf Dauer mit Warnung weiter — auch nach Ablauf der Gnadenfrist
|
||
> (`cache_expired` ist laut SDK-Vertrag vorübergehend). Wer das anders will, prüft in
|
||
> [`LicenseGate`](../src/ClawdDotNet.App/Services/LicenseGate.cs) zusätzlich auf
|
||
> `cache_expired` und behandelt es als Urteil. Es sollte eine Entscheidung sein, nicht
|
||
> ein Nebeneffekt.
|
||
|
||
### Offline-Gnadenfrist ist echt begrenzt
|
||
|
||
Seit 2.1 wertet der Client `cache_ttl_hours` des Projekts aus (Vorgabe 168 h). Vorher
|
||
galt faktisch das Ablaufdatum der Lizenz — bei einer Lizenz bis 2040 also unbegrenzt.
|
||
Der verbleibende Rest steht in `CacheExpiresAt` und wird beim Start angezeigt, wenn die
|
||
Prüfung aus dem Zwischenspeicher kam.
|
||
|
||
`state.dat` steigt auf Schema 3; Schema 2 wird weiter gelesen. Ein Rückschritt auf ein
|
||
älteres SDK verwirft den Zwischenspeicher — dann ist einmal eine Online-Prüfung nötig.
|
||
|
||
### Kein Public-Key mehr
|
||
|
||
Die frühere Fassung führte einen Ed25519-Public-Key als „Vertrauensanker". Im
|
||
Deploymentcenter gibt es dazu keine Gegenseite — der Client liest ausschließlich das Feld
|
||
`status`. Ein Schlüssel, der nichts prüft, ist schlimmer als keiner: Er lässt Schutz
|
||
vermuten, wo keiner ist. Details in
|
||
[Deploymentcenter-Anbindung-Review](Deploymentcenter-Anbindung-Review.md), Abschnitt 2.1.
|
||
|
||
### Der Projekt-Slug ist `clawddotnet`
|
||
|
||
[`LicenseInfo.ProductSlug`](../src/ClawdDotNet.App/Services/LicenseInfo.cs) gilt für
|
||
**alle** Module — Lizenz, Bugtracker, Fehler-Stream und Update-Prüfung greifen auf
|
||
dieselbe Tabelle `dc_projects` zu.
|
||
|
||
Bis zur Umstellung stand hier `clawd`, der Name aus dem LicenseLabrador-Backend. Das
|
||
ist eine Falle mit langer Zündschnur: Der Server beantwortet ein unbekanntes Projekt mit
|
||
demselben `not_found` wie einen unbekannten Schlüssel — der Unterschied steht
|
||
ausschließlich in `message` (`"Project not found"` gegen `"Invalid license key"`). Wer
|
||
den Text nicht durchreicht, sucht den Fehler beim Lizenzschlüssel, während das Projekt
|
||
gar nicht existiert. Der Torwächter gibt die Serverantwort deshalb mit aus und schreibt
|
||
sie ins Protokoll.
|
||
|
||
### Deaktivieren läuft über das WebUI
|
||
|
||
`POST /api/license/v1/deactivate` verlangt den `shared_key` des Servers. Der gehört nicht
|
||
in eine ausgelieferte Anwendung, deshalb ist der Weg die Hardware-Liste im WebUI
|
||
(Schaltfläche „Freigeben").
|
||
|
||
### Laufende Nachprüfung
|
||
|
||
[`LicenseWatch`](../src/ClawdDotNet.App/Services/LicenseWatch.cs) prüft alle zwölf
|
||
Stunden nach — dieselbe Unterscheidung wie oben. Ohne das wirkt ein Widerruf erst beim
|
||
nächsten Start, bei einem wochenlang laufenden Dienst also praktisch nie.
|
||
|
||
---
|
||
|
||
## 4. Version und Updates
|
||
|
||
### Eine Stelle für die Version
|
||
|
||
`<Version>` in [Directory.Build.props](../Directory.Build.props) ist die Wahrheit.
|
||
`Deploymentcenter.BuildInfo.targets` (seit 2.1 einbindbar) erzeugt daraus zur
|
||
Übersetzungszeit `ClawdDotNet.App.ReleaseInfo` mit `Version`, `GitCommit`,
|
||
`GitCommitShort`, `BuildDateUtc`, `Channel` und `Summary`.
|
||
|
||
Der Wert geht an vier Stellen nach draußen, die vorher alle geraten haben:
|
||
|
||
| Stelle | Vorher |
|
||
|---|---|
|
||
| Aktivierungsliste (`app_version`) | fest `"1.0.0"` im SDK — jede Installation gleich |
|
||
| Heartbeat (`version`) | gab es nicht |
|
||
| Fehlermeldungen (`build`) | — |
|
||
| Versionsvergleich der Update-Prüfung | `0.0.<BuildInfo.Build>`, behelfsweise |
|
||
|
||
Die Klasse heißt bewusst `ReleaseInfo`, nicht `BuildInfo`: Diesen Namen trägt in
|
||
`ClawdDotNet.Core` schon ein von Hand geführter Zähler mit Änderungstext. Zwei
|
||
gleichnamige Klassen mit verschiedener Bedeutung wären eine Falle. Umgestellt über
|
||
`DeploymentcenterBuildInfoClass` in der csproj.
|
||
|
||
### Prüfung
|
||
|
||
Einmalig beim Start gegen `GET /api/updateservice/v1/check`, über
|
||
`Deploymentcenter.Client.UpdateClient`. Läuft nebenher und blockiert nichts; liegt eine
|
||
neuere Version vor, erscheint ein Hinweis mit Changelog. Ob und wann aktualisiert wird,
|
||
entscheidet der Benutzer — eine Anwendung, die sich beim Start selbst beendet, um sich zu
|
||
erneuern, ist genau dann im Weg, wenn man sie braucht.
|
||
|
||
Seit 2.1 liefern beide Wege vollständige Daten: die statische `latest.json` in camelCase,
|
||
die API in snake_case, jeweils über ein eigenes Modell (`VersionInfo` bzw.
|
||
`ApiReleaseInfo`). Vorher kam über den API-Zweig außer der Versionsnummer nichts an — und
|
||
der ist genau der Rückfall, wenn die `latest.json` fehlt. Die Download-Adresse wird
|
||
mitgeführt (`UpdateAvailability.DownloadUrl`), damit der `update-agent` später ohne
|
||
weitere Änderung anschließen kann.
|
||
|
||
Der `update-agent` ist noch **nicht** eingebunden, und solange kein Release über
|
||
`pack-and-deploy` veröffentlicht wird, hat die Prüfung nichts zu finden.
|
||
|
||
---
|
||
|
||
## 5. Fehler-Stream
|
||
|
||
Ungefangene Ausnahmen gehen an `POST /api/errors/v1/report`. Verdrahtet in
|
||
[`App.axaml.cs`](../src/ClawdDotNet.Desktop/App.axaml.cs) an drei Stellen:
|
||
`AppDomain.UnhandledException`, `TaskScheduler.UnobservedTaskException` und
|
||
`Dispatcher.UIThread.UnhandledException`.
|
||
|
||
Erst nach dem Aufbau verdrahtet, nicht in `Main`: Vorher gibt es weder Einstellungen
|
||
noch Token. Die Kehrseite ist bewusst in Kauf genommen — ein Absturz *während* des
|
||
Starts erreicht das Deploymentcenter nicht, steht aber im Protokoll.
|
||
|
||
**Eigene Drosselung** in
|
||
[`ErrorReporter`](../src/ClawdDotNet.Core/Deploymentcenter/ErrorReporter.cs): Derselbe
|
||
Fehler (Typ + oberste Stelle im Stacktrace) geht höchstens einmal alle fünf Minuten
|
||
raus. Der Server drosselt auch, aber erst, nachdem die Anfragen über die Leitung waren.
|
||
Die Fehlermeldung selbst gehört nicht zum Kennzeichen — sie enthält oft wechselnde
|
||
Werte, und dann wäre jeder Aufruf ein neuer Fehler.
|
||
|
||
Bekannte, harmlose Fehler lassen sich serverseitig unter **Bugtracker → Ignore-Regeln**
|
||
stummschalten. Sie werden weiter gezählt; der Zähler ist der Zweck: Dass ein bekannter
|
||
Fehler auftritt, ist normal — dass er plötzlich hundertmal so oft auftritt, bedeutet,
|
||
dass sich etwas geändert hat.
|
||
|
||
---
|
||
|
||
## 6. Bugtracker
|
||
|
||
[`BugtrackerClient`](../src/ClawdDotNet.Core/Deploymentcenter/BugtrackerClient.cs) für
|
||
bewusst formulierte Einträge (Fehler, Wunsch, Idee) mit Titel und Beschreibung, gegen
|
||
`POST /api/bugtracker/v1/report`. Der Absender wird serverseitig aus dem Token
|
||
abgeleitet und lässt sich nicht frei wählen.
|
||
|
||
Der Client ist da und über `AppHost.Deploymentcenter.Bugtracker` erreichbar; **eine
|
||
Oberfläche dafür fehlt noch** („Fehler melden"-Schaltfläche). Ein Agenten-Tool wäre der
|
||
nächste sinnvolle Schritt — Agenten könnten dann selbst Wünsche und Fehler eintragen,
|
||
und der Agenten-Workflow des Deploymentcenters (Claim/Lease über
|
||
`manage?action=next`) würde sie abarbeiten.
|
||
|
||
---
|
||
|
||
## 7. Wo was liegt
|
||
|
||
| Datei | Inhalt |
|
||
|---|---|
|
||
| [`Deploymentcenter/DeploymentcenterApi.cs`](../src/ClawdDotNet.Core/Deploymentcenter/DeploymentcenterApi.cs) | Gemeinsamer Unterbau: Bearer-Header, HTTPS-Pflicht, Umschlag auspacken, `DeploymentcenterException` mit stabilem `Code` |
|
||
| [`Deploymentcenter/Watchdog/`](../src/ClawdDotNet.Core/Deploymentcenter/Watchdog) | Heartbeat-Client, Zustandsermittlung, Takt-Dienst |
|
||
| [`Deploymentcenter/ErrorReporter.cs`](../src/ClawdDotNet.Core/Deploymentcenter/ErrorReporter.cs) | Fehler-Stream mit Drosselung |
|
||
| [`Deploymentcenter/BugtrackerClient.cs`](../src/ClawdDotNet.Core/Deploymentcenter/BugtrackerClient.cs) | Bugtracker-Einträge |
|
||
| [`Deploymentcenter/TokenProvisioner.cs`](../src/ClawdDotNet.Core/Deploymentcenter/TokenProvisioner.cs) | Sub-Token je Instanz |
|
||
| [`Services/DeploymentcenterService.cs`](../src/ClawdDotNet.App/Services/DeploymentcenterService.cs) | Verdrahtung: Token beschaffen, Heartbeat starten, Update prüfen |
|
||
| [`Services/LicenseGate.cs`](../src/ClawdDotNet.App/Services/LicenseGate.cs) | Startprüfung |
|
||
| [`Services/LicenseWatch.cs`](../src/ClawdDotNet.App/Services/LicenseWatch.cs) | Laufende Nachprüfung |
|
||
|
||
Die Lizenz läuft bewusst **nicht** über `DeploymentcenterApi`: Sie hat ein eigenes
|
||
Antwortformat (kein `status`/`error`-Umschlag — `status` trägt dort den Lizenzzustand),
|
||
einen eigenen Zwischenspeicher und muss vor allem anderen laufen.
|
||
|
||
Watchdog, Fehler-Stream und Bugtracker deckt das SDK `Deploymentcenter.Client` nicht ab;
|
||
dafür ist der eigene Unterbau da. Hardware-ID v2, Lizenz-Zwischenspeicher und
|
||
Update-Prüfung kommen aus dem SDK — die nachzubauen wäre Verdopplung.
|
||
|
||
---
|
||
|
||
## 8. Tests
|
||
|
||
[`tests/ClawdDotNet.Core.Tests/Deploymentcenter/`](../tests/ClawdDotNet.Core.Tests/Deploymentcenter):
|
||
Bearer-Header, Fehlerumschlag → Ausnahme mit Code (auch bei HTTP 200), HTTPS-Pflicht mit
|
||
Localhost-Ausnahme, Heartbeat-Pfad und -Rumpf, zwei Instanzen → zwei Monitore, Checks
|
||
und Metriken, Antwort-Auswertung, Drosselung des Fehler-Streams, Sub-Token-Bezug.
|
||
|
||
---
|
||
|
||
## 9. Offen
|
||
|
||
1. **Oberfläche für den Bugtracker** — Client vorhanden, Schaltfläche fehlt.
|
||
2. **Agenten-Tool für den Bugtracker** — würde den Agenten-Workflow des
|
||
Deploymentcenters nutzbar machen.
|
||
3. **Release-Strecke** — `pack-and-deploy` aufrufen und `<Version>` dabei mitgeben,
|
||
danach `update-agent` einbinden. Die Versionsnummer selbst ist mit 2.1 erledigt.
|
||
4. **SDK als Git-Submodul** unter `external/` statt Cross-Repo-Pfad.
|
||
5. **Hierarchie** (`parent_source`): Läuft die Instanz auf einem Host, der selbst als
|
||
Monitor geführt wird, sollte sie ihn als übergeordnete Entität eingetragen bekommen —
|
||
sonst erzeugt ein Hostausfall eine Meldung je Instanz. Das ist im WebUI zu pflegen,
|
||
nicht im Client (siehe aber Anmerkung 2 in den Rückmeldungen).
|