UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4
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>
This commit is contained in:
@@ -119,8 +119,25 @@ und Tippfehler in Bindungspfaden fallen erst zur Laufzeit auf.
|
||||
Benachrichtigung.
|
||||
- Befehle: `[RelayCommand] private void Speichern() { … }` → bindbar als
|
||||
`SpeichernCommand`.
|
||||
- Formatierung gehört in `Styles/Shell.axaml`, nicht an einzelne Steuerelemente.
|
||||
Vorhandene Klassen: `heading`, `caption`, `toolbar`, `card`, `statusbar`.
|
||||
- Formatierung gehört in `Styles/`, nicht an einzelne Steuerelemente. Seit der
|
||||
Umsetzung des Entwurfs aus `Mockup/` liegt sie in drei Dateien:
|
||||
`Theme.axaml` (Farben je Thema, Schriften), `Icons.axaml` (Symbolgeometrien),
|
||||
`Shell.axaml` (Steuerelement-Vorlagen und Stilklassen).
|
||||
Klassen: `h1`, `h2`, `kicker`, `label`, `caption`, `muted`, `mono`, `card`,
|
||||
`console`, `hr`, `sep`, `toolbar`, `thead`, `tr`, `th`, `td`, `tag`,
|
||||
`statusbar`, `topbar`, `sidebar`, `nav`; an Schaltflächen zusätzlich
|
||||
`primary`, `toolbar`, `ghost`, `flat`, `icon`.
|
||||
- Symbole über `Controls/StrokeIcon.cs` mit einer Geometrie aus `Icons.axaml`.
|
||||
Die Farbe wird geerbt — nicht gesetzt.
|
||||
- Rahmen mit Eckmarken über `Controls/BlueprintFrame.cs`. Sparsam: nur Dialoge
|
||||
und die Info-Karte.
|
||||
- Tabellen von Hand aus `Border.thead` + `ListBox.table`, nicht mit `DataGrid`.
|
||||
Das Paket ist nicht mehr referenziert.
|
||||
- **Avalonia 12 hat die Ressourcenschlüssel des Fluent-Themas umgebaut.** Die aus
|
||||
11er-Anleitungen bekannten Namen (`ButtonBackground`, `TextControlBackground`,
|
||||
`ControlCornerRadius` …) existieren nicht mehr; ein Setter darauf ist wirkungslos
|
||||
und fällt nicht auf. Für neue Steuerelemente deshalb eine eigene `ControlTheme`
|
||||
in `Shell.axaml` schreiben statt zu versuchen, Fluent umzufärben.
|
||||
- Ordner öffnen: `Core.Storage.SystemShell.OpenFolder(pfad)`. **Kein** `explorer.exe`.
|
||||
- Dateinamen erzeugen: `Core.Storage.PortableFileName.Sanitize(name)`.
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
# 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.
|
||||
+8
-1
@@ -221,9 +221,16 @@ Offen:
|
||||
|---|---|---|
|
||||
| DC1 | Oberfläche „Fehler melden" | Client vorhanden, Schaltfläche fehlt |
|
||||
| DC2 | Agenten-Tool für den Bugtracker | macht den Claim/Lease-Workflow des Deploymentcenters nutzbar |
|
||||
| DC3 | Release-Strecke: `pack-and-deploy` mit `<Version>` aufrufen, dann `update-agent` | ohne Release hat die Update-Prüfung nichts zu finden. Versionsnummer selbst ist erledigt (`Directory.Build.props` → `ReleaseInfo`) |
|
||||
| DC3 | Release-Strecke | **erledigt für win-x64/dev** ([`deploy/publish.py`](../deploy/publish.py), 0.1.1 veröffentlicht). Offen: `linux-x64` nach der Avalonia-Portierung, `prod` |
|
||||
| DC4 | SDK als Git-Submodul unter `external/` statt Cross-Repo-Pfad | betrifft auch die CI |
|
||||
| DC5 | Betreiber: Evaluator-Cron einrichten, Token ausstellen, `parent_source` pflegen | **ohne den Cron ist die Überwachung wertlos** |
|
||||
| DC6 | Update anwenden statt nur melden | **erledigt.** Agent liegt im Paket (Prüfsumme geprüft), Rückfrage in der Oberfläche, geordnetes Herunterfahren vor dem Agentenstart, `maintenance` an den Watchdog |
|
||||
| DC7 | Erstinstallation über `--action install` + `setup.json` | **erledigt.** Setup-Kette live durchgespielt (Login, Katalog, Token mit Rechteschranke), Round-Trip SDK-Writer → SettingsManager trägt. `fileWindows`/`fileLinux` gesetzt |
|
||||
| DC8 | Signaturpflicht (`--require-signature`) | Schlüssel steht, ab 0.1.2 wird signiert, Prüfung beidseitig getestet. **Blockiert:** `LaunchUpdateAgent` reicht den Schalter nicht durch — aus der Anwendung heraus nicht erzwingbar (gemeldet) |
|
||||
|
||||
Details: [Deploymentcenter 2.2–2.4 Integrationsplan](Deploymentcenter-2.4-Integrationsplan.md).
|
||||
Der Zugangsschutz aus 2.4 (Lizenzschlüssel als Basic-Auth-Zugang zur Release-Ablage) ist
|
||||
bereits umgesetzt — er muss im **ersten** veröffentlichten Release enthalten sein.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -62,6 +62,104 @@ Ebenso vorhanden und wiederverwendbar:
|
||||
Geprüft gegen die REST- und Realtime-API von Rocket.Chat. Alles Folgende ist
|
||||
Standardfunktion einer selbstgehosteten Instanz, kein Enterprise-Feature.
|
||||
|
||||
### 2.0 Prüfung gegen die echte Instanz (Rocket.Chat 8.7, August 2026)
|
||||
|
||||
Alles unten Stehende wurde gegen die Testinstanz gemessen, nicht aus der Dokumentation
|
||||
übernommen. **Drei Annahmen waren falsch** — sie sind hier korrigiert.
|
||||
|
||||
**Bestätigt:**
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---|---|
|
||||
| `subscriptions.get` als Sammelabruf | liefert je Raum `rid`, `t`, `name`, `unread`, `userMentions`, `groupMentions`, `alert` — genau der Vorfilter, auf dem der Poll steht |
|
||||
| `chat.postMessage`, auch mit `tmid` (Thread) | funktioniert |
|
||||
| `channels.history` / `groups.history` / `im.history` mit `oldest` | funktioniert; Raumart bestimmt den Endpunkt |
|
||||
| `subscriptions.read` | funktioniert; danach steht `ls` und `unread` fällt auf 0 |
|
||||
| `im.create`, `groups.create`, Senden in privaten Gruppen | funktioniert |
|
||||
| Erwähnungen | kommen als **`mentions[]`-Feld mit Benutzernamen** — der Erwähnungsfilter braucht kein Textparsen |
|
||||
| Unbekannter Raum | `400 [invalid-channel]` |
|
||||
| `users.create` mit Rolle `bot` | funktioniert (Admin) |
|
||||
|
||||
**Korrekturen:**
|
||||
|
||||
1. **Berechtigungsnamen.** Sie heißen `create-personal-access-tokens` (Rollen: `admin`,
|
||||
`user`) und `user-generate-access-token` (Rolle: `admin`) — nicht wie zuvor notiert.
|
||||
2. **Ein Admin kann *kein* Token für einen fremden Benutzer prägen.**
|
||||
`users.generatePersonalAccessToken` lehnt `userId` ab („must NOT have additional
|
||||
properties") — der Endpunkt gilt nur für den aufrufenden Benutzer.
|
||||
`users.createToken` verlangt ein `secret`, für das es in dieser Instanz keine
|
||||
Einstellung gibt. Beide Wege sind zu.
|
||||
3. **Systemnachrichten.** Die Historie liefert auch Ereignisse wie „Benutzer beigetreten"
|
||||
(Feld `t`, z. B. `uj`). Ohne Filter antwortet ein Agent auf einen Raumbeitritt. Im
|
||||
Tool umgesetzt und geprüft.
|
||||
|
||||
**Offen geblieben:** Das Ratenlimit (`API_Enable_Rate_Limiter` = an, 10 Aufrufe/60 s)
|
||||
griff bei 14 schnellen Aufrufen **nicht** — Administratoren umgehen es. Für einen
|
||||
Benutzer mit reiner `bot`-Rolle ist es damit **nicht** gemessen. Der Entwurf bleibt mit
|
||||
einem Sammelabruf je Takt weit darunter; nachzumessen, sobald ein Agenten-Benutzer
|
||||
nutzbar ist.
|
||||
|
||||
### 2.0.1 Der Stolperstein: 2FA verhindert die automatische Bereitstellung
|
||||
|
||||
Ein frisch per API angelegter Benutzer **kann sich nicht anmelden**: Rocket.Chat antwortet
|
||||
mit `totp-required` und schickt einen Code per E-Mail. Ursache ist
|
||||
`Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In` (in der Testinstanz aktiv) — jeder
|
||||
neue Benutzer bekommt E-Mail-2FA automatisch.
|
||||
|
||||
Geprüft und ausgeschlossen: `users.update` kennt kein Feld dafür („must NOT have
|
||||
additional properties"), `users.resetTOTP` betrifft nur App-basiertes TOTP.
|
||||
**Es gibt keinen Weg über die Admin-API, das E-Mail-2FA eines einzelnen Benutzers
|
||||
abzuschalten.**
|
||||
|
||||
Damit stehen drei Wege offen — die Entscheidung gehört dir, weil sie eine
|
||||
Sicherheitseinstellung berührt:
|
||||
|
||||
| Weg | Ablauf | Preis |
|
||||
|---|---|---|
|
||||
| **A (empfohlen)** | `Auto_Opt_In` global auf **aus**, dann Benutzer anlegen (Rollen `bot` + `user`), als dieser anmelden, PAT erzeugen, Passwort verwerfen | Neue **menschliche** Benutzer bekommen E-Mail-2FA dann nicht mehr automatisch. Bestehende Konten und TOTP bleiben unberührt |
|
||||
| **B** | Setting bleibt; jeder Agent braucht ein echtes Postfach, ClawdDotNet holt den 2FA-Code per IMAP (das Mail-Tool kann das) | Funktioniert, ist aber ein zerbrechlicher Umweg |
|
||||
| **C** | Halbautomatisch: API legt den Benutzer an, ein Mensch erzeugt das PAT einmalig in der Oberfläche | Kein „HR-Agent" möglich — bei jedem neuen Agenten Handarbeit |
|
||||
|
||||
**Weg A ist umgesetzt und gemessen** (August 2026): Nach dem Abschalten von `Auto_Opt_In`
|
||||
läuft die Kette vollständig durch —
|
||||
|
||||
```
|
||||
users.create (Rollen bot + user) → login als dieser Benutzer →
|
||||
users.generatePersonalAccessToken → PAT
|
||||
```
|
||||
|
||||
Damit ist ein „HR-Agent", der einen neuen Agenten samt Chat-Konto einrichtet, technisch
|
||||
möglich. Die Rolle `user` ist dabei nötig, **nicht** nur `bot`: Nur sie bringt die
|
||||
Berechtigung `create-personal-access-tokens` mit.
|
||||
|
||||
Ein Benutzer, der **vor** der Umstellung angelegt wurde, behält sein 2FA-Flag dauerhaft —
|
||||
er lässt sich nicht nachträglich retten und muss neu angelegt werden.
|
||||
|
||||
Ob sich `Auto_Opt_In` nach der Bereitstellung wieder einschalten lässt, ohne die
|
||||
bestehenden Agenten zu verlieren, ist plausibel (das Flag wird beim Anlegen gesetzt), aber
|
||||
weiterhin **nicht gemessen**.
|
||||
|
||||
Für das Tool selbst ist die Frage folgenlos: Es nimmt `userId` und `authToken` aus der
|
||||
Konfiguration entgegen, gleich woher sie stammen.
|
||||
|
||||
### 2.0.2 Zwei Fehler, die erst der Live-Test zeigte
|
||||
|
||||
Beide wären in keinem Schreibtischtest aufgefallen und sind behoben:
|
||||
|
||||
1. **`unread` zählt in dieser Instanz nur Erwähnungen.** Eine gewöhnliche Nachricht setzt
|
||||
allein `alert=true`. Schwerer wiegt: `userMentions` ist ein Zähler über *ungelesene*
|
||||
Erwähnungen und bleibt stehen, solange nichts gelesen wurde. Ein einziger alter,
|
||||
ungelesener Ruf machte den Raum damit dauerhaft „heiß" — und anschließend wurde **jede**
|
||||
weitere Nachricht ausgeliefert, auch ohne Erwähnung.
|
||||
*Behoben:* Die Erwähnungsprüfung sitzt jetzt an der einzelnen Nachricht; der
|
||||
Raum-Zähler ist nur noch ein billiger Vorfilter. Zusätzlich wird jeder geprüfte Raum als
|
||||
gelesen markiert, auch wenn nichts zu wecken war — sonst veralten die Zähler.
|
||||
2. **Die Startmarke verschluckte die erste echte Nachricht.** Ein Raum wird erst dann zum
|
||||
Kandidaten, wenn Verkehr da ist — genau dann setzte der alte Code aber „Marke auf jetzt,
|
||||
nichts wecken". Die auslösende Nachricht ging verloren.
|
||||
*Behoben:* Beim ersten Abruf eines Raums wird begrenzt zurückgeschaut
|
||||
(`initialLookbackMinutes`, Standard 5) statt zu überspringen.
|
||||
|
||||
### 2.1 Identität — ein echter Benutzer je Agent
|
||||
|
||||
Die Anforderung „jeder Agent mit eigenem Benutzer, in Gruppen und im Direktkontakt" ist
|
||||
@@ -198,27 +296,143 @@ ist das am Ende nicht optional.
|
||||
|
||||
### 2.8 Tool-Zuschnitt
|
||||
|
||||
**Umgesetzt** (`src/ClawdDotNet.Tools.RocketChat`, gegen die Testinstanz geprüft):
|
||||
|
||||
```
|
||||
Tool: RocketChat
|
||||
Aktionen: send_message | reply | send_file | list_rooms | read_room
|
||||
| mark_read | search
|
||||
Aktionen: send_message | reply | send_file | list_rooms | read_room | mark_read
|
||||
Job: rocketchat_poll
|
||||
```
|
||||
|
||||
Noch nicht umgesetzt: `search`.
|
||||
|
||||
**Dateiversand.** `send_file` schickt eine Datei aus dem Workspace direkt in einen Raum
|
||||
oder als Direktnachricht — der kurze Weg für „stell mir das zusammen und schick es rüber",
|
||||
ohne Umweg über die Cloud. Pfade tragen dieselben Prefixe wie bei FileRW und FTP
|
||||
(`personal:` / `shared:` / ohne Prefix), samt Prüfung gegen einen Ausbruch aus dem
|
||||
Verzeichnis. Grenze über `maxUploadMb` (Standard 25, Serverseite erlaubt 100).
|
||||
|
||||
Dabei zeigte sich die **dritte Doku-Korrektur**: Der Ein-Schritt-Endpunkt `rooms.upload`
|
||||
existiert in 8.7 nicht mehr — er antwortet mit einem nackten HTML-404. Aktuell ist ein
|
||||
zweistufiger Ablauf:
|
||||
|
||||
1. `rooms.media/:rid` nimmt die Datei als `multipart/form-data` und liefert eine Datei-Id;
|
||||
sichtbar ist damit noch **nichts**.
|
||||
2. `rooms.mediaConfirm/:rid/:fileId` veröffentlicht sie als Nachricht.
|
||||
|
||||
Ohne den zweiten Schritt liegt die Datei hochgeladen, aber unsichtbar auf dem Server.
|
||||
Voraussetzung ist damit Rocket.Chat 6.x oder neuer; einen Rückfall auf `rooms.upload` für
|
||||
ältere Instanzen gibt es bewusst nicht, weil er sich hier nicht prüfen ließe.
|
||||
|
||||
Anders als `send_message` steht `RocketChat.send_file` in der Staging-Policy auf
|
||||
**`approve`**: Eine Nachricht formuliert der Agent, eine Datei verlässt den Workspace als
|
||||
Ganzes. Wer das im Alltag als zu hinderlich empfindet, streicht die eine Zeile in
|
||||
`StagingPolicy.DefaultRules` — dann schützt weiterhin die Raum-Allowlist.
|
||||
|
||||
Geprüft gegen die Testinstanz (8 von 8): Markdown aus dem persönlichen Workspace · CSV aus
|
||||
dem geteilten mit abweichendem Dateinamen · Datei per Direktnachricht · Ausbruchsversuch
|
||||
`../` abgewiesen · absoluter Pfad abgewiesen · fehlende Datei · fehlender `localPath` ·
|
||||
nicht freigegebener Raum.
|
||||
|
||||
### 2.9 Eingehende Dateien und Links
|
||||
|
||||
Was hereinkommt, ist genauso wichtig wie das, was hinausgeht — und stand anfangs nicht im
|
||||
Entwurf.
|
||||
|
||||
**Dateien.** Eine Dateisendung trägt den Begleittext in `msg`, die Datei selbst aber
|
||||
daneben in `files[]`. Wer nur `msg` liest, sieht bei einer reinen Dateisendung einen
|
||||
**leeren Beitrag**. Weckmeldung und `read_room` führen Anhänge deshalb eigens auf:
|
||||
|
||||
```
|
||||
[20:06] @richard (messageId: rD68…): schau dir die Zahlen bitte an.
|
||||
Datei: auswertung.csv (text/csv, 77 B) — fileId: 6a821828ea0ad1bcab878f74
|
||||
```
|
||||
|
||||
Mit dieser `fileId` holt `download_file` die Datei in den Workspace. Der Abruf geht gegen
|
||||
`/file-upload/{fileId}/download` mit denselben Kopfzeilen wie die API — ohne sie antwortet
|
||||
der Server mit 403 (`FileUpload_ProtectFiles` ist aktiv).
|
||||
|
||||
Schutzmaßnahmen, weil Name und Inhalt vom Absender bestimmt sind:
|
||||
|
||||
- Der Zielname wird auf den reinen Dateinamen reduziert; Pfadangaben darin verfallen.
|
||||
Nur das Verzeichnis (`personal:` / `shared:`) darf der Agent wählen.
|
||||
- Ausführbare Endungen (`.exe`, `.ps1`, `.bat`, `.jar`, …) werden abgelehnt, sofern nicht
|
||||
`allowDangerousDownloads` gesetzt ist.
|
||||
- Größengrenze `maxDownloadMb` (Standard 25) — geprüft an `Content-Length` **und**
|
||||
während des Schreibens, da die Angabe fehlen darf.
|
||||
- Geschrieben wird über eine `.part`-Nebendatei; bricht der Abruf ab, bleibt keine halbe
|
||||
Datei liegen, die der Agent für vollständig hält.
|
||||
|
||||
Dabei fiel ein Windows-Fehler auf, der leicht zu übersehen ist: `Path.GetFileName` behält
|
||||
`"shared:kopie.csv"` unverändert bei, weil `:` dort kein Pfadtrenner ist, sondern ein
|
||||
**NTFS-Alternativdatenstrom** eingeleitet wird. Die Datei landete als Datenstrom am
|
||||
Verzeichnis statt als eigene Datei. Der Präfix wird jetzt vor der Namensbereinigung
|
||||
abgetrennt, und `WorkspaceFile` weist Doppelpunkte grundsätzlich ab.
|
||||
|
||||
**Links.** Rocket.Chat entpackt Links selbst und legt in `urls[].meta` Titel und
|
||||
Beschreibung der Zielseite ab. Der Agent bekommt das mitgeliefert:
|
||||
|
||||
```
|
||||
Link: https://www.rocket.chat/ — Rocket.Chat | Secure CommsOS™ …
|
||||
```
|
||||
|
||||
Das erspart oft einen eigenen Seitenabruf. **Wichtig:** Diese Vorschau ist Text der
|
||||
verlinkten Seite, also fremdbestimmt — sie steht deshalb wie alles andere innerhalb der
|
||||
`<untrusted_content>`-Rahmung. Ein tatsächlicher Abruf der Seite bleibt Sache des
|
||||
WebFetch-Tools und damit eine bewusste Entscheidung des Agenten, keine Nebenwirkung des
|
||||
Empfangens.
|
||||
|
||||
Geprüft (9 von 9): Datei in der Weckmeldung samt `fileId` · Link mit Titel · Download in
|
||||
den persönlichen und in den geteilten Workspace · Inhalt stimmt · Ausbruch über den
|
||||
Dateinamen entschärft · gefährliche Endung abgelehnt · unbekannte `fileId` · fehlende
|
||||
`fileId`.
|
||||
|
||||
Der Weckpfad ist gegen die Testinstanz durchgespielt (6 von 6 Erwartungen):
|
||||
ruhiger Takt weckt nicht · Nachricht ohne Erwähnung weckt nicht · Erwähnung weckt ·
|
||||
Direktnachricht weckt auch ohne Erwähnung · eigene Nachricht weckt nicht ·
|
||||
private Gruppe weckt.
|
||||
|
||||
Konfiguration je Agent:
|
||||
|
||||
```json
|
||||
"RocketChat": {
|
||||
"baseUrl": "https://chat.example.org",
|
||||
"userId": "aBcD…",
|
||||
"authToken": "…", // von ConfigSecrets geschützt
|
||||
"allowedRooms": ["GENERAL", "finanz-team"],
|
||||
"authToken": "…", // von ConfigSecrets geschützt (Schlüssel "authtoken")
|
||||
"username": "agent-hermes", // für den Erwähnungsfilter
|
||||
"allowedRooms": ["GENERAL", "finanz-team", "richard"],
|
||||
"defaultRoom": "finanz-team",
|
||||
"mentionOnly": true,
|
||||
"maxWakesPerRoomPerHour": 12
|
||||
"agentUsernames": ["agent-hermes", "agent-atlas"],
|
||||
"maxWakesPerRoomPerHour": 12,
|
||||
"maxMessagesPerRoom": 20,
|
||||
"initialLookbackMinutes": 5,
|
||||
"maxUploadMb": 25
|
||||
}
|
||||
```
|
||||
|
||||
**`allowedRooms` gilt auch für Direktnachrichten.** Ein DM-Raum trägt den Namen des
|
||||
Gegenübers — wer per DM erreichbar sein soll, steht dort mit seinem **Benutzernamen**
|
||||
(oben `"richard"`). Ohne Eintrag ignoriert der Agent die Direktnachricht. Eine leere Liste
|
||||
erlaubt alles; das Tool erfindet keine Allowlist.
|
||||
|
||||
Mit `"@benutzername"` als `room` beginnt der Agent auch ein **neues** Gespräch (`im.create`)
|
||||
— nur bei ausdrücklicher `@`-Schreibweise, damit ein vertippter Kanalname nicht
|
||||
stillschweigend zur Direktnachricht wird.
|
||||
|
||||
Zum Verhalten des Abrufs:
|
||||
|
||||
- **Erster Takt je Raum setzt nur die Marke** und weckt nicht — sonst käme beim Einrichten
|
||||
die gesamte Raumgeschichte auf einmal in den Kontext.
|
||||
- Die Marke wandert auf die jüngste **gesehene** Nachricht, auch auf gefilterte. Sonst
|
||||
würde eine ignorierte Agentennachricht bei jedem Takt erneut geprüft.
|
||||
- **Gelesen-Markierung erst nach dem Einsammeln** — bricht der Takt vorher ab, bleibt der
|
||||
Zähler stehen und nichts geht verloren.
|
||||
- Der Schleifenschutz vergleicht gegen `agentUsernames`, nicht gegen ein Server-Flag: Wir
|
||||
wissen selbst am besten, welche Konten unsere Agenten sind.
|
||||
- **Kein Eintrag in der Staging-Policy** — `RocketChat.send_message` bleibt bewusst `auto`
|
||||
(Standard). Der Schutz sitzt an `allowedRooms`, nicht an einer Einzelfreigabe.
|
||||
|
||||
---
|
||||
|
||||
## 3 — Redundanz: was passiert, wenn Rocket.Chat ausfällt
|
||||
|
||||
Reference in New Issue
Block a user