- Engine: leere Antworten nachfassen, Aufgaben enden mit Ergebniszeile (EmptyResponse/Incomplete), gleiche Fehlschlaege erkennen (RepeatedFailure), Zeitzeile vor Chatnachrichten, Werkzeugergebnisse gerahmt, Vorpruefung vor dem Staging, Belegkosten wie RunUsage - Staging: gescheiterte Freigaben als Failed; AgentEditor-Aenderungen freigabepflichtig, Selbstbearbeitung gesperrt (allowSelfEdit) - Taskboard: Termine beim Anlegen pruefen, naechster/letzter Lauf, pausieren, Sofortlauf, Kostenprognose, Hinweis auf wachsenden Kontext; Jobs-Reiter und /api/jobs entfernt - Release: Instanzmanager samt Weboberflaeche im Paket, setup.json fragt manager.json ab, Token nicht mehr ins systemd-Journal Version 0.1.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1035 lines
57 KiB
Markdown
1035 lines
57 KiB
Markdown
# Instanzmanager — Befund und Konzept
|
||
|
||
Warum mehrere Instanzen heute nicht unabhängig voneinander laufen, und wie es
|
||
stattdessen aussehen soll.
|
||
|
||
Stand: 2026-09-08. Anlass: Beim Instanzwechsel hat sich die Anwendung aufgehängt
|
||
und wurde von Windows beendet. Vorhaben: **#528**, zugehöriger Fehler: **#529**.
|
||
|
||
Die Entscheidungen in Abschnitt 2 sind getroffen. Was danach kommt, folgt aus
|
||
ihnen.
|
||
|
||
---
|
||
|
||
## 1 — Der Befund
|
||
|
||
### 1.1 Was passiert ist
|
||
|
||
Am 2026-09-08 wurde aus der laufenden Anwendung heraus eine zweite Instanz
|
||
angelegt und auf sie gewechselt:
|
||
|
||
| Zeit | Quelle | Eintrag |
|
||
|---|---|---|
|
||
| 17:26:48 | Dateisystem | `Instance-Test2` angelegt |
|
||
| 17:26:50 | `Lifetime.log` | `Application is shutting down...` |
|
||
| 17:26:55 | `Diagnostics.log` | Zwei Ereignisströme beendet, 33 s und 776 s Laufzeit |
|
||
| 17:27:45 | Windows-Ereignisprotokoll | `AppHangB1` — „hat aufgehört mit Windows zu interagieren und wurde geschlossen" |
|
||
|
||
**Die Anwendung wurde nicht beendet, sie wurde erschlagen.** Bemerkenswert ist,
|
||
was dabei *nicht* geschah: Zu diesem Prozess steht **keine einzige
|
||
Protokollzeile** in den Instanzprotokollen — der Schreibpuffer wurde nie geleert.
|
||
Keine Fehlermeldung, kein Bugtracker-Eintrag, kein Watchdog-Alarm. Der Vorfall
|
||
ist ausschließlich rekonstruierbar, weil das Betriebssystem ihn festgehalten hat.
|
||
|
||
**Das ist der Kern:** Meldeweg, Watchdog und Fehler-Stream leben *in* dem
|
||
Prozess, über dessen Zustand sie berichten sollen. Fällt er hart, fällt der
|
||
Meldeweg mit — genau in dem Fall, der gemeldet werden müsste.
|
||
|
||
### 1.2 Was heute geteilt ist, obwohl es getrennt sein müsste
|
||
|
||
| Geteilt | Wirkung | Beleg |
|
||
|---|---|---|
|
||
| **Ein Protokollverzeichnis** | Alle Instanzen schreiben in dieselben Dateien, ohne Kennzeichnung, welche spricht | `Startup.log` enthält abwechselnd Zeilen von `Test` und `Test2` |
|
||
| **Eine `Settings.json`** | Vier Stellen schreiben sie im laufenden Betrieb. Zwei Instanzen: die letzte gewinnt, stillschweigend | `LicenseGate`, `MainWindowViewModel`, `AgentsPageViewModel`, `SettingsPageViewModel` |
|
||
| **Ein Sicherungsverzeichnis** | Ein Ordner für alle Archive. Die Aufbewahrung räumt dort nach dem Präfix `backup_<Instanzname>_` auf — und ein Präfix ist keine Grenze: `backup_Test_2_….zip` fängt mit `backup_Test_` an, `Test` wirft es also weg | `AppSettings.BackupDirectory` |
|
||
| **Ein Sicherungs-Schalter** | Wer die tägliche Sicherung für eine Instanz einschaltet, schaltet sie für alle ein | `AppSettings.AutoBackupEnabled` |
|
||
| **Ein Web-Port als Vorgabe** | Jede Instanz bringt `webServerPort: 7001` mit; die zweite kann nicht binden | `InstanceConfig.WebServerPort` |
|
||
|
||
### 1.3 Der Wechsel kann gar nicht anders enden
|
||
|
||
Der Instanzmanager ist heute ein Fenster *innerhalb* einer laufenden Instanz.
|
||
Wechseln heißt deshalb: Der Prozess fährt sich selbst herunter, startet sich mit
|
||
`--instance` neu und beendet sich. Wer die Instanz wechselt, beendet zwangsläufig
|
||
die, aus der heraus er es tut.
|
||
|
||
---
|
||
|
||
## 2 — Die Entscheidungen
|
||
|
||
Getroffen am 2026-09-08.
|
||
|
||
### E-M1 — Der Weg zwischen Manager und Instanz
|
||
|
||
**Entschieden: HTTP über einen Unix-Domain-Socket je Instanz, unter Windows über
|
||
eine Named Pipe.** Der Socket liegt im Instanzverzeichnis
|
||
(`<Instanz>/manager.sock`), Rechte 0600.
|
||
|
||
Warum nicht TCP: Ports müssten verwaltet, vergeben und gegen Kollisionen
|
||
gesichert werden — genau das Problem, an dem die heutige Vorbelegung `7001`
|
||
scheitert. Ein Socket im Instanzverzeichnis braucht keine Vergabe, und die
|
||
Zugriffsrechte stehen schon am Dateisystem.
|
||
|
||
Warum trotzdem HTTP darüber: Weil der Manager Anfragen an die Instanzen
|
||
**weiterleitet** (E-M5). Bleibt das Protokoll HTTP, ist das Weiterleiten eine
|
||
Weiterleitung und keine Übersetzung — und die bestehenden Endpunkte, DTOs und
|
||
der Ereignisstrom aus #494 gelten unverändert weiter. Kestrel kann auf beidem
|
||
hören, `SocketsHttpHandler` kann auf beidem verbinden.
|
||
|
||
### E-M2 — Die Oberflächen
|
||
|
||
**Entschieden: Jede Instanz behält ihr eigenes Desktop-Fenster, der Manager
|
||
bekommt ein eigenes.** Die Instanzfenster laufen im Hintergrund und werden beim
|
||
Wechsel nach vorn geholt. Ein Wechsel ist damit kein Neustart mehr, sondern ein
|
||
Fensterwechsel.
|
||
|
||
### E-M3 — Lizenz
|
||
|
||
**Entschieden: Die Lizenz liegt im Manager und wird dort geprüft.**
|
||
Instanzprozesse starten nicht ohne aktiven Manager. Eine Lizenz erlaubt beliebig
|
||
viele Instanzen.
|
||
|
||
Folge: `LicenseGate` und `LicenseWatch` wandern aus dem Instanzprozess in den
|
||
Manager. `AppHost.Callbacks.License` entfällt für Instanzen.
|
||
|
||
### E-M4 — Einstellungen
|
||
|
||
**Entschieden: Der Manager kennt nur das Nötigste, alles andere gehört der
|
||
Instanz.** Die Aufteilung steht in Abschnitt 6.
|
||
|
||
### E-M5 — Die Weboberfläche
|
||
|
||
**Entschieden: Eine Adresse, ein Zugang, alle Instanzen.** Der Manager besitzt
|
||
Port, Zugang und Rahmen. Anfragen zu Chat, Agenten, Aufgaben und Freigaben
|
||
reicht er über E-M1 an die betroffene Instanz weiter.
|
||
|
||
**Er leitet weiter, er implementiert nicht.** Keine Fachlogik im Manager — die
|
||
Weiterleitung kennt Kennungen und Pfade, sonst nichts.
|
||
|
||
### E-M6 — Wenn der Manager ausfällt
|
||
|
||
**Entschieden: Laufende Instanzen laufen weiter.** Der neu gestartete Manager
|
||
findet sie über eine Datei im Instanzverzeichnis wieder und übernimmt die
|
||
Aufsicht.
|
||
|
||
Damit wird der Manager nicht zu dem Einzelpunkt, gegen den dieses ganze Vorhaben
|
||
gebaut wird. Die Prüfung „gibt es einen Manager?" findet **beim Start** einer
|
||
Instanz statt, nicht fortlaufend.
|
||
|
||
Ausnahme: Ein **Lizenzwiderruf** ist kein Ausfall, sondern ein Urteil. Erfährt
|
||
der Manager davon, hält er die Instanzen an.
|
||
|
||
---
|
||
|
||
## 3 — Der Schnitt
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Instanzmanager (ein Prozess je Maschine) │
|
||
│ · eigenes Fenster + die eine Weboberfläche │
|
||
│ · Instanzen anlegen, starten, anhalten, entfernen │
|
||
│ · Aufsicht: lebt sie? stürzt sie wiederholt ab? │
|
||
│ · Lizenz · Update · DC-Master-Token → Sub-Token je Instanz │
|
||
│ · KEINE Datenbank. KEINE Fachlogik. KEINE Werkzeuge. │
|
||
└──────┬───────────────────────────────────────────────────────┘
|
||
│ manager.sock (HTTP über UDS / Named Pipe)
|
||
┌───┴────────┬────────────────┬───────────────┐
|
||
▼ ▼ ▼ ▼
|
||
┌────────┐ ┌────────┐ ┌────────┐ Fenster je Instanz
|
||
│Instanz │ │Instanz │ │Instanz │ (laufen im Hinter-
|
||
│ A │ │ B │ │ C │ grund, kommen beim
|
||
│ │ │ │ │ │ Wechsel nach vorn)
|
||
│ Engine · Werkzeuge · Taskboard · Staging · state.db │
|
||
│ eigenes Protokoll · eigenes Sub-Token · eigener Meldeweg │
|
||
│ Telegram · Rocket.Chat · Mail · FTP — ausschliesslich hier │
|
||
└────────┘ └────────┘ └────────┘
|
||
```
|
||
|
||
### 3.1 Der Manager hat keine Datenbank
|
||
|
||
Alles, was er braucht, liegt als JSON im Projektverzeichnis: die Instanzliste,
|
||
seine drei Einstellungen, je Instanz eine kleine Zustandsdatei (läuft sie? seit
|
||
wann? mit welcher Prozesskennung? wie oft zuletzt abgestürzt?).
|
||
|
||
`state.db` einer Instanz fasst er **nicht** an — die Datei gehört genau einem
|
||
Prozess, und prozessübergreifend gilt die Schreibsperre von `SqliteStorage`
|
||
nicht.
|
||
|
||
### 3.2 Werkzeuge laufen ausschließlich in der Instanz
|
||
|
||
Telegram, Rocket.Chat, Mail, FTP, Datenbank — alle Werkzeuge gehören der Instanz
|
||
und nur ihr. Der Manager kennt sie nicht und spricht nie mit ihnen.
|
||
|
||
**Das gilt auch für die Telegram-Anmeldung.** Sie ist ein Rückruf in
|
||
`AppHost.Callbacks`; mit E-M2 beantwortet ihn das **eigene Fenster der Instanz**.
|
||
Über den Manager läuft davon nichts. (Eine frühere Fassung dieses Papiers hat den
|
||
Rückruf als Beleg für Kopflosigkeit angeführt — das war missverständlich: Der
|
||
Rückruf zeigt nur, dass die Anwendungsschicht ihre Umgebung nicht festschreibt.)
|
||
|
||
---
|
||
|
||
## 4 — Was der Manager protokolliert und meldet
|
||
|
||
**Nur, was aus seiner Sicht relevant ist:**
|
||
|
||
| Ereignis | Protokoll | Ans Deploymentcenter |
|
||
|---|---|---|
|
||
| Instanz gestartet | ja | Zustandswechsel |
|
||
| Instanz geordnet beendet | ja | Zustandswechsel |
|
||
| **Instanz abgestürzt** | ja | **Fehlermeldung stellvertretend** |
|
||
| Instanz startet wiederholt neu | ja | Fehlermeldung |
|
||
| Instanz antwortet nicht mehr auf dem Socket | ja | Fehlermeldung |
|
||
| Lizenz ungültig, Update fehlgeschlagen | ja | Fehlermeldung |
|
||
|
||
**Was die Instanz betrifft, bleibt in der Instanz.** Ein fehlgeschlagener
|
||
Werkzeugaufruf, ein abgebrochener Agentenlauf, eine überschrittene Budgetgrenze —
|
||
das protokolliert und meldet die Instanz selbst, mit ihrem eigenen Sub-Token.
|
||
Der Manager sieht davon nichts und soll davon nichts sehen.
|
||
|
||
Der Fall aus 1.1 ist genau der, für den es den Manager gibt: Die Instanz kann
|
||
nichts mehr sagen, also sagt er es an ihrer Stelle — mit Prozesskennung,
|
||
Beendigungsgrund und dem Ausschnitt aus ihrem Protokoll, der noch auf der Platte
|
||
liegt.
|
||
|
||
### 4.1 Jede Instanz meldet weiterhin selbst
|
||
|
||
Zwei Wege, mit Absicht:
|
||
|
||
- Die **Instanz** meldet, solange sie lebt — eigener Fehler-Stream, eigener
|
||
Heartbeat, eigener Watchdog-Monitor. Das Deploymentcenter führt Instanzen
|
||
bereits über `source` + `instance` als eigene Monitore.
|
||
- Der **Manager** meldet, was die Instanz nicht mehr melden kann.
|
||
|
||
Jeder Weg deckt den Ausfall des anderen ab. Ein Manager, der jede Meldung
|
||
durchreicht, wäre der neue Einzelpunkt.
|
||
|
||
---
|
||
|
||
## 5 — Lizenz und Token
|
||
|
||
**Der Manager hält das Master-Token, die Instanzen halten Sub-Token.**
|
||
|
||
Das Verfahren ist gebaut: `TokenProvisioner` tauscht über
|
||
`POST /api/tokens/v1/provision` das Master-Token gegen ein Sub-Token je Instanz,
|
||
mit den Rechten `watchdog:ping` und `bugtracker:report`. Rechte lassen sich dabei
|
||
nur einschränken, nie erweitern; jedes Sub-Token ist einzeln widerrufbar.
|
||
|
||
Zu tun bleibt der Umzug: Heute wird bei der Einrichtung provisioniert
|
||
(`setup.json`, `"source": "provision"`), künftig beim Anlegen einer Instanz durch
|
||
den Manager.
|
||
|
||
**Achtung bei der Projektbindung.** Seit SDK 2.8.1 erben Watchdog-Monitore das
|
||
Projekt ihres Ping-Tokens; ein Monitor mit `project_slug = NULL` erzeugt Befunde
|
||
im Scheinprojekt „default", und `dc_findings(project_slug=…)` liefert für eine
|
||
laufende, meldende Instanz dann **null Befunde**. Die Sub-Token müssen also
|
||
projektgebunden ausgestellt werden — sonst sieht die Aufsicht aus wie „nichts
|
||
auffällig".
|
||
|
||
Wir stehen auf **SDK 2.6.0**, aktuell ist **2.8.2**. **#218 ist damit
|
||
Voraussetzung, nicht Nacharbeit.**
|
||
|
||
---
|
||
|
||
## 6 — Einstellungen: die Aufteilung
|
||
|
||
### Der Manager (`manager.json`)
|
||
|
||
| Feld | Warum es nicht in die Instanz kann |
|
||
|---|---|
|
||
| `webUiPort`, `webUiBinding`, `webUiToken` | Eine Weboberfläche für alle |
|
||
| `licenseKey` | E-M3: die Lizenz gilt der Installation |
|
||
| `deploymentcenterUrl`, `deploymentcenterMasterToken` | Aus ihm entstehen die Sub-Token |
|
||
| `instancesDirectory` | Wo die Instanzen liegen — das muss er wissen, bevor es eine gibt |
|
||
| `updateChannel` | Aktualisiert wird das Programm, nicht die Instanz |
|
||
|
||
Mehr nicht. Farbschema, Teilerstellungen und Protokollstufe sind Werte einer
|
||
Oberfläche und gehören zu der, die sie anzeigt.
|
||
|
||
### Die Instanz (`InstanceSettings.json`)
|
||
|
||
Alles Übrige, und drei Felder ziehen aus den Anwendungseinstellungen dorthin
|
||
um — sie waren nie anwendungsweit, sie sahen nur so aus:
|
||
|
||
| Feld | Heute | Künftig |
|
||
|---|---|---|
|
||
| `logDirectory` | geteilt, alle Instanzen in denselben Dateien | `<Instanz>/Logs`, Vorbelegung |
|
||
| `backupDirectory` + `backupKeepCount` | geteilt, Präfixkollision bei ineinanderliegenden Namen | je Instanz |
|
||
| `autoBackupEnabled` + `autoBackupTime` | geteilt, ein Schalter für alle | je Instanz |
|
||
| `webServerPort` | je Instanz, Vorbelegung 7001, Kollision | **entfällt** — es gibt nur noch den Socket |
|
||
|
||
`autoBackupEnabled` und `autoBackupTime` standen in der ersten Fassung dieser Tabelle
|
||
nicht. Sie sind mitgegangen, weil die Hälfte eines Blocks zu verschieben eine
|
||
Einstellungsseite hinterlässt, auf der zwei von vier Sicherungsfeldern für alle Instanzen
|
||
gelten und zwei nur für eine — und weil ein Schalter, der die Sicherung für alle
|
||
einschaltet, derselbe Fehler ist wie ein Ordner für alle.
|
||
|
||
`deploymentcenterEnvironment` bleibt bei der Instanz: Eine Maschine kann eine
|
||
Produktions- und eine Testinstanz nebeneinander betreiben.
|
||
|
||
---
|
||
|
||
## 7 — Noch zu klären
|
||
|
||
**Sicherungen im Manager — gebaut** (2026-09-11, #530): verschlüsselt mit dem
|
||
Lizenzschlüssel, auf Wunsch per FTP hinaus. Siehe Abschnitt 9.10.
|
||
|
||
**Eine Sache ist daran aber schon entschieden:** Zurückspielen heißt, in ein
|
||
Instanzverzeichnis zu schreiben, und das darf **ausschließlich bei inaktiver
|
||
Instanz** geschehen — sonst schreibt man unter einer offenen `state.db` das
|
||
Dateisystem um. Reihenfolge, wenn es kommt: anhalten, zurückspielen, starten. Und
|
||
nie ohne Rückfrage. Der Manager kann das inzwischen: Er kennt den Zustand jeder
|
||
Instanz über ihren Socket und hält sie geordnet an.
|
||
|
||
**Wie stellt eine Instanz fest, dass ein Manager da ist? — Entschieden**
|
||
(2026-09-10, #535): über eine Unterschrift, die der Manager beim Start mitgibt.
|
||
Siehe Abschnitt 9.9.
|
||
|
||
**Startet der Manager eine abgestürzte Instanz wieder? — Entschieden** (2026-09-08):
|
||
ja, aber nur, wenn die Instanz es verlangt. Der Schalter heißt „Autostart durch
|
||
Manager" und steht in der Instanzdatei; siehe Abschnitt 9.7.
|
||
|
||
**Was ist mit einer Instanz, die verstummt? — Entschieden** (2026-09-10): Ja, der
|
||
Manager darf sie abschießen — aber erst am Ende einer Leiter, und nie ohne
|
||
Nachweis. Siehe Abschnitt 9.8.
|
||
|
||
**Der Aufhänger aus 1.1 ist nicht geklärt** (#529). Er verschwindet mit dem
|
||
Umbau vermutlich von selbst, weil es den Selbst-Neustart nicht mehr gibt — aber
|
||
„vermutlich" ist kein Befund. Zwei unbelegte Verdachtsmomente: `BackupScheduler`
|
||
hat sechs `await` und kein einziges `ConfigureAwait(false)` und fängt sich damit
|
||
den Oberflächenfaden ein; `ShutdownRequested` ist ein `async void`-Behandler.
|
||
|
||
---
|
||
|
||
## 8 — Was aus #494 wird
|
||
|
||
Die Weboberfläche ist nicht umsonst gebaut. Sie zerfällt in zwei Teile, die
|
||
beide bleiben:
|
||
|
||
| Teil | Wohin |
|
||
|---|---|
|
||
| Endpunkte, DTOs, Maskierung, Ereignisstrom | **In die Instanz.** Statt auf einem TCP-Port hört Kestrel auf dem Socket. Der Code bleibt, die Bindung ändert sich |
|
||
| `wwwroot` (Gemini), Anmeldung, Token, Rahmen | **In den Manager.** Dazu die Instanzauswahl und die Verwaltungsansichten |
|
||
| Weiterleitung `/api/instanzen/{id}/…` → Instanz | **Neu im Manager** |
|
||
|
||
Der Ereignisstrom ist die eine Stelle, die wirklich neu gedacht werden muss: N
|
||
Instanzen, ein Strom zum Browser. Der Manager hält je laufender Instanz eine
|
||
Verbindung und führt sie zusammen, mit der Instanzkennung an jedem Ereignis.
|
||
|
||
---
|
||
|
||
## 9 — Reihenfolge
|
||
|
||
| Stufe | Inhalt | Woran man es misst |
|
||
|---|---|---|
|
||
| 1 ✔ | Einstellungen und Protokolle je Instanz trennen (Abschnitt 6) | Zwei Instanzen gleichzeitig, keine schreibt der anderen ins Protokoll |
|
||
| 2 ✔ | Instanzprozess kopflos lauffähig, Kestrel auf dem Socket | Eine Instanz läuft ohne Fenster, der Socket antwortet |
|
||
| 3 ◑ | Manager: anlegen, starten, anhalten, Prozessaufsicht, Lizenz, Sub-Token | Eine Instanz abschießen — der Manager merkt es und meldet es |
|
||
| 4 ◑ | Weboberfläche zieht um, Weiterleitung, ein Ereignisstrom | Eine Adresse, ein Zugang, Wechsel ohne Neustart |
|
||
| 5 ✔ | Instanzfenster als eigener Prozess, Wechsel holt das Fenster nach vorn | Kein Neustart mehr beim Wechseln |
|
||
| 6 | Sicherungen im Manager (falls Abschnitt 7 dafür entscheidet) | Zurückspielen hält die Instanz an und startet sie wieder |
|
||
|
||
**Stufe 1 ist die, die man überspringen möchte und nicht überspringen darf.**
|
||
Solange alle Instanzen in dieselben Protokolldateien schreiben, ist jede
|
||
Fehlersuche im Mehrinstanzbetrieb Ratearbeit — und genau in dieser Lage ist der
|
||
Vorfall aus 1.1 entstanden.
|
||
|
||
### 9.1 Was Stufe 1 gebracht hat
|
||
|
||
Fünf Schlüssel sind aus `AppSettings` in `InstanceSettings.json` gewandert:
|
||
`logDirectory`, `backupDirectory`, `backupKeepCount`, `autoBackupEnabled`,
|
||
`autoBackupTime`. Leer heißt jetzt `<Instanz>/Logs` und `<Instanz>/Backups`; ein
|
||
relativer Wert zählt weiterhin gegen das Instanzverzeichnis (#375).
|
||
|
||
**Keine Übernahme der Altwerte, aber eine Ansage.** Ein anwendungsweiter Pfad, in alle
|
||
Instanzen kopiert, wäre wieder derselbe Ordner für alle. Stehen die Schlüssel noch in
|
||
einer alten `Settings.json`, meldet `SettingsManager.AbandonedKeys` sie, und der Start
|
||
protokolliert einmal, was nicht mehr gilt und wohin diese Instanz stattdessen schreibt.
|
||
Beim nächsten Speichern verschwinden sie aus der Datei.
|
||
|
||
**`webServerPort` bleibt vorerst stehen.** Es entfällt erst mit **Stufe 4**, wenn der
|
||
Manager den Port übernimmt — nicht schon, sobald Kestrel den Socket hat. Eine frühere
|
||
Fassung dieses Absatzes sagte „mit Stufe 2"; das war falsch gerechnet: Solange es keinen
|
||
Manager gibt, der die Weboberfläche trägt, hieße Wegnehmen, sie ersatzlos abzuschalten.
|
||
|
||
---
|
||
|
||
## 10 — Was das für Vorhandenes bedeutet
|
||
|
||
**„Kern als Dienst, Oberfläche als Client"** aus dem Roadmap-Rückstau wird
|
||
überholt: Dort ging es um *einen* Kern und *eine* Oberfläche. Der dort notierte
|
||
Auslöser — „lohnt sich, sobald der erste UI-Absturz eine Nacht Agentenarbeit
|
||
kostet" — ist mit 1.1 eingetreten.
|
||
|
||
**#214 (24/7-Betriebsform)** sagt heute „Kein kopfloser Betrieb: Die Oberfläche
|
||
läuft laut Zielbetrieb immer mit". Das gilt nach E-M2 in anderer Form: Der
|
||
Manager läuft immer mit, die Instanzfenster laufen mit, aber eine Instanz *kann*
|
||
ohne Fenster laufen.
|
||
|
||
**#218 (SDK 2.6.0 → aktuell)** wird Voraussetzung — siehe Abschnitt 5.
|
||
|
||
### 9.2 Was Stufe 2 gebracht hat
|
||
|
||
**Kestrel hört auf dem Socket** — `<Instanz>/manager.sock` unter Linux, unter Windows
|
||
eine Named Pipe, deren Name aus einem Fingerabdruck des Instanzpfades entsteht
|
||
(`InstanceEndpoint`). Zwei Instanzen bekommen zwangsläufig zwei Adressen, ohne dass
|
||
jemand etwas vergeben müsste.
|
||
|
||
**Der Socket ist immer da, auch ohne Weboberfläche.** Er ist nicht Teil von #494, sondern
|
||
die Steuerfläche der Instanz: `webUiEnabled` schaltet den TCP-Zugang, nicht den Socket.
|
||
Was aus `WebUiHost` wurde, heißt deshalb jetzt `InstanceHttpHost` — ein Kestrel, zwei
|
||
Zugänge.
|
||
|
||
**Der Torwächter unterscheidet die Zugänge.** Über TCP gilt Token-Pflicht, über den
|
||
Socket nicht: Dort steht die Grenze am Dateisystem (0600 beziehungsweise
|
||
`CurrentUserOnly`), und wer die Socketdatei öffnen darf, sieht ohnehin in das Verzeichnis,
|
||
in dem alles liegt, was ein Token schützen würde. **Woran die beiden auseinandergehalten
|
||
werden, ist der Punkt, an dem ein Irrtum teuer wäre:** Bei Socket-Verbindungen stehen in
|
||
`HttpContext.Connection` keine IP-Adressen — daraus zu schließen „keine IP, also Socket"
|
||
wäre eine Vermutung an der Stelle, an der ein Torwächter steht, und ließe im Zweifel eine
|
||
TCP-Anfrage ohne Token durch. Der Socket-Zuhörer hängt deshalb jeder Verbindung selbst
|
||
eine Marke an (`LocalTransport`); wo sie fehlt, gilt Token-Pflicht.
|
||
|
||
**Ein belegter Port kostet nur noch die Weboberfläche.** Vorher gab `TryStart` in dem Fall
|
||
`null` zurück und die ganze HTTP-Fläche war weg. Das ist seit dem Socket die falsche
|
||
Rangfolge: Ihn zu verlieren, weil eine zweite Instanz schon läuft, hieße die Instanz
|
||
unerreichbar zu machen — in genau dem Fall, für den es den Manager gibt.
|
||
|
||
**Kopfloser Betrieb** über `--headless --instance <Pfad>`. Die Verzweigung sitzt in
|
||
`Program.Main` vor `BuildAvaloniaApp`, damit auf einem Server ohne Bildschirmsitzung kein
|
||
Fenstersystem angefasst wird. Gefragt wird nichts: Instanz aus dem Argument, Lizenz über
|
||
`NonInteractiveLicensePrompt`, Telegram-Anmeldung aus.
|
||
|
||
Nachgemessen, nicht angenommen: Eine kopflose Instanz auf `Instance-Test` lief ohne
|
||
Fenster, und eine rohe HTTP-Anfrage aus einem fremden Prozess (PowerShell, direkt auf die
|
||
Pipe) bekam `200 OK` auf `/api/info` — ohne Token.
|
||
|
||
**Zwei Lücken, die offen bleiben.** Die Socketdatei-Fälle (liegengebliebene Leiche,
|
||
Aufräumen beim Beenden) sind unter Windows gegenstandslos und werden dort mit Begründung
|
||
übersprungen — der Unix-Domain-Socket selbst ist auf der Entwicklungsmaschine also
|
||
ungeprüft, obwohl er die Zielplattform ist. Und das geordnete Ende auf `SIGTERM` ließ sich
|
||
hier nicht auslösen: Windows kennt das Signal für einen fensterlosen Prozess nicht.
|
||
Beides gehört auf die Linux-Testmaschine.
|
||
|
||
### 9.3 Was Stufe 3 bisher gebracht hat
|
||
|
||
**Die Aufsicht steht, Lizenz und Sub-Token fehlen noch.** Neu ist das Projekt
|
||
`ClawdDotNet.Manager` mit genau **einem** Projektverweis: `ClawdDotNet.Core`. Das ist
|
||
keine Sparsamkeit, sondern die halbe Architektur — `ClawdDotNet.App` schleppt sechzehn
|
||
Werkzeugprojekte hinter sich her, und „keine Werkzeuge im Manager" soll eine Eigenschaft
|
||
des Bauplans sein und keine Absichtserklärung. Damit `InstanceDirectoryManager` von beiden
|
||
Seiten erreichbar ist, ist er nach `Core/Instances` gezogen; dorthin gehört er ohnehin,
|
||
denn er beschreibt, wie eine Instanz auf der Platte liegt.
|
||
|
||
**Der Socket ist die Auskunftsquelle, nicht die Prozessliste.** Das ist die eine
|
||
Entscheidung, an der alles hängt. Die Prozessliste sagt, *dass* etwas da ist — sie sagt
|
||
nicht, ob es noch arbeitet, und genau das war der Zustand aus 1.1: Die Anwendung stand
|
||
einwandfrei in der Prozessliste, bis Windows sie als „reagiert nicht mehr" beendet hat.
|
||
Ein Aufsichtsdurchgang fragt deshalb den Socket und kennt drei Antworten:
|
||
|
||
| Socket | Prozess | Befund |
|
||
|---|---|---|
|
||
| antwortet | — | läuft |
|
||
| schweigt | ist weg | **Absturz**, mit Abschiedscode |
|
||
| schweigt | lebt | **verstummt** — nach genügend Fehlversuchen in Folge |
|
||
|
||
Nicht beim ersten Aussetzer: Eine einzelne verpasste Antwort ist Alltag, und ein Alarm bei
|
||
jedem Aussetzer ist nach einer Woche der Alarm, den niemand mehr liest.
|
||
|
||
**Ein Start gilt erst, wenn die Instanz antwortet.** Zwischen „der Prozess läuft" und „die
|
||
Instanz läuft" liegen Datenbank, Agenten und Kestrel. Wer nur auf `Process.Start` wartet,
|
||
hält eine Instanz für gestartet, die gleich an einer kaputten Konfiguration scheitert.
|
||
|
||
**Angehalten wird durch Bitten, nicht durch Schießen.** Neu auf der Instanzseite:
|
||
`POST /api/instanz/beenden`, **ausschließlich über den Socket** — über TCP gibt es den
|
||
Endpunkt nicht, auch mit gültigem Token nicht, und er antwortet dort mit 404 statt 403.
|
||
Die Lebensdauer einer Instanz gehört dem Manager; eine Browsersitzung soll den Prozess,
|
||
der sie bedient, nicht beenden können. Erst nach Ablauf der Frist wird hart beendet.
|
||
|
||
**Wiederfinden nach einem Managerausfall (E-M6).** Die Zustandsdatei
|
||
`<Instanz>/instance-state.json` ist der *Hinweis*, der Socket ist der *Beweis*: Erst wird
|
||
gefragt, wer da antwortet, und nur wenn dieselbe Instanzkennung zurückkommt, wird die
|
||
Prozesskennung aus der Datei überhaupt angefasst. Andersherum — Prozess suchen, finden,
|
||
für die eigene Instanz halten — führt nach einem Neustart der Maschine geradewegs dazu,
|
||
ein fremdes Programm zu beaufsichtigen und beim Anhalten zu beenden. Startzeit und
|
||
Prozesskennung zusammen entlarven eine wiederverwendete Nummer.
|
||
|
||
**Selbsttätiger Neustart ist bewusst nicht gebaut.** Die Abnahmebedingung lautet „merkt es
|
||
und meldet es", und zum Neustarten trifft dieses Papier keine Entscheidung. Eine Instanz
|
||
in einer Schleife wieder hochzufahren, die jedes Mal abstürzt, wäre die falsche Vorgabe
|
||
für etwas, das niemand entschieden hat. **Das ist eine offene Frage** — siehe Abschnitt 7.
|
||
|
||
**Das Register und die stellvertretende Meldung.** `InstanceRegistry` hält je Instanz eine
|
||
Aufsicht und hat zwei Zusagen, die beide aus E-M6 folgen: Beim Start wird *übernommen*,
|
||
nicht gestartet — ein Manager, der beim Hochkommen alles hochfährt, was er findet, startet
|
||
nach einem Neustart der Maschine auch das, was jemand mit Absicht angehalten hat. Und beim
|
||
Beenden wird nichts beendet: Wer den Manager schließt, will den Manager schließen. Die
|
||
einzige Ausnahme ist `StopAllAsync` — für einen Grund, der alle betrifft, und der heißt
|
||
Lizenzwiderruf.
|
||
|
||
`SupervisionReporter` macht aus einem Befund eine Meldung im Bugtracker, **stellvertretend**:
|
||
mit Prozesskennung, Abschiedscode und dem Ausschnitt aus dem Protokoll der Instanz, der
|
||
noch auf der Platte liegt. Der Idempotenzschlüssel ist stabil je Instanz und Art — stürzt
|
||
dieselbe Instanz wiederholt ab, zählt der Server hoch, statt den Bugtracker mit Zwillingen
|
||
zu füllen; wiederholte Abstürze sind ein Befund und nicht ein Dutzend gleicher.
|
||
|
||
**Ein leerer Protokollauszug ist die aussagekräftigste Zeile der Meldung.** Genau das war
|
||
der Zustand aus 1.1: Der Prozess wurde erschlagen, bevor sein Schreibpuffer geleert war,
|
||
und hinterließ nichts. Die Meldung sagt das ausdrücklich, statt eine leere Stelle zu
|
||
lassen.
|
||
|
||
**`manager.json`** trägt die acht Felder aus Abschnitt 6 und sonst nichts. Lizenzschlüssel,
|
||
Master-Token und Web-Token liegen darin verschlüsselt und zur Laufzeit im Klartext — wie
|
||
in den Anwendungseinstellungen, mit Absicht dieselbe Bauart. Unsinnige Aufsichtswerte
|
||
(Abfragetakt 0, Alarm nach null Fehlversuchen) werden eingefangen: Die Datei lässt sich von
|
||
Hand bearbeiten.
|
||
|
||
**Ein Defekt, den erst die Tests gezeigt haben.** Eine angehaltene Instanz erschien als
|
||
`Instance-Alpha` statt `Alpha` — die Aufsicht kennt den Namen erst, wenn die Instanz einmal
|
||
geantwortet hat, und eine angehaltene tut das nie. Genau diese Ansicht zeigt der Manager
|
||
aber an. Das Register führt den Namen jetzt aus `InstanceSettings.json` mit.
|
||
|
||
**Abnahme, mit echten Prozessen gemessen.** Der Manager hat die Instanz `Test` in 0,7 s
|
||
gestartet und über die Named Pipe identifiziert, die Zustandsdatei geschrieben
|
||
(`pid=54476`, `id=d68af6fd`), dann wurde der Prozess von außen abgeschossen. Der nächste
|
||
Durchgang meldete `Crashed`, Abschiedscode −1, meldenswert, und räumte die Zustandsdatei
|
||
weg. Dazu 20 Tests gegen gesteuerte Prozesse — ein Absturz zum gewünschten Zeitpunkt und
|
||
eine wiederverwendete Prozesskennung sind mit einem echten Programm nicht herzustellen.
|
||
|
||
**Ein Hinweis zur Messumgebung.** Die Abnahme lief über einen eigens gebauten Treiber
|
||
außerhalb der Werkzeugumgebung. Ein Prozess, den ein Agentenwerkzeug hier startet, landet
|
||
in einem Job-Objekt, das seine Kinder sofort mitnimmt; der Instanzprozess starb dadurch,
|
||
bevor er die erste Protokollzeile schreiben konnte. Das ist ein Artefakt der Umgebung und
|
||
keine Eigenschaft des Managers — aber es heißt, dass ein Abnahmetest dieser Art nicht in
|
||
die gewöhnliche Testreihe gehört.
|
||
|
||
### 9.4 Lizenz und Sub-Token im Manager
|
||
|
||
**Ein neues Projekt: `ClawdDotNet.Licensing`.** `LicenseGate`, `LicenseWatch`,
|
||
`LicenseInfo` und `ILicensePrompt` sind aus `ClawdDotNet.App` dorthin gezogen. Der Grund
|
||
ist der Schnitt: Zwei Prozesse brauchen dieselbe Lizenzprüfung, und keiner von beiden darf
|
||
den anderen kennen. In `Core` konnte es nicht landen — dann zöge jedes der sechzehn
|
||
Werkzeugprojekte das Deploymentcenter-SDK mit.
|
||
|
||
Dabei ist die Kopplung an `SettingsManager` verschwunden: `LicenseGate` kennt jetzt nur
|
||
`ILicenseStore` — Serveradresse lesen, Schlüssel lesen und schreiben, speichern. Die
|
||
Anwendung führt ihn in `Settings.json`, der Manager in `manager.json`. Der Torwächter prüft
|
||
Schlüssel; Dateien verwaltet er nicht. Die Anwendungsversion kommt als Parameter statt aus
|
||
einem globalen Zugriff — diese Schicht weiß nicht, welche Anwendung sie benutzt.
|
||
|
||
**`ManagerCore` ist die Klammer:** eigene Einstellungen, Lizenz, Register, Meldeweg. Was
|
||
darüber liegt — ein Fenster, die eine Weboberfläche für alle — setzt darauf auf.
|
||
|
||
- **Ohne gültige Lizenz startet er keine Instanz.** Laufende werden trotzdem in die Liste
|
||
genommen: Man soll sie sehen — und anhalten können.
|
||
- **Ein Widerruf hält alle an.** Der einzige Fall, in dem der Manager das von sich aus
|
||
tut. Ein Serverausfall gehört ausdrücklich nicht dazu; das SDK trennt Urteil und
|
||
gescheiterten Versuch, und nur das Urteil kommt hier an. Sonst nähme ein Netzproblem
|
||
sämtliche Installationen gleichzeitig mit.
|
||
- **Ohne hinterlegte Lizenzprüfung sperrt er nichts.** Sonst gäbe es eine Henne-Ei-Sperre,
|
||
bevor überhaupt jemand einen Schlüssel eintragen kann.
|
||
|
||
**Sub-Token je Instanz.** Der Manager hält das Master-Token und tauscht es beim **Anlegen**
|
||
einer Instanz gegen ein eigenes — `watchdog:ping` und `bugtracker:report`, einzeln
|
||
widerrufbar. Beim Anlegen und nicht beim ersten Start, weil die Instanzdatei dann
|
||
zweifelsfrei niemandem gehört; sie später zu schreiben, während der Instanzprozess läuft,
|
||
hieße zwei Schreiber auf einer Datei. Für Instanzen, die es vor dem Manager schon gab, wird
|
||
es vor dem Start nachgeholt.
|
||
|
||
Danach wird nachgefragt (`whoami`), ob das neue Token an ein Projekt gebunden ist — seit
|
||
SDK 2.8.1 erbt ein Watchdog-Monitor das Projekt seines Ping-Tokens, und ohne Bindung landen
|
||
die Befunde im Scheinprojekt „default". Ein fehlgeschlagener Tokenbezug hält die Instanz
|
||
nicht auf: Monitoring, das nur bei perfekter Rechtelage läuft, ist genau dann still, wenn
|
||
man es braucht.
|
||
|
||
**Was noch fehlt, damit E-M3 vollständig ist:** Die Instanzen prüfen die Lizenz weiterhin
|
||
selbst. Das lässt sich erst abschalten, wenn eine Instanz feststellen kann, ob ein Manager
|
||
da ist (Abschnitt 7) — vorher entstünde eine Fassung ganz ohne Durchsetzung. Bis dahin
|
||
führen beide ihren eigenen Schlüssel.
|
||
|
||
**Ein Testfehler, der zweimal derselbe war.** In den Manager-Tests hatte ich die Antwort des
|
||
Sockets hinterlegt, *bevor* der Manager hochfuhr — damit sagt man „diese Instanz läuft
|
||
bereits", der Manager übernimmt sie (E-M6), der Starter wird nie angefasst, und der Test
|
||
prüft etwas anderes, als er behauptet. Es gibt jetzt eine Hilfsmethode, die genau das
|
||
ausschließt.
|
||
|
||
### 9.5 Was Stufe 4 gebracht hat
|
||
|
||
**Den Managerprozess.** `ClawdDotNet.Instanzmanager` ist eine Konsolenanwendung: Sie prüft
|
||
die Lizenz, übernimmt, was läuft, hängt die Weboberfläche auf und wartet. Sie beendet beim
|
||
Herunterfahren keine Instanz.
|
||
|
||
**Zwei neue Projekte.** `ClawdDotNet.Manager.Web` trägt die Weboberfläche — ohne jeden
|
||
Verweis auf `ClawdDotNet.Web`, denn dort hängt `App` und daran sechzehn Werkzeugprojekte.
|
||
`ClawdDotNet.ManagerApp` ist der Einstiegspunkt. `wwwroot` ist aus der Instanz in den
|
||
Manager gezogen (Abschnitt 8).
|
||
|
||
**Der Schnitt der HTTP-Fläche.** Unter `/api/verwaltung` steht, was den Manager angeht:
|
||
Instanzen auflisten, anlegen, starten, anhalten. **Alles andere unter `/api` geht
|
||
unverändert an eine Instanz** — er leitet weiter, er implementiert nicht. Welche Instanz
|
||
gemeint ist, sagt die Kopfzeile `X-Instanz`; läuft genau eine, ist sie gemeint, und laufen
|
||
mehrere, wird nachgefragt statt geraten. Eine Anfrage an die falsche Instanz ist schlimmer
|
||
als eine abgelehnte.
|
||
|
||
**Der zusammengeführte Ereignisstrom.** Der Manager hält je laufender Instanz eine
|
||
Zuleitung und schreibt an jedes Ereignis, von wem es kommt (`instanz`, `instanzName`).
|
||
Ergänzt statt verpackt: Ein Umschlag wäre einfacher gewesen, hätte aber jede Ansicht der
|
||
Weboberfläche zum Umschreiben gezwungen.
|
||
|
||
**Abnahme, gegen laufende Prozesse gemessen:**
|
||
|
||
| Geprüft | Ergebnis |
|
||
|---|---|
|
||
| `/api/verwaltung/instanzen` ohne Token | 401 |
|
||
| Oberfläche ohne Token | 200 — die leere Hülle, wie vorgesehen |
|
||
| Instanz über den Manager starten | `Running`, PID 18104 |
|
||
| `/api/info` durchgereicht | Antwort **der Instanz** (`instanceId d68af6fd`) |
|
||
| `/api/status` durchgereicht | 503 der Instanz, unverfälscht |
|
||
| `/events/logs` | Zeilen der Instanz, mit `"instanz":"Instance-Test"` daran |
|
||
| Anhalten über den Manager | `Stopped` |
|
||
| **Manager abgeschossen** | Instanz läuft weiter (E-M6) |
|
||
| **Manager neu gestartet** | „1 Instanzen bekannt, 1 laufen bereits" — übernommen |
|
||
|
||
**Drei Defekte, die erst der Lauf gezeigt hat:**
|
||
|
||
1. **Der Manager hing an einer Konsolenabfrage.** `ConsoleLicensePrompt` wartete auf eine
|
||
Eingabe, die niemand geben konnte; die Weboberfläche kam nie hoch, und im Protokoll
|
||
stand nichts davon, weil die Frage auf der Standardausgabe steht. Ein Test auf
|
||
`Console.IsInputRedirected` reicht dafür **nicht**: Er beantwortet „ist die Eingabe
|
||
umgeleitet", nicht „kann jemand antworten" — ein Prozess ganz ohne Konsole, der
|
||
gefährlichste Fall, ist nicht umgeleitet. Der Manager fragt jetzt gar nicht; ein
|
||
Schlüssel gehört in `manager.json`.
|
||
2. **Eine unlesbare `manager.json` wurde stillschweigend durch Vorgaben ersetzt.** Der
|
||
Manager suchte dann im falschen Verzeichnis, hörte auf einem anderen Port und fragte
|
||
nach einer Lizenz, die woanders stand — und nichts sagte, woran es lag. Es kostete eine
|
||
halbe Stunde und einen fehlenden Backslash. `LoadProblem` meldet den Grund jetzt.
|
||
3. **Der Instanzprozess erbte `CLAWD_CONFIG_DIR` vom Manager** und legte sich im
|
||
Konfigurationsverzeichnis des Managers eine eigene `Settings.json` an. Eine
|
||
Umgebungsvariable vererbt sich, ein Argument nicht — deshalb nimmt der Manager sein
|
||
Konfigurationsverzeichnis jetzt über `--config`. Für eine systemd-Unit ist das ohnehin
|
||
der geradere Weg.
|
||
|
||
**Was in Stufe 4 noch fehlt:** Die Weboberfläche selbst kennt noch keine Instanzen — sie
|
||
spricht `/api/…` an und bekommt die eine laufende. Instanzauswahl, `X-Instanz` und die
|
||
Verwaltungsansicht sind Arbeit an `wwwroot` und gehören damit in Geminis Spur. Und
|
||
`webServerPort` steht noch in der Instanz: Ihr TCP-Zugang ist erst überflüssig, wenn die
|
||
Oberfläche im Manager vollständig ist.
|
||
|
||
### 9.6 Was Stufe 5 gebracht hat
|
||
|
||
**Der Wechsel ist kein Neustart mehr.** Vorher hieß „Instanz wechseln": Der laufende
|
||
Prozess fährt sich herunter und startet sich mit einer anderen Instanz neu — wer
|
||
wechselte, beendete zwangsläufig die Instanz, aus der heraus er es tat, und genau dabei
|
||
hat sich die Anwendung am 2026-09-08 aufgehängt. Jetzt laufen alle, und es kommt eine nach
|
||
vorn.
|
||
|
||
**Die Instanz holt ihr Fenster selbst.** Neu ist `POST /api/instanz/fenster`, wieder
|
||
ausschließlich über den Socket. Der Manager kennt kein Fenstersystem und soll keines
|
||
kennen; er bittet, und wer ein Fenster hat, holt es nach vorn. Kopflos läuft die Bitte ins
|
||
Leere, und das ist die richtige Antwort. **Das letzte Wort hat der Fenstermanager** —
|
||
Windows lässt einen Hintergrundprozess den Vordergrund nicht einfach an sich reißen; der
|
||
kurze Ausflug über `Topmost` half im Versuch, unter Linux entscheidet der Fenstermanager.
|
||
Mehr als bitten kann eine Anwendung nicht.
|
||
|
||
**`--managed` schaltet den Wechsel im Instanzfenster ab.** Der Manager gibt es jedem
|
||
Instanzprozess mit; das Fenster gräut seinen Instanz-Schalter aus und erklärt auf Nachfrage,
|
||
wo gewechselt wird. Zwei Schlösser an derselben Tür: Der Knopf ist aus, und der Weg
|
||
dahinter prüft es noch einmal — hinter ihm wartet ein Selbst-Neustart.
|
||
|
||
**`instanceWindows` in `manager.json`**, vorbelegt auf `true`: Jede Instanz behält ihr
|
||
Fenster (E-M2). Auf einer Maschine ohne Bildschirmsitzung gehört `false` hin.
|
||
|
||
**Abnahme, an echten Fenstern gemessen:**
|
||
|
||
| Geprüft | Ergebnis |
|
||
|---|---|
|
||
| Instanz über den Manager gestartet | PID 18128, Fenstertitel „ClawdDotNet" |
|
||
| Befehlszeile | `--managed --instance …` |
|
||
| Fenster von außen minimiert | `IsIconic = True` |
|
||
| `POST /instanzen/Instance-Test/fenster` | „Fenster von 'Test' nach vorn geholt" |
|
||
| Danach | `IsIconic = False`, **im Vordergrund** |
|
||
| Prozess danach | **PID 18128, unverändert** — nichts neu gestartet |
|
||
|
||
Die letzte Zeile ist die Abnahmebedingung: kein Neustart mehr beim Wechseln.
|
||
|
||
### 9.7 Autostart durch Manager
|
||
|
||
Entschieden am 2026-09-08. Der Schalter heißt **`autostartByManager`** und steht in
|
||
`InstanceSettings.json` — bei der Instanz, nicht beim Manager.
|
||
|
||
**Zwei Dinge in einem Schalter, weil es dieselbe Zusage ist:** Der Manager startet diese
|
||
Instanz, wenn er selbst hochkommt, und er startet sie wieder, wenn sie abstürzt. Ohne den
|
||
Schalter rührt er nichts an — ein Manager, der nach einem Neustart der Maschine alles
|
||
hochfährt, was er findet, startet auch das, was jemand mit Absicht angehalten hat.
|
||
Ausgeschaltet vorbelegt.
|
||
|
||
**Der Sicherungsautomat ist der eigentliche Inhalt.** Wieder hochzufahren ist die einfache
|
||
Hälfte; die schwierige ist zu erkennen, wann man es lassen muss. Nach **drei Neustarts in
|
||
zehn Minuten** (`maxRestartsInWindow`, `restartWindowMinutes` in `manager.json`) setzt der
|
||
Manager aus und meldet es. Die Instanz bleibt dann aus, bis jemand sie von Hand startet —
|
||
und dieser Start räumt den Zähler ab, denn wer von Hand startet, hat entweder etwas
|
||
geändert oder weiß, was er tut.
|
||
|
||
Das Zeitfenster wandert: Eine Instanz, die einmal im Monat abstürzt und wieder hochkommt,
|
||
läuft nie in die Grenze — eine, die im Minutentakt fällt, sofort.
|
||
|
||
**Warum der Schalter bei der Instanz steht und nicht beim Manager:** Er ist eine
|
||
Eigenschaft dieser Instanz. Und die Instanzdatei gehört dem Instanzprozess — sie vom
|
||
Manager schreiben zu lassen, während die Instanz läuft, wären zwei Schreiber auf einer
|
||
Datei. Der Manager **liest** den Schalter; geändert wird er auf der Einstellungsseite der
|
||
Instanz.
|
||
|
||
**Ein Defekt, den erst diese Tests gezeigt haben.** `StartAsync` gab „läuft schon" zurück,
|
||
wenn die Aufsicht die Instanz für laufend hielt — ohne nachzusehen. Zwischen zwei
|
||
Aufsichtsdurchgängen liegen aber Sekunden, und in denen kann sie wegbrechen: Wer in dieser
|
||
Lücke „starten" verlangte, bekam einen Erfolg, hinter dem nichts stand, und der nächste
|
||
Durchgang meldete einen Absturz, den niemand mehr erwartete. Jetzt wird gefragt, bevor
|
||
zugestimmt wird.
|
||
|
||
**Und ein Testdouble, das die Wirklichkeit nicht abbildete.** Der falsche Socket antwortete
|
||
unabhängig davon, ob ein Prozess lief. Damit war jeder Testaufbau, der die Antwort vor dem
|
||
Hochfahren hinterlegte, stillschweigend ein anderer Test als beabsichtigt — dem Manager
|
||
sagt eine Antwort „diese Instanz läuft bereits", er übernimmt sie, und der Starter wird nie
|
||
angefasst. Das ist mir dreimal passiert. Der falsche Socket hängt jetzt am falschen
|
||
Starter: Er antwortet nur, wenn wirklich ein Prozess läuft.
|
||
|
||
---
|
||
|
||
## 11 — Übergabe an den Frontend-Agenten
|
||
|
||
Stand 2026-09-09. Der C#-Teil des Instanzmanagers ist fertig; was fehlt, ist die
|
||
Oberfläche. Dieser Abschnitt ist die Übergabe dafür.
|
||
|
||
### 11.1 Der Auftrag, in einem Satz
|
||
|
||
**Bringe der Weboberfläche bei, dass es mehrere Instanzen gibt** — Vorhaben **#531**,
|
||
alles in `src/ClawdDotNet.Manager.Web/wwwroot/`.
|
||
|
||
Heute spricht sie `/api/…` an und bekommt die eine laufende Instanz. Laufen zwei,
|
||
antwortet der Manager mit **HTTP 409** und der Bitte, die Kopfzeile `X-Instanz` zu setzen.
|
||
Dann ist die Oberfläche unbedienbar — und das ist der einzige Punkt, der den
|
||
Mehrinstanzbetrieb im Browser noch aufhält.
|
||
|
||
### 11.2 Die Grenze, und sie ist hart
|
||
|
||
| Nicht anfassen | Warum |
|
||
|---|---|
|
||
| **Jede `.cs`-Datei, überall** | Dahinter stehen Prozessaufsicht, Lizenz, Token und die Weiterleitung. Ein Fehler dort beendet Instanzen oder lässt einen Absturz unbemerkt |
|
||
| `*.csproj`, `ClawdDotNet.slnx`, `deploy/`, `NuGet.Config` | Bauplan und Release-Strecke |
|
||
| `docs/Roadmap.md` zwischen den `<!-- dc:roadmap -->`-Markern | Erzeugte Kopie |
|
||
|
||
**Ein Griff nach C# ist kein Regelverstoß, sondern ein Befund:** Er heißt, dass die API
|
||
etwas nicht hergibt, was sie hergeben müsste. Dann melden (`dc_report`, Projekt
|
||
`clawddotnet`) und an der Oberfläche weiterbauen — nicht selbst in den Kern greifen.
|
||
|
||
Zur Kontrolle vor jeder Abgabe: `git status` darf **ausschließlich** Dateien unter
|
||
`src/ClawdDotNet.Manager.Web/wwwroot/` zeigen.
|
||
|
||
### 11.3 Was sich seit dem WebUI-Leitfaden geändert hat
|
||
|
||
Der [WebUI-Leitfaden](WebUI-Leitfaden.md) gilt weiter für Stil, Sicherheit und Aufbau der
|
||
Oberfläche. Vier Dinge darin sind überholt:
|
||
|
||
| Damals | Heute |
|
||
|---|---|
|
||
| `wwwroot` liegt in `src/ClawdDotNet.Web/` | **`src/ClawdDotNet.Manager.Web/`** |
|
||
| Die Oberfläche läuft im Prozess der Instanz | Sie läuft im **Instanzmanager**, einem eigenen Prozess |
|
||
| `http://127.0.0.1:7001`, Token in `InstanceSettings.json` | **`webUiPort` und `webUiToken` in `manager.json`** (Vorgabe 7000) |
|
||
| Eine Instanz, ein `/api` | **N Instanzen**, der Manager leitet weiter |
|
||
|
||
Der Rest des Leitfadens — kein `innerHTML`, kein Paketmanager, Token nur im
|
||
`sessionStorage`, Farben aus `app.css` — gilt unverändert.
|
||
|
||
### 11.4 Die API, gegen die du baust
|
||
|
||
**Was den Manager selbst angeht** (wird *nicht* weitergeleitet):
|
||
|
||
| Aufruf | Was er tut |
|
||
|---|---|
|
||
| `GET /api/verwaltung/instanzen` | Liste: `{ id, name, zustand, prozess, autostart }` |
|
||
| `POST /api/verwaltung/instanzen` `{ "name": "…" }` | Legt eine an |
|
||
| `POST /api/verwaltung/instanzen/{id}/start` | Startet sie |
|
||
| `POST /api/verwaltung/instanzen/{id}/stop` | Hält sie an |
|
||
| `POST /api/verwaltung/instanzen/{id}/fenster` | Holt ihr Desktop-Fenster nach vorn |
|
||
|
||
`id` ist der Ordnername (`Instance-Test`). `zustand` ist einer von `Unknown`, `Stopped`,
|
||
`Running`, `Silent`, `Crashed`.
|
||
|
||
**Alles andere unter `/api`** geht unverändert an eine Instanz — `/api/info`,
|
||
`/api/agents`, `/api/chat`, `/api/jobs`, `/api/tasks`, `/api/staging`, `/api/usage`,
|
||
`/api/logs/modules`. Die Übertragungstypen sind dieselben wie bisher; hier ändert sich
|
||
nichts außer der Adressierung.
|
||
|
||
**Welche Instanz gemeint ist**, sagt die Kopfzeile `X-Instanz` mit der `id`. Fehlt sie:
|
||
Läuft genau eine, ist sie gemeint. Laufen mehrere, kommt **409** mit einem erklärenden
|
||
Text — dann muss die Oberfläche eine wählen lassen. Läuft keine, ebenfalls 409.
|
||
|
||
**Der Ereignisstrom** (`/events`, `/events/logs`) führt alle laufenden Instanzen zusammen.
|
||
Jedes Ereignis hat **zusätzlich** `instanz` (die `id`) und `instanzName`. Die bisherigen
|
||
Felder bleiben unverändert — vorhandene Ansichten funktionieren weiter, sie wissen nur
|
||
noch nicht, von wem etwas kommt.
|
||
|
||
**Antwortcodes, die eine eigene Behandlung verdienen:**
|
||
|
||
| Code | Bedeutung |
|
||
|---|---|
|
||
| 401 | Token fehlt oder taugt nicht → zurück zur Anmeldung (gibt es schon) |
|
||
| **402** | Ohne gültige Lizenz startet keine Instanz |
|
||
| **409** | Unklar, welche Instanz gemeint ist — oder eine Aktion passt nicht zum Zustand |
|
||
| **502** | Die Instanz antwortet nicht. **Kein Fehler des Managers** — sag das auch so |
|
||
|
||
### 11.5 Reihenfolge
|
||
|
||
1. **`X-Instanz` mitschicken.** In `modules/api.js` gibt es genau eine Stelle, an der
|
||
`fetch` aufgerufen wird — dort die Kopfzeile aus einer gewählten Instanz setzen. Beim
|
||
Ereignisstrom geht das nicht (`EventSource` kann keine Kopfzeilen); dort ist es auch
|
||
nicht nötig, der Manager führt ohnehin alle zusammen.
|
||
2. **Instanzauswahl im Rahmen.** Eine Liste aus `/api/verwaltung/instanzen`, die gewählte
|
||
Instanz im `sessionStorage` neben dem Token. Zustand sichtbar machen — `Silent` und
|
||
`Crashed` sind keine Nebensache, sondern der Grund, warum es den Manager gibt.
|
||
3. **Verwaltungsansicht.** Anlegen, starten, anhalten, Fenster nach vorn. Die Liste nach
|
||
jeder Aktion neu holen; die Zustände ändern sich im Hintergrund.
|
||
4. **Herkunft der Ereignisse zeigen.** `instanzName` an Chat-, Schritt- und
|
||
Protokollzeilen, damit in einem Fenster mit drei Instanzen erkennbar ist, wer spricht.
|
||
|
||
### 11.6 Zwei Fallen
|
||
|
||
**Der Zustand ändert sich, ohne dass jemand klickt.** Der Manager sieht alle paar Sekunden
|
||
nach; eine Instanz kann abstürzen oder verstummen, während die Liste offen steht. Eine
|
||
Ansicht, die nur beim Öffnen lädt, zeigt dann etwas Falsches — und zwar genau in dem
|
||
Moment, für den es diese Ansicht gibt.
|
||
|
||
**Der Manager beendet beim Schließen keine Instanz.** Wer die Weboberfläche zumacht oder
|
||
den Manager beendet, beendet nichts. Was läuft, läuft weiter, und der nächste Manager
|
||
findet es wieder. Eine Oberfläche, die „Manager beenden" mit „alles beenden" beschriftet,
|
||
sagt die Unwahrheit.
|
||
|
||
### 11.7 Wie du es ausprobierst
|
||
|
||
```bash
|
||
dotnet build
|
||
```
|
||
|
||
`manager.json` liegt unter `%APPDATA%\ClawdDotNet\` (Windows) beziehungsweise
|
||
`~/.config/clawddotnet/` (Linux, klein geschrieben wie in `AppPaths`); ein anderer Ort geht
|
||
über `--config <Verzeichnis>`. Nötig sind
|
||
mindestens `webUiToken` (≥ 20 Zeichen) und `instanceExecutable`, wenn Manager und Instanz
|
||
nicht im selben Ausgabeordner liegen.
|
||
|
||
```bash
|
||
dotnet run --project src/ClawdDotNet.ManagerApp
|
||
```
|
||
|
||
Der Manager schreibt seine Adresse auf die Standardausgabe und protokolliert nach
|
||
`<Datenverzeichnis>/Logs/Manager/`. Zum Prüfen mit zwei Instanzen: eine zweite über
|
||
`POST /api/verwaltung/instanzen` anlegen, beide starten — dann muss die Oberfläche eine
|
||
wählen lassen.
|
||
|
||
### 11.8 Was ausdrücklich liegen bleibt
|
||
|
||
Diese drei Punkte sind **C#** und warten auf den Kern-Agenten. Nicht anfangen:
|
||
|
||
1. **`webServerPort` aus der Instanz entfernen.** Erst sinnvoll, wenn 11.5 steht.
|
||
2. **Lizenzprüfung aus der Instanz nehmen** (E-M3 vollständig). Braucht vorher einen
|
||
Handschlag, an dem eine Instanz erkennt, ob ein Manager da ist.
|
||
3. **Verstummte Instanzen neu starten?** Nicht entschieden — siehe Abschnitt 7.
|
||
|
||
### 9.8 Geordnetes Ende und Puls
|
||
|
||
Entschieden am 2026-09-10, aus zwei Befunden derselben Sitzung.
|
||
|
||
**Der erste: Jedes Beenden war ein Abschneiden.** `TaskScanner.DisposeAsync` bricht den
|
||
laufenden Takt über sein Token ab — mitten in `ProcessAsync`, also *nachdem* eine Aufgabe
|
||
beansprucht wurde und *bevor* `CompleteClaimAsync` sie wieder freigibt. Sie steht danach
|
||
auf `running` und bleibt dort, bis das Lease abläuft. Ein Agentenlauf mitten im
|
||
Werkzeugaufruf verlor seine Antwort ganz.
|
||
|
||
**Der zweite, schlimmere: Der Manager konnte eine Instanz mit Fenster überhaupt nicht
|
||
geordnet beenden.** `Control.ShutdownRequested` war nur kopflos verdrahtet
|
||
(`HeadlessRun`), nicht in der Avalonia-Anwendung — und ein Fenster ist die Vorgabe
|
||
(`InstanceWindows`). Die Bitte über den Socket wurde angenommen, mit 202 beantwortet, und
|
||
niemand hörte zu. Der Manager wartete seine Frist ab und schoss den Prozess dann **jedes
|
||
Mal** ab. Das geordnete Ende gab es auf dem Papier.
|
||
|
||
#### Erst die Tür, dann warten
|
||
|
||
`ShutdownCoordinator` (in `Core/Instances`) schließt zuerst alle Arbeitsquellen und wartet
|
||
danach, bis nichts mehr offen ist. Die Reihenfolge ist der ganze Trick: Andersherum
|
||
beansprucht der Scanner im nächsten Takt die nächste Aufgabe, und die Zahl der offenen
|
||
Arbeiten geht nie auf null.
|
||
|
||
Zwei Quellen sind angemeldet — Agentenläufe und Aufgaben. Beide können genau zweierlei:
|
||
keine neue Arbeit mehr annehmen und sagen, wie viel noch läuft. Was daraus folgt,
|
||
entscheidet der Koordinator. Die Frist (zwei Minuten) ist eine Zusage, keine Hoffnung:
|
||
Danach endet die Instanz, ob fertig oder nicht — sonst hinge sie an einem Agentenlauf, der
|
||
auf ein Modell wartet, das nicht antwortet. Was dann noch offen war, steht im Protokoll.
|
||
|
||
Beide Wege dorthin gibt es wirklich: Der Manager bittet über den Socket, und gleichzeitig
|
||
kann jemand am Fenster auf das Kreuz klicken. Ein zweiter Aufruf wartet deshalb auf
|
||
denselben Vorgang, statt einen neuen anzustoßen.
|
||
|
||
#### Der Puls
|
||
|
||
Der Manager fragte bisher `/api/info` und wusste danach nur, dass überhaupt jemand
|
||
geantwortet hat. Das reicht, um „läuft" von „tot" zu unterscheiden — aber nicht für die
|
||
beiden Fälle, die gleich aussehen und gegensätzlich zu behandeln sind: **eine Instanz, die
|
||
hängt, und eine, die gerade aufräumt.** Die erste gehört nach einer Frist abgeschossen,
|
||
die zweite in Ruhe gelassen.
|
||
|
||
`GET /api/instanz/puls` liegt hinter dem Socket-Gate, gehört also dem Manager allein, und
|
||
nennt zusätzlich Phase und offene Arbeiten. Bewusst weiter **gefragt und nicht gemeldet**:
|
||
Ein Puls, den die Instanz von sich aus schickt, fehlt genau dann, wenn sie hängt — und
|
||
dann kann der Manager „hängt" nicht von „hat es vergessen" unterscheiden. Wer fragt, weiß,
|
||
wann er keine Antwort bekommen hat.
|
||
|
||
#### Die Leiter
|
||
|
||
| Stufe | Was passiert |
|
||
|---|---|
|
||
| 3 Durchgänge ohne Antwort | `Unresponsive`, gemeldet. Vorher nicht — ein Agentenlauf, der die Maschine auslastet, ist kein Vorfall |
|
||
| sofort danach | Einmal über den Socket um ein Ende bitten. **Ein Versuch, mehr nicht** — wer eben auf demselben Socket geschwiegen hat, hört wahrscheinlich auch das nicht. Es kann ein einzelner hängender Endpunkt sein und nicht der ganze Prozess; gewartet wird auf die Uhr, nicht auf die Antwort |
|
||
| nach 30 s (`silentKillGraceSeconds`) | Der Prozess wird beendet — und der Vollzug **abgewartet**. `Kill()` ist eine Bitte an das Betriebssystem, keine vollzogene Tatsache |
|
||
| danach | Gemeldet als **Absturz**, nicht als geordnetes Ende. Gewollt hat es niemand — und nur so greift der Autostart |
|
||
| vor jedem Start | `EnsureDeadAsync`: Antwortet noch jemand auf dem Socket? Lebt das Prozesshandle noch? Solange ja, wird **nicht** gestartet |
|
||
|
||
Antwortet die Instanz zwischendurch wieder, war es ein Aussetzer, und niemand wird
|
||
abgeschossen (`Recovered`).
|
||
|
||
#### Der Fehlschluss, der hier begraben liegt
|
||
|
||
`StopAsync` brach seine Warteschleife ab, sobald der Socket nicht mehr antwortete, meldete
|
||
„beendet" und gab `true` zurück — **ohne den Prozess anzufassen.** Bei einer verstummten
|
||
Instanz ist der Socket aber von vornherein still: Der Aufruf meldete sofort Erfolg, der
|
||
Prozess lief weiter, und der Neustart setzte einen zweiten auf dieselbe `state.db`.
|
||
|
||
Schweigen ist nur dann ein Todesnachweis, wenn es keinen anderen Zeugen gibt. Hält der
|
||
Manager ein Prozesshandle und sagt das „lebt noch", dann lebt der Prozess — er antwortet
|
||
bloß nicht mehr. Nur bei einer wiederaufgenommenen Instanz ohne Handle muss das Schweigen
|
||
reichen, weil mehr nicht zu haben ist.
|
||
|
||
#### Und die Fristen greifen ineinander
|
||
|
||
Die Instanz gibt sich zwei Minuten zum Auslaufen, der Manager wartet bis zu fünf
|
||
(`maxDrainWaitSeconds`), solange sie **meldet**, dass sie aufräumt. Wäre seine Frist
|
||
kürzer, schösse er mitten in das Aufräumen hinein, um das er gerade gebeten hat. Und
|
||
trotzdem endlich: Eine Instanz, die dauerhaft „ich räume noch auf" sagt, hängt genauso wie
|
||
eine, die schweigt.
|
||
|
||
### 9.9 Der Handschlag: woran eine Instanz einen Manager erkennt
|
||
|
||
Behoben am 2026-09-10 (#535).
|
||
|
||
**Das Loch.** Mit E-M3 wanderte die Lizenzprüfung in den Manager: Eine geführte Instanz
|
||
prüft nicht mehr selbst, weil der Manager es zentral getan hat. Woran sie „geführt"
|
||
erkannte, war der Schalter `--managed` — und ein Schalter ist keine Erkennung.
|
||
`ClawdDotNet --managed` startete eine Instanz ganz ohne Lizenzprüfung. Vor E-M3 gab es die
|
||
Lücke nicht, weil jede Instanz selbst geprüft hat; sie ist mit der Zentralisierung
|
||
entstanden.
|
||
|
||
**Die Lösung.** `ManagerHandshake` in `Core/Instances`. Der Manager unterschreibt
|
||
Instanzverzeichnis und Ablaufzeitpunkt mit HMAC-SHA256; die Instanz rechnet dieselbe
|
||
Unterschrift nach. An diese Stelle kommt der Manager nur, wenn seine Lizenzprüfung
|
||
bestanden hat — `ManagerCore.StartInstanceAsync` weist vorher mit
|
||
`StartRefusal.NoLicense` ab. **Dass die Unterschrift existiert, ist der Beweis.**
|
||
|
||
**Vier Eigenschaften, und jede hat einen Grund:**
|
||
|
||
| | Warum |
|
||
|---|---|
|
||
| **Gebunden an das Verzeichnis** — und geprüft wird gegen das, in dem die Instanz *tatsächlich* läuft | Sonst unterschriebe man das Argument statt der Sache: Eine Unterschrift für Alpha ließe Beta starten, indem man beim Prüfen Alpha vorzeigt |
|
||
| **Fünf Minuten gültig**, Ablauf geht in die Unterschrift ein | Sonst höbe jemand eine Unterschrift auf und startete damit nächste Woche eine Instanz ohne Lizenz. Am Ablauf zu drehen bricht die Unterschrift |
|
||
| **Über die Umgebung, nicht die Befehlszeile** | Eine Befehlszeile steht in jeder Prozessliste — die Unterschrift wäre abzulesen und abzuschreiben |
|
||
| **Ungültig heißt nicht geführt**, nicht Abbruch | Dann prüft die Instanz ihre Lizenz eben selbst — der Weg von vor E-M3. Kein neuer Fehlerfall, sondern der alte. Wer eine Lizenz hat, merkt nichts |
|
||
|
||
**Was das aushält und was nicht — ausdrücklich.** Der Schlüssel steht in der Quelldatei und
|
||
liegt damit in jeder ausgelieferten Programmdatei; wer ihn sucht, findet ihn. Das ist die
|
||
bewusste Abwägung und keine Nachlässigkeit: Es geht nicht darum, jemanden abzuwehren, der
|
||
Werkzeuge und Willen mitbringt, sondern darum, dass der **gewöhnliche** Weg um die Lizenz
|
||
herum verschlossen ist. Ein Schalter, den man in der Hilfe liest, ist etwas anderes als ein
|
||
HMAC, den man nachbauen muss.
|
||
|
||
**Abgenommen an echten Prozessen**, nicht nur im Test — und die Unterschrift für den
|
||
Positivfall wurde unabhängig in Python gerechnet, damit nicht der C#-Code sich selbst
|
||
bestätigt:
|
||
|
||
| Lauf | Ergebnis |
|
||
|---|---|
|
||
| `--managed`, keine Unterschrift | „Lizenz erforderlich" — kein Start |
|
||
| `--managed` + gültige Unterschrift | Instanz kommt hoch, meldet ihren Named Pipe |
|
||
| Unterschrift für ein anderes Verzeichnis | „Lizenz erforderlich" |
|
||
| Gültige Unterschrift, Ablauf hochgedreht | „Lizenz erforderlich" |
|
||
|
||
### 9.10 Sicherungen: verschlüsselt mit dem Lizenzschlüssel
|
||
|
||
Entschieden am 2026-09-11, als bewusste Vereinfachung gegenüber dem Schlüsselbund aus #190.
|
||
|
||
**Die Vorgabe.** Eine Sicherung wird mit dem Lizenzschlüssel des Managers und einem festen
|
||
Salt aus dem Code verschlüsselt. Beim Zurückspielen versucht der Manager den eigenen
|
||
Schlüssel; passt er nicht, fragt ein Fenster nach dem Lizenzschlüssel der Quellinstallation.
|
||
Die ganze Sicherung liegt als ein verschlüsseltes Archiv vor, auf Wunsch zusätzlich per FTP
|
||
hinaus.
|
||
|
||
**Warum das trägt.** Der Lizenzschlüssel ist der eine Wert, den jede Installation ohnehin hat
|
||
und den ihr Betreiber ohnehin kennt — und er liegt im Manager, nicht neben den Archiven.
|
||
Damit fällt der Grund weg, aus dem die tägliche Sicherung bisher **keine** Zugangsdaten
|
||
enthielt: Eine Passphrase hätte gespeichert werden müssen, und neben den Sicherungen
|
||
abgelegt wäre sie wirkungslos gewesen. Jetzt reisen die Zugangsdaten mit, und eine Sicherung
|
||
übersteht den Maschinenwechsel ohne Passphrase.
|
||
|
||
**Zwei Abweichungen von der Vorgabe, beide erzwungen:**
|
||
|
||
| Vorgabe | Umgesetzt | Warum |
|
||
|---|---|---|
|
||
| RAR | nicht möglich | RAR zu schreiben braucht das lizenzpflichtige Programm des Herstellers |
|
||
| verschlüsseltes ZIP | eigener Behälter `.clawdbak` | .NET kann kein ZIP mit Kennwort schreiben, und das verbreitete ZipCrypto ist gebrochen. Weil der Schlüssel ohnehin abgeleitet wird, ließe sich ein solches ZIP auch mit 7-Zip nicht öffnen — die Verträglichkeit brächte nichts |
|
||
|
||
Der Behälter: Kopf mit Kennung, Fassung und einer **Schlüsselprobe**; das Manifest als eigener
|
||
Abschnitt; das ZIP in Stücken zu 1 MiB, jedes mit AES-256-GCM und eigener Nonce, das letzte
|
||
mit Endmarke. Die Stückelung, weil AES-GCM in einem Zug alles im Speicher bräuchte. Das
|
||
Manifest vorn, damit die Übersicht nicht jedes Archiv ganz entschlüsseln muss. Die Endmarke,
|
||
damit ein abgeschnittenes Archiv auffällt. Und die Schlüsselprobe, damit „fremder Schlüssel"
|
||
von „beschädigt" zu unterscheiden ist, **bevor** etwas angehalten wird.
|
||
|
||
**Was das aushält und was nicht — ausdrücklich.** Wer Lizenzschlüssel und Archiv hat, hat
|
||
alle Zugangsdaten darin. Der Schlüssel ist dem Hersteller bekannt, und er gilt für alle
|
||
Instanzen einer Installation. Das ist die Abwägung, nicht ein Versehen: Ein Archiv auf einem
|
||
FTP-Server oder einem USB-Stick soll niemandem nützen, der es nur findet — aufhalten soll es
|
||
nicht jemanden, der Schlüssel und Absicht mitbringt.
|
||
|
||
**Die Reihenfolge beim Zurückspielen.** Erst die Schlüsselprobe, dann das Anhalten. Wer
|
||
anhält und danach merkt, dass der Schlüssel nicht passt, hat eine laufende Instanz für nichts
|
||
gestoppt. Passt er nicht, antwortet der Endpunkt mit **422** und dem Code `fremde_lizenz`,
|
||
und die Oberfläche öffnet das Fenster für den Schlüssel der Quelle. Groß- und Kleinschreibung
|
||
und Leerzeichen spielen dabei keine Rolle — dort tippt ein Mensch einen fremden Schlüssel ab.
|
||
|
||
**Geführte Instanzen sichern sich nicht mehr selbst.** Nur der Manager hat den
|
||
Lizenzschlüssel. Liefen beide, lägen jeden Tag zwei Archive im Ordner, eines davon
|
||
unverschlüsselt und ohne Zugangsdaten. Die Einstellungen — Schalter, Uhrzeit, Aufbewahrung,
|
||
Zielordner — bleiben in der `InstanceSettings.json`; der Manager liest sie. Einmal am Tag
|
||
heißt dabei auch über einen Neustart des Managers hinweg: Eine Datei von heute, die nach der
|
||
eingestellten Uhrzeit entstanden ist, gilt als erledigt.
|
||
|
||
**Ohne Lizenzschlüssel im Manager wird nichts gesichert** — nicht ersatzweise unverschlüsselt.
|
||
Das steht beim Start als Warnung im Protokoll.
|
||
|
||
**FTP.** Hochgeladen wird unter einem vorläufigen Namen und erst danach umbenannt: Eine halbe
|
||
Datei unter richtigem Namen würde beim Aufräumen als gültige Sicherung mitgezählt, während
|
||
eine vollständige dafür gelöscht wird. Auf dem Server wird je Instanz aufgeräumt, sonst füllt
|
||
ein automatischer Upload ihn, bis ausgerechnet die jüngste Sicherung scheitert. TLS wird
|
||
genommen, wo der Server es anbietet, und lässt sich mit `requireTls` erzwingen; Zertifikate
|
||
werden nicht geprüft, weil eigene FTP-Server meist selbstsignierte haben. Ein gescheiterter
|
||
Upload lässt die lokale Sicherung stehen und steht in der Meldung. Das FTP-Kennwort liegt
|
||
verschlüsselt in `manager.json` und geht nie an die Oberfläche zurück.
|
||
|
||
**Ein Präfix ist keine Grenze.** Welche Datei zu welcher Instanz gehört, entscheidet ein
|
||
Zeitstempel unmittelbar nach dem Namen, nicht der Anfang des Namens. Sonst zählte `Test` die
|
||
Archive von `Test_2` mit und würfe beim Aufräumen fremde weg (siehe 1.2).
|
||
|
||
**Und der Schlüsselbund aus #190?** Er bleibt, verliert aber seine Rolle für Sicherungen:
|
||
Dafür ist jetzt der Lizenzschlüssel da. Das Wiederherstellungsfach ist nur noch für den Fall
|
||
nötig, dass eine laufende Installation ihr Maschinenfach verliert, ohne dass eine Sicherung
|
||
im Spiel ist. Die Naht für #180 bleibt davon unberührt.
|
||
|
||
**Nicht über die Oberfläche abgenommen.** Behälter, Sicherungsdienst und der Rundlauf über
|
||
einen Maschinenwechsel sind mit Tests belegt; der Weg durch die Weboberfläche — das Fenster
|
||
für den fremden Schlüssel, der FTP-Upload gegen einen echten Server — noch nicht. Beides steht
|
||
in #537.
|