Files
ClawdDotNet/docs/Instanzmanager-Konzept.md
T
RichardandClaude Opus 5 0b6ea73ec1 Entscheidungen aus dem Testbetrieb umgesetzt (#572): #577 #579 #581 #589 #193 #205 #594 #595 #597 #598 #599 #588
- 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>
2026-09-15 11:23:10 +02:00

1035 lines
57 KiB
Markdown
Raw Blame History

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