Eine Roadmap statt neun verteilter Listen
Der Stand lag ueber eine Bestandsaufnahme, drei Konzeptpapiere, vier Umsetzungsplaene und zwei Deploymentcenter-Dokumente verteilt - jedes mit eigener Reihenfolge, teils widersprueglich. Alle offenen Punkte daraus sind in docs/Roadmap.md zusammengefuehrt. Aufbau der neuen Roadmap - Statusvokabular: erledigt / beschlossen-offen / Entscheidung noetig / zurueckgestellt / Idee. Damit steht das Geparkte sichtbar drin, statt in einem Konzeptpapier zu verschwinden. - Herkunft-Spalte traegt das alte Kuerzel (S4, K2, B9, T4, F-A1, C1, DC1), damit die archivierten Papiere auffindbar bleiben, ohne sie zu lesen. - Abschnitt 1 Reihenfolge, 2 offene Entscheidungen, 3 die Vorhaben nach Bereich, 4 Ideenspeicher, 5 Chronik des Erledigten, 6 Modell-Einstufung, 7 Herkunftskarte. Was dabei sichtbar wurde - Neun Entscheidungen blockieren Arbeit, ohne dass sie Aufwand kosten - allen voran Matrix oder Rocket.Chat. Sie stehen jetzt gesammelt in Abschnitt 2 statt verstreut in den Diskussionsteilen der Konzepte. - B8 (keine Tiefenbegrenzung bei AgentComm) ist unveraendert offen. Das ist keine Theorie: A haelt sein Gate, waehrend es auf B wartet - ruft B nun A, warten beide bis zum Timeout. Der Testfall A13 dafuer fehlt bis heute. - Die vier Umsetzungsplaene vom 2026-08-05 sind alle unumgesetzt und waren in keiner Roadmap verzeichnet. Archiv Sechs Dokumente ziehen nach docs/archiv/: Bestandsaufnahme, Konzepte Backup/Finanz/Analyse, Linux-Portierung-Analyse, Lizenz-HardwareId-v2 (gegenstandslos - LicenseLabrador ist ersetzt), Deploymentcenter-Review und der 2.4-Integrationsplan. Sie bleiben als Begruendung lesbar, werden aber nicht mehr fortgeschrieben; das README ordnet jedes einzeln ein und warnt, dass ihre Quelltext-Verweise ins Leere gehen koennen. Bauplan bleibt Bauplan Taskboard, Audit, Staging, Memory, Agentenkommunikation, RocketChat und Deploymentcenter-Integration bleiben in docs/ - sie sind die Detailvorgabe fuer die Umsetzung, nicht Vorhabenlisten. Jedes bekommt oben eine Zeile, die seine Rolle und den Umsetzungsstand nennt und auf die Roadmap zeigt. Dasselbe fuer die vier Umsetzungsplaene: die Reihenfolge gilt in der Roadmap, nicht im Plan. Nebenbei repariert: Taskboard-Konzept und Teststrategie verwiesen auf AgentScheduler und ToolJobScheduler, die seit der Scanner-Konsolidierung geloescht sind. Alle Dokument-Verweise in docs/ sind geprueft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
# ClawdDotNet — Bestandsaufnahme (Juli 2026)
|
||||
|
||||
Vollständiges Review von Core-Engine, Security-Layer, Tools, Scheduling und UI.
|
||||
Stand: Commit `92e50d3`.
|
||||
|
||||
> **Dies ist ein datierter Befund, keine Todoliste.** Er wird nicht fortgeschrieben.
|
||||
> Was davon offen ist, steht in der [Roadmap](../Roadmap.md) — dort auch, was seither
|
||||
> erledigt wurde. Die Abschnitte zur Oberfläche beziehen sich auf die Windows-Forms-
|
||||
> Fassung, die es seit dem 2026-08-23 nicht mehr gibt.
|
||||
|
||||
**Kurzfassung:** Die Architektur ist gut — die Trennung Core/Tools/UI, das `IAgentTool`-Interface,
|
||||
der Tool-Job-Mechanismus und das Instanz-Konzept tragen. Die Probleme liegen fast alle in
|
||||
drei Bereichen: (1) Sicherheitsprüfungen, die als String-Vergleiche implementiert sind und
|
||||
deshalb umgehbar sind, (2) fehlende Nebenläufigkeits-Absicherung im geteilten Chat-Kontext,
|
||||
(3) kein Prompt-Caching — der mit Abstand größte Kostenhebel.
|
||||
|
||||
---
|
||||
|
||||
## 1. Kritische Sicherheitslücken (P0)
|
||||
|
||||
### S1 — DatabaseTool: Tabellen-Whitelist ist vollständig umgehbar
|
||||
[`DatabaseTool.cs:218`](../../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L218)
|
||||
|
||||
```csharp
|
||||
return allowed.Any(t => t != null && inputLower.Contains(t));
|
||||
```
|
||||
|
||||
Die Prüfung sucht den erlaubten Tabellennamen *irgendwo* im SQL-String — auch in einem
|
||||
Kommentar oder String-Literal. Bei `allowedTables: ["prices"]` und `accessLevel: ReadWrite`:
|
||||
|
||||
```sql
|
||||
DELETE FROM users -- prices
|
||||
```
|
||||
|
||||
`IsWriteAttempt` → true (ReadWrite erlaubt), `IsTableAllowed` → true (enthält "prices").
|
||||
Die `users`-Tabelle wird gelöscht. Gleiches gilt für jede beliebige andere Tabelle.
|
||||
|
||||
Zusätzlich:
|
||||
- `IsWriteAttempt`/`IsAdminAttempt` ([Z.189/195](../../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L189))
|
||||
sind ebenfalls Substring-Prüfungen → False Positives (`SELECT * FROM prices WHERE note='update'`
|
||||
wird als Schreibzugriff blockiert).
|
||||
- Mehrere Statements pro Aufruf sind nicht unterbunden (`;`-Verkettung).
|
||||
- Query-Ergebnisse haben kein `LIMIT` → eine Tabelle mit 100k Zeilen landet komplett im Kontext.
|
||||
|
||||
**Fix:** Echtes SQL-Parsing (z.B. via `Microsoft.SqlServer.TransactSql.ScriptDom` für MSSQL bzw.
|
||||
einem generischen Tokenizer), oder — deutlich robuster — den freien SQL-Zugriff durch
|
||||
vordefinierte, parametrisierte Named Queries ersetzen. Freies SQL vom LLM ist grundsätzlich
|
||||
schwer abzusichern.
|
||||
|
||||
### S2 — SocialMediaManager: Kommando-Injection über yt-dlp
|
||||
[`SocialMediaManagerTool.cs:442`](../../src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs#L442), auch Z.484 und Z.490
|
||||
|
||||
```csharp
|
||||
var psi = new ProcessStartInfo(ytDlpPath, $"--print \"%(id)s\" --playlist-end 1 {channelUrl}")
|
||||
```
|
||||
|
||||
`channelUrl` stammt aus dem Agenten-Input und wird ungequotet in die Argumentliste interpoliert.
|
||||
`UseShellExecute = false` verhindert Shell-Metazeichen, aber **nicht** Options-Injection:
|
||||
yt-dlp kennt `--exec`, das beliebige Befehle nach dem Download ausführt.
|
||||
|
||||
Ein Wert wie `--exec "cmd /c ..." https://youtube.com/...` führt zu Codeausführung.
|
||||
Kritisch wird das, weil der Agent untrusted Content verarbeitet (YouTube-Titel, X-Posts,
|
||||
Mails) — eine Prompt-Injection in abgerufenen Inhalten kann den Agenten dazu bringen,
|
||||
genau so einen "URL"-Parameter zu setzen.
|
||||
|
||||
**Fix:** `ProcessStartInfo.ArgumentList` verwenden (quotet korrekt), zusätzlich die URL gegen
|
||||
eine Regex validieren (`^https://(www\.)?(youtube\.com|youtu\.be)/…`) und `--`-Separator
|
||||
vor dem URL-Argument setzen.
|
||||
|
||||
### S3 — DirectAPI: API-Keys landen im Modell-Kontext
|
||||
[`DirectAPITool.cs:83`](../../src/ClawdDotNet.Tools.DirectAPI/DirectAPITool.cs#L83), auch Z.99, 154, 177, 263
|
||||
|
||||
```csharp
|
||||
var url = $"https://api.twelvedata.com/quote?symbol={symbol}&apikey={apiKey}";
|
||||
…
|
||||
return CreateSuccessResult(fetchedAt, dataAsOf, url, json.Value); // url → "source"
|
||||
```
|
||||
|
||||
Die vollständige URL inklusive `apikey=` wird als `source` an das Modell zurückgegeben. Damit
|
||||
landet der Key:
|
||||
- im Chat-Kontext → wird bei **jedem** Folge-Request erneut an OpenRouter gesendet,
|
||||
- in `ChatContext.json` auf der Platte,
|
||||
- in den Logs.
|
||||
|
||||
**Fix:** Key als Header senden statt als Query-Parameter (wo möglich), und in `source`
|
||||
grundsätzlich eine bereinigte URL zurückgeben (Query-String entfernen oder `apikey` maskieren).
|
||||
|
||||
### S4 — PermissionGate ist faktisch wirkungslos
|
||||
[`PermissionGate.cs:7`](../../src/ClawdDotNet.Core/Security/PermissionGate.cs#L7)
|
||||
|
||||
```csharp
|
||||
public bool IsAllowed(string agentId, string toolName, AgentConfig agentConfig)
|
||||
=> agentConfig.Tools.ContainsKey(toolName);
|
||||
```
|
||||
|
||||
Der einzige Check ist "Tool ist dem Agenten zugewiesen" — was `ToolRegistry.GetForAgent` ohnehin
|
||||
schon filtert. Der `agentId`-Parameter wird nicht verwendet. Es gibt keine Aktions-Granularität
|
||||
(lesen vs. schreiben vs. senden), kein Rate-Limiting, kein Audit. Die gesamte Sicherheit liegt
|
||||
ad-hoc in den einzelnen Tools — jedes mit eigener, inkonsistenter Logik.
|
||||
|
||||
**Fix:** Das Gate zum zentralen Policy-Punkt ausbauen: Aktions-Ebene (`Tool.Action`),
|
||||
Rate-Limits pro Agent/Tool/Zeitfenster, Audit-Log jedes Aufrufs mit Argumenten, und ein
|
||||
Approval-Hook für irreversible Aktionen (siehe F-A1).
|
||||
|
||||
### S5 — WebFetch: kein SSRF-Schutz bei Redirects
|
||||
[`WebFetchTool.cs:58`](../../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L58)
|
||||
|
||||
Die Domain-Whitelist wird nur auf die *ursprüngliche* URL angewendet. `HttpClient` folgt
|
||||
Redirects standardmäßig — eine erlaubte Domain kann auf `http://169.254.169.254/`,
|
||||
`http://localhost:8418/` (dein Gitea!) oder beliebige interne Hosts weiterleiten.
|
||||
|
||||
Nebenbei: `uri.Host.Replace("www.", "")` ([Z.59](../../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L59))
|
||||
ersetzt das Fragment überall im Hostnamen, nicht nur am Anfang.
|
||||
|
||||
**Fix:** `HttpClientHandler { AllowAutoRedirect = false }` und Redirects manuell auflösen,
|
||||
dabei jede Zwischen-URL erneut gegen die Whitelist prüfen. Zusätzlich private IP-Bereiche
|
||||
(RFC1918, Loopback, Link-Local) hart blockieren.
|
||||
|
||||
### S6 — FileRW: Path-Traversal-Prüfung per Präfix
|
||||
[`FileRWTool.cs:211`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L211)
|
||||
|
||||
```csharp
|
||||
if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
|
||||
```
|
||||
|
||||
Ohne abschließenden Verzeichnis-Separator: Root `C:\Instances\Dev\Workspace` erlaubt Zugriff auf
|
||||
`C:\Instances\Dev\Workspace-Backup\…`. In der aktuellen Verzeichnisstruktur nicht direkt
|
||||
ausnutzbar, aber eine Zeitbombe.
|
||||
|
||||
**Fix:** `rootPath` mit `Path.DirectorySeparatorChar` terminieren, oder besser
|
||||
`Path.GetRelativePath` + Prüfung auf `..`-Präfix.
|
||||
|
||||
### S7 — Secrets im Klartext
|
||||
`openRouterApiKey`, DB-`connectionString` (mit Passwort), Mail-`password`, Telegram-`password2FA`
|
||||
liegen unverschlüsselt in `AgentSettings.json` / `InstanceConfig.json`
|
||||
([`InstanceConfig.cs:46`](../../src/ClawdDotNet.Core/Config/InstanceConfig.cs#L46)).
|
||||
|
||||
**Fix:** DPAPI (`ProtectedData.Protect` mit `CurrentUser`-Scope) für alle Secret-Felder,
|
||||
oder Windows Credential Manager. Zumindest sollten die Felder beim Speichern verschlüsselt
|
||||
und erst zur Laufzeit entschlüsselt werden.
|
||||
|
||||
---
|
||||
|
||||
## 2. Bugs (P1)
|
||||
|
||||
### B1 — ContextCompactor zerstört die tool_call-Paarung ⚠️
|
||||
[`ContextCompactor.cs:140`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
|
||||
|
||||
```csharp
|
||||
var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
|
||||
messages.Clear();
|
||||
… messages.AddRange(tail);
|
||||
```
|
||||
|
||||
Die letzten 6 Nachrichten werden blind übernommen. Wenn die Grenze mitten in einer
|
||||
Tool-Sequenz liegt, beginnt der Tail mit einer `tool`-Nachricht, deren zugehörige
|
||||
`assistant`-Nachricht mit `tool_calls` gerade gelöscht wurde. Die OpenRouter/Anthropic-API
|
||||
lehnt das mit HTTP 400 ab ("tool_result without preceding tool_use").
|
||||
|
||||
Das passiert **genau dann**, wenn der Kontext lang wird — also im wichtigsten Moment.
|
||||
Bei einer typischen Sequenz `assistant(2 tool_calls) → tool → tool` ist die Wahrscheinlichkeit
|
||||
hoch.
|
||||
|
||||
**Fix:** Die Tail-Grenze auf einen sicheren Schnittpunkt verschieben — rückwärts suchen bis zu
|
||||
einer Nachricht mit Rolle `user` oder `assistant` ohne `tool_calls`. Analog muss ein
|
||||
`assistant` mit `tool_calls` am Ende immer seine vollständigen `tool`-Antworten behalten.
|
||||
|
||||
### B2 — Race Condition auf dem geteilten Chat-Kontext ⚠️
|
||||
[`AgentEngine.cs:250`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L250) und Z.268/316/322
|
||||
|
||||
`_chatContexts[agentId]` ist eine geteilte `List<ChatMessage>`. Nur der *Lookup* läuft unter
|
||||
`_lock` — alle `messages.Add(...)` im Loop passieren ungeschützt.
|
||||
|
||||
Für denselben Agenten kann `ChatAsync` mehrfach parallel laufen:
|
||||
- Telegram-/Mail-ToolJob weckt den Agenten (`ToolJobScheduler.cs:187`),
|
||||
- der Benutzer schickt gleichzeitig eine WebView-Nachricht,
|
||||
- ein anderer Agent ruft `send_message` auf (`AgentEngine.cs:528`).
|
||||
|
||||
Folgen: `List<T>`-Korruption (nicht threadsicher), verschränkte Tool-Call-Sequenzen →
|
||||
API-400-Fehler, und `_runningChats[agentId] = linkedCts` überschreibt den vorherigen Eintrag,
|
||||
sodass `AbortChat` nur den zuletzt gestarteten Lauf abbricht.
|
||||
|
||||
**Fix:** Pro Agent ein `SemaphoreSlim(1,1)`, das den gesamten `ChatAsync`-Durchlauf serialisiert.
|
||||
Wartende Nachrichten in eine Queue statt parallel starten.
|
||||
|
||||
Beim Umsetzen kamen zwei Folgeprobleme dazu, die denselben Ursprung haben:
|
||||
- `ExecuteToolCallAsync` fing `OperationCanceledException` mit ab und gab sie als
|
||||
Tool-Fehlerergebnis zurück. Der Abbruch griff dadurch erst einen Schritt später.
|
||||
- `send_message` an den eigenen Agenten wäre mit dem neuen Gate in einen Deadlock gelaufen
|
||||
(der laufende Chat hält es bereits) — wird jetzt abgelehnt.
|
||||
|
||||
### B3 — `maxTokens` vermischt Abrechnungs-Budget und Kontextgröße ⚠️
|
||||
[`LoopGuard.cs:22`](../../src/ClawdDotNet.Core/Engine/LoopGuard.cs#L22), Defaults in
|
||||
[`AgentConfig.cs:105`](../../src/ClawdDotNet.Core/Config/AgentConfig.cs#L105)
|
||||
|
||||
`RecordTokens` summiert `response.Usage.TotalTokens` über alle Schritte. Da jeder Schritt den
|
||||
**kompletten** Kontext erneut sendet, wächst diese Summe quadratisch:
|
||||
|
||||
| Schritt | Kontext | kumuliert |
|
||||
|---|---|---|
|
||||
| 1 | 20.000 | 20.000 |
|
||||
| 2 | 21.000 | 41.000 |
|
||||
| 3 | 22.000 | 63.000 |
|
||||
| 4 | 23.000 | **86.000** → Abbruch |
|
||||
|
||||
Mit `maxTokens: 80_000` bricht ein völlig normaler Chat nach 4 Schritten mit
|
||||
"Max tokens exceeded" ab, obwohl der Kontext nie über 23k lag. Das ist vermutlich die
|
||||
häufigste Frustquelle im laufenden Betrieb.
|
||||
|
||||
**Fix:** Zwei getrennte Begriffe:
|
||||
- `maxCumulativeTokens` — Kostenbudget pro Run (bewusst hoch, z.B. 500k)
|
||||
- `maxContextTokens` — Kontextgröße, steuert die Compaction (existiert bereits)
|
||||
|
||||
und beide getrennt im UI ausweisen.
|
||||
|
||||
### B4 — Kostenanzeige ist doppelt falsch
|
||||
[`frm_main.cs:292`](../../frm_main.cs#L292)
|
||||
|
||||
```csharp
|
||||
_statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2);
|
||||
```
|
||||
|
||||
1. **50/50-Aufteilung**: Reales Verhältnis in Agenten-Loops ist eher 95:5 (Prompt:Completion).
|
||||
Da Output ~5x teurer ist, überschätzt die Anzeige die Kosten um Faktor ~2,5.
|
||||
2. **Modell fehlt in der Preistabelle**: `AgentConfig.Model` hat den Default
|
||||
`anthropic/claude-sonnet-4-5` — dieser Eintrag existiert in `ModelPricing`
|
||||
([`OpenRouterStatusService.cs:15`](../../Services/OpenRouterStatusService.cs#L15)) nicht.
|
||||
`CalculateCost` gibt dann stillschweigend `0` zurück.
|
||||
|
||||
Die Tabelle ist zudem veraltet (`claude-sonnet-4`, `claude-opus-4`, `claude-haiku-4.5`).
|
||||
|
||||
**Fix:** `Usage` um `PromptTokens`/`CompletionTokens` erweitern und bis in `AgentRunResult`
|
||||
durchreichen. Preise live vom `/models`-Endpoint holen (liefert `pricing.prompt`/`pricing.completion`)
|
||||
statt hartzucodieren. Bei unbekanntem Modell sichtbar warnen statt 0 anzuzeigen.
|
||||
|
||||
### B5 — Tool-Ergebnisse landen ungekappt im Kontext
|
||||
Ein einziger `WebFetch` mit dem Default `maxResponseKb: 512`
|
||||
([`WebFetchTool.cs:90`](../../src/ClawdDotNet.Tools.WebFetch/WebFetchTool.cs#L90)) erzeugt bis zu
|
||||
512 KB Text ≈ **130.000 Tokens** in einer einzigen Tool-Antwort. `FileRW.read`
|
||||
([`FileRWTool.cs:245`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L245)) hat gar kein Limit,
|
||||
`Database.query` und `Mail.read_inbox` ebenfalls nicht.
|
||||
|
||||
Gekürzt wird erst nachträglich in der Compaction — und dort nur außerhalb der letzten
|
||||
6 Nachrichten. Der teure Request ist zu dem Zeitpunkt längst bezahlt.
|
||||
|
||||
**Fix:** Kappung beim Einfügen in `ExecuteToolCallAsync`
|
||||
([`AgentEngine.cs:668`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L668)) — zentral, für alle
|
||||
Tools, mit konfigurierbarem `maxToolResultTokens` und einem Hinweis an das Modell, dass gekürzt
|
||||
wurde (inkl. Angebot, gezielt nachzulesen).
|
||||
|
||||
### B6 — `Task.Delay` wirft bei langen Cron-Intervallen
|
||||
[`AgentScheduler.cs:89`](../../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L89) und
|
||||
[`ToolJobScheduler.cs:126`](../../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L126)
|
||||
|
||||
`Task.Delay` wirft `ArgumentOutOfRangeException` bei Werten über ~24,8 Tagen. Ein jährlicher
|
||||
Cron (`0 3 1 1 *`) erzeugt eine Wartezeit von bis zu 365 Tagen. Der `catch` fängt nur
|
||||
`OperationCanceledException` — die Exception beendet den Scheduler-Task still. Da die Tasks in
|
||||
`_schedulerTasks` nie beobachtet werden (`await` erst beim Dispose), fällt das nicht auf: der
|
||||
Job läuft ab dann einfach nie wieder.
|
||||
|
||||
**Fix:** Delay auf max. 1 Tag deckeln und in einer Schleife erneut prüfen. Zusätzlich alle
|
||||
Scheduler-Tasks mit einem `ContinueWith`-Fehler-Logger versehen.
|
||||
|
||||
### B7 — Cron rechnet in Lokalzeit
|
||||
[`CronExpression.cs:51`](../../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L51) mit
|
||||
`DateTime.Now`. Bei Zeitumstellung: im Oktober läuft ein `0 2 * * *`-Job doppelt, im März gar nicht.
|
||||
|
||||
**Fix:** Intern in UTC rechnen und nur für die Anzeige konvertieren, oder `TimeZoneInfo`
|
||||
explizit berücksichtigen.
|
||||
|
||||
### B8 — Keine Rekursionsbremse bei AgentComm / AgentSpawn
|
||||
[`AgentEngine.cs:510`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L510)
|
||||
|
||||
Agent A ruft `send_message(B)` → `ChatAsync(B)` → B ruft `send_message(A)` → … Es gibt keine
|
||||
Tiefenbegrenzung und keinen Zyklus-Check. Bei `SpawnAgentAsync` schützt immerhin der
|
||||
`IsRunning`-Check, bei `send_message` nicht. Begrenzt wird das nur durch die Timeouts —
|
||||
in der Zwischenzeit brennt jeder Hop einen vollständigen LLM-Run.
|
||||
|
||||
**Fix:** Aufruf-Tiefe im `AgentToolContext` mitführen (`maxDelegationDepth`, Default 3) und
|
||||
den Aufrufpfad zur Zyklenerkennung mitgeben.
|
||||
|
||||
### B9 — `index_Count` liest die Datei außerhalb des Locks erneut
|
||||
[`FileRWTool.cs:569`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L569) ruft
|
||||
[`index_Count`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L572) auf, nachdem das Lock
|
||||
freigegeben wurde — Race mit parallelen `stock_add`-Aufrufen, plus ein überflüssiges
|
||||
vollständiges Parsen der Index-Datei. Der Zähler ist innerhalb des Locks ohnehin bekannt
|
||||
(`index.Count`).
|
||||
|
||||
### B10 — `PersistChatState` schreibt bei jedem Eintrag alles neu
|
||||
[`AgentEngine.cs:593`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L593)
|
||||
|
||||
Vollständige History **und** vollständiger Kontext werden als `WriteIndented`-JSON bei jedem
|
||||
einzelnen Chat-Eintrag rausgeschrieben → O(n²) Schreiblast über eine Sitzung. Bei einem Agenten
|
||||
mit 500 Nachrichten sind das mehrere MB pro Nachricht.
|
||||
|
||||
**Fix:** History als Append-only JSONL, Kontext nur periodisch/beim Beenden persistieren,
|
||||
`WriteIndented = false`.
|
||||
|
||||
### B11 — Kein `max_tokens` im Request
|
||||
[`AgentEngine.cs:105`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L105) und Z.279 setzen
|
||||
`ChatRequest.MaxTokens` nie. Ein Modell kann in einem Schritt sein volles Output-Limit
|
||||
ausschöpfen.
|
||||
|
||||
### B12 — Kein Retry/Backoff
|
||||
[`OpenRouterClient.cs:42`](../../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L42) wirft bei jedem
|
||||
Nicht-2xx sofort. Ein einzelnes HTTP 429 killt einen kompletten geplanten Run. Bei OpenRouter
|
||||
sind 429/502/503 im Normalbetrieb zu erwarten.
|
||||
|
||||
**Fix:** Polly o.ä. mit exponentiellem Backoff + Jitter für 429/5xx, `Retry-After` respektieren.
|
||||
|
||||
### B14 — Compaction dupliziert den System-Prompt bei kurzen Konversationen ⚠️
|
||||
[`ContextCompactor.cs:140`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
|
||||
|
||||
*Gefunden durch den Property-Test, nicht beim Lesen des Codes.*
|
||||
|
||||
```csharp
|
||||
var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
|
||||
messages.Clear();
|
||||
if (systemMsg is not null) messages.Add(systemMsg); // ← System-Prompt
|
||||
…
|
||||
messages.AddRange(tail); // ← enthält ihn nochmal
|
||||
```
|
||||
|
||||
Bei `messages.Count <= 6` ist `Skip(0)` — der Tail enthält also **alle** Nachrichten
|
||||
einschließlich der System-Nachricht. Diese wird danach ein zweites Mal angehängt.
|
||||
|
||||
Ergebnis: zwei `system`-Nachrichten, die zweite mitten in der Konversation. Zusätzlich wird
|
||||
die komplette Konversation dupliziert, obwohl sie gerade zusammengefasst wurde — die
|
||||
Compaction *vergrößert* den Kontext in diesem Fall, statt ihn zu verkleinern.
|
||||
|
||||
Erreichbar, sobald wenige Nachrichten die Token-Schwelle reißen — z.B. ein einzelner
|
||||
`WebFetch` mit 500 KB Ergebnis (siehe B5). Minimales Gegenbeispiel aus FsCheck:
|
||||
System-Prompt + eine User-Nachricht.
|
||||
|
||||
**Fix:** Den System-Prompt aus dem Tail herausfiltern, bevor er wieder angehängt wird —
|
||||
und den Tail generell erst ab der Position hinter der System-Nachricht bilden.
|
||||
|
||||
### B13 — Inkonsistente `instanceId`
|
||||
`RunAsync`/`ChatAsync` bekommen `instanceId` als Parameter, `SendMessageAsync`/`SpawnAgentAsync`
|
||||
verwenden dagegen das Feld `_instanceId`, das nur gesetzt wird, wenn
|
||||
`SetAgentConfigProvider` aufgerufen wurde — sonst `""`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Konzeptfehler (P2)
|
||||
|
||||
### K1 — Geplante Agenten haben kein Gedächtnis
|
||||
`RunAsync` baut bei jedem Cron-Tick eine frische Nachrichtenliste
|
||||
([`AgentEngine.cs:89`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L89)). Ein Agent, der alle
|
||||
30 Minuten läuft, weiß nichts von seinem letzten Lauf: er ruft dieselben Quellen erneut ab,
|
||||
zieht dieselben Schlüsse und kann keine Entwicklung über Zeit verfolgen.
|
||||
|
||||
Das ist gleichzeitig die größte Fähigkeitslücke **und** eine dauerhafte Token-Verschwendung.
|
||||
Der `IStateStore` (SQLite) existiert bereits — es fehlt das Konzept darüber.
|
||||
|
||||
### K2 — Prompt-Injection ist unbehandelt
|
||||
Agenten lesen untrusted Content (Web, Mail, Telegram, X, YouTube-Transkripte) und besitzen
|
||||
gleichzeitig ausgehende Fähigkeiten (Mail senden, X posten, Dateien schreiben, DB schreiben).
|
||||
Tool-Ergebnisse werden ununterscheidbar von echten Anweisungen in den Kontext gelegt.
|
||||
|
||||
Das ist die klassische gefährliche Kombination aus Datenzugriff, untrusted Input und
|
||||
Exfiltrations-Kanal. Ein präparierter Tweet oder eine Mail kann den Agenten steuern.
|
||||
|
||||
**Ansatz:** Tool-Ergebnisse konsequent als Daten rahmen (`<untrusted_content source="...">`),
|
||||
im System-Prompt verankern, dass daraus keine Anweisungen befolgt werden, und irreversible
|
||||
Aktionen an eine Freigabe koppeln (siehe F-A1).
|
||||
|
||||
### K3 — Keine Tests
|
||||
Kein Testprojekt in `ClawdDotNet.slnx`. Bei einer Engine mit Zustandsmaschine, Compaction,
|
||||
Scheduling und Nebenläufigkeit ist das riskant — die Bugs B1 und B2 wären mit
|
||||
Unit-Tests sofort aufgefallen.
|
||||
|
||||
**Ansatz:** `ClawdDotNet.Core.Tests` mit einem Fake-`OpenRouterClient` (Interface extrahieren).
|
||||
Erste Kandidaten: `CronExpression`, `ContextCompactor` (Paarungs-Invarianten!), `LoopGuard`,
|
||||
`FileRWTool`-Pfadprüfungen.
|
||||
|
||||
### K4 — Kein Streaming
|
||||
`request.Stream = false` fest verdrahtet ([`OpenRouterClient.cs:34`](../../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L34)).
|
||||
Bei langen Antworten wirkt die UI eingefroren; es gibt nur den Typing-Indicator.
|
||||
|
||||
### K5 — Kein Kostenlimit
|
||||
Es gibt kein hartes Budget pro Agent/Instanz/Tag. Ein Agent in einer Schleife (siehe B8) kann
|
||||
unbeaufsichtigt Guthaben verbrennen. Die Credits-Anzeige ist rein informativ.
|
||||
|
||||
### K6 — Chat-Historie wächst unbegrenzt
|
||||
`_chatHistories` und die zugehörigen JSON-Dateien werden nie rotiert oder archiviert.
|
||||
|
||||
---
|
||||
|
||||
## 4. Token-Optimierung
|
||||
|
||||
Das ist der Bereich mit dem größten unmittelbaren Hebel. Nach Wirkung sortiert:
|
||||
|
||||
### T1 — Prompt-Caching einführen (Einsparung: 60–90 % der Input-Tokens) ★★★
|
||||
|
||||
Der wichtigste Punkt. Aktuell wird bei **jedem** Schritt der gesamte Prompt neu gesendet und
|
||||
voll berechnet — inklusive System-Prompt (Identity + Soul + SystemPrompt) und aller
|
||||
Tool-Definitionen. Das ist ein stabiler Präfix, der sich innerhalb eines Runs nie ändert.
|
||||
|
||||
Anthropic-Modelle über OpenRouter unterstützen `cache_control`-Breakpoints: gecachte
|
||||
Input-Tokens kosten nur ~10 % des Normalpreises (Schreiben in den Cache kostet einmalig ~125 %).
|
||||
|
||||
**Rechenbeispiel** — 10-Schritt-Run, Präfix 15k Tokens (System + 10 Tool-Definitionen):
|
||||
|
||||
| | Input-Tokens (Präfix) | relative Kosten |
|
||||
|---|---|---|
|
||||
| heute | 10 × 15.000 = 150.000 | 100 % |
|
||||
| mit Caching | 15.000 (×1,25) + 9 × 15.000 (×0,1) ≈ 32.250 effektiv | **~21 %** |
|
||||
|
||||
Umsetzung:
|
||||
1. `ChatMessage.Content` muss das Array-Format unterstützen
|
||||
(`[{ "type": "text", "text": "...", "cache_control": { "type": "ephemeral" } }]`).
|
||||
Aktuell ist es ein reiner `string` ([`ChatMessage.cs:12`](../../src/ClawdDotNet.Core/Api/Models/ChatMessage.cs#L12)).
|
||||
2. Cache-Breakpoint ans Ende des System-Prompts und ans Ende der Tool-Definitionen setzen.
|
||||
3. Optional einen dritten Breakpoint nach der letzten stabilen Konversationsgrenze (rollierend).
|
||||
4. `Usage` um `prompt_tokens_details.cached_tokens` erweitern, damit der Effekt messbar wird.
|
||||
|
||||
Wichtig: Die Reihenfolge der Nachrichten vor dem Breakpoint muss **byte-identisch** bleiben —
|
||||
d.h. keine Zeitstempel oder wechselnden Hinweise im System-Prompt.
|
||||
|
||||
### T2 — Tool-Ergebnisse beim Einfügen kappen (Einsparung: stark schwankend, bis 90 %) ★★★
|
||||
Siehe B5. Ein einziger ungekappter `WebFetch` kann mehr Tokens erzeugen als der gesamte
|
||||
restliche Run. Zentral in `ExecuteToolCallAsync` lösen, nicht pro Tool.
|
||||
|
||||
Sinnvolle Defaults: 4.000 Tokens pro Tool-Ergebnis, mit Kürzungshinweis und der Möglichkeit,
|
||||
gezielt weiterzulesen (Offset-Parameter bei `FileRW.read`, `LIMIT`/`OFFSET` bei `Database.query`).
|
||||
|
||||
### T3 — Günstiges Modell für die Compaction (Einsparung: ~95 % der Compaction-Kosten) ★★
|
||||
[`ContextCompactor.cs:114`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L114) nutzt
|
||||
`model` — also das teure Modell des Agenten — um bis zu 30k Zeichen zusammenzufassen.
|
||||
Bei Opus kostet eine einzige Compaction so mehr als der halbe Run.
|
||||
|
||||
**Fix:** `summaryModel` konfigurierbar machen, Default z.B. `google/gemini-2.5-flash` oder
|
||||
`anthropic/claude-haiku-4.5`. Zusammenfassen ist eine anspruchslose Aufgabe.
|
||||
|
||||
### T4 — Proaktiv statt reaktiv kompaktieren ★★
|
||||
`CompactIfNeededAsync` läuft **nach** dem API-Call
|
||||
([`AgentEngine.cs:123`](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L123)) und nutzt die
|
||||
`promptTokens` der gerade bezahlten Anfrage. Der überfüllte Prompt wurde also bereits berechnet.
|
||||
|
||||
**Fix:** Vor dem Senden prüfen (mit `EstimateTokens`, das es schon gibt) und erst dann den
|
||||
Request bauen. Die gemessenen `promptTokens` dienen zur Kalibrierung der Schätzung.
|
||||
|
||||
### T5 — `maxTokens`-Semantik korrigieren ★★
|
||||
Siehe B3. Aktuell führt der Fehler dazu, dass Läufe abbrechen und **neu gestartet** werden —
|
||||
was die Tokenkosten verdoppelt statt sie zu begrenzen.
|
||||
|
||||
### T6 — Tool-Definitionen verschlanken ★
|
||||
Die Beschreibungen sind großzügig: `FileRW` allein hat ~500 Zeichen Description plus ein
|
||||
Schema mit 11 Properties ≈ 900 Tokens
|
||||
([`FileRWTool.cs:26`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L26)). Bei 8 zugewiesenen
|
||||
Tools sind das schnell 4–6k Tokens — bei **jedem** Schritt (ohne Caching).
|
||||
|
||||
Maßnahmen:
|
||||
- Beschreibungen auf das Nötige kürzen, Beispiele in den System-Prompt statt ins Schema.
|
||||
- `stock_add` aus `FileRW` herauslösen — es ist ein fachfremdes Feature, das das Schema aufbläht,
|
||||
auch für Agenten, die es nie nutzen.
|
||||
- Für Agenten mit vielen Tools: zweistufiges Laden (`list_toolsets` → `load_toolset`).
|
||||
|
||||
### T7 — `RunAsync` statt `ChatAsync` für Job-Wakeups prüfen ★
|
||||
[`ToolJobScheduler.cs:186`](../../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L186)
|
||||
entscheidet über `UseChatContext`. Mit `true` wird die komplette (potenziell riesige)
|
||||
Chat-Historie in einen "Prüfe neue Mails"-Tick gezogen. Für zustandslose Ticks ist `RunAsync`
|
||||
um Größenordnungen günstiger — der Default sollte bewusst gesetzt und im UI erklärt sein.
|
||||
|
||||
### T8 — `max_tokens` setzen ★
|
||||
Siehe B11. Verhindert Ausreißer bei der Ausgabe (die teuerste Token-Art).
|
||||
|
||||
### T9 — Caching-Wirkung messbar machen ★
|
||||
Ohne `cached_tokens` im `Usage`-Modell lässt sich der Erfolg von T1 nicht belegen. Sollte
|
||||
zusammen mit T1 kommen und im Token-Dashboard auftauchen.
|
||||
|
||||
### Erwartete Gesamtwirkung
|
||||
T1 + T2 + T3 + T4 zusammen sollten die Kosten eines typischen Multi-Step-Runs auf
|
||||
**etwa ein Viertel bis ein Fünftel** senken — ohne Qualitätsverlust, da nur Redundanz und
|
||||
Ballast entfernt werden.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vorschläge für Erweiterungen
|
||||
|
||||
### Neue Tools
|
||||
|
||||
| Tool | Nutzen |
|
||||
|---|---|
|
||||
| **Memory** | Strukturiertes Langzeitgedächtnis (Fakten, Entscheidungen, Beobachtungen mit Zeitstempel) auf dem vorhandenen SQLite-Store. Löst K1 und spart massiv Tokens, weil Wissen nicht neu hergeleitet wird. **Höchste Priorität.** |
|
||||
| **WebSearch** | Aktuell können Agenten nur bekannte Domains abrufen (`WebFetch`), aber nichts finden. Brave/Tavily/SearXNG-Anbindung. |
|
||||
| **Http** | Generisches REST-Tool mit Allowlist pro Agent. `DirectAPI` ist fest auf Finanz-Provider verdrahtet — jede neue API erfordert heute Code. |
|
||||
| **Shell** | Sandboxed, mit Kommando-Allowlist. Ersetzt die hartcodierten yt-dlp/ffmpeg-Aufrufe (und behebt S2 gleich mit). |
|
||||
| **Git** | Für den "Senior Developer"-Agenten — jetzt, wo das Projekt in Git liegt. |
|
||||
| **Vision** | Charts/Screenshots analysieren — für das Trading-Team naheliegend. |
|
||||
| **Notify** | Einheitliche Benachrichtigung (Telegram existiert; ntfy/Discord/Push ergänzen). |
|
||||
|
||||
### Neue Konzepte
|
||||
|
||||
**F-A1 — Freigabe-Workflow (Human-in-the-Loop)**
|
||||
Irreversible Aktionen (Mail senden, X posten, DB-Schreibzugriff, Datei löschen) landen in einer
|
||||
Freigabe-Queue im Hauptfenster, statt sofort ausgeführt zu werden. Pro Tool/Aktion
|
||||
konfigurierbar: `auto` | `approve` | `deny`. Das entschärft K2 und S1/S2 erheblich.
|
||||
|
||||
**F-A2 — Audit-Log**
|
||||
Jeder Tool-Aufruf mit Agent, Zeitstempel, Argumenten, Ergebnis-Status in eine eigene
|
||||
SQLite-Tabelle. Aktuell existiert nur `LogDebug` — nach einem Zwischenfall ist nicht
|
||||
rekonstruierbar, was ein Agent getan hat.
|
||||
|
||||
**F-A3 — Budget-Guard**
|
||||
Hartes USD-Limit pro Agent und pro Instanz und Tag. Bei Überschreitung wird der Agent pausiert
|
||||
und im UI markiert. Setzt B4 (korrekte Kostenerfassung) voraus.
|
||||
|
||||
**F-A4 — Run-Historie**
|
||||
`AgentRunResult` wird aktuell nur im Speicher als "letztes Ergebnis" gehalten
|
||||
([`AgentScheduler.cs:14`](../../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L14)).
|
||||
Persistierte Runs (mit Schritten, Tokens, Kosten, Fehlern) wären die Grundlage für Diagnose
|
||||
und Kostenanalyse.
|
||||
|
||||
**F-A5 — Task-Queue statt rekursivem `send_message`**
|
||||
Agent-zu-Agent-Kommunikation läuft heute als synchrone Rekursion (B8). Eine echte
|
||||
Aufgaben-Queue mit Zustand (`pending`/`running`/`done`) wäre robuster, nachvollziehbar und
|
||||
würde Zyklen strukturell verhindern.
|
||||
|
||||
**F-A6 — Secret-Store**
|
||||
DPAPI-verschlüsselte Secrets (S7), mit UI zum Setzen/Rotieren.
|
||||
|
||||
**F-A7 — Testprojekt**
|
||||
Siehe K3.
|
||||
|
||||
---
|
||||
|
||||
## 6. Vorgeschlagene Reihenfolge
|
||||
|
||||
> **Abgelöst durch die [Roadmap](../Roadmap.md)** (Juli 2026). Die offenen Punkte
|
||||
> werden dort weitergeführt; dieser Abschnitt bleibt als Stand der Bestandsaufnahme
|
||||
> eingefroren. F-A1/S4, F-A2, F-A5, T6, T7 sowie B6–B8 sind in den Roadmap-Vorhaben
|
||||
> A1–A4 aufgegangen.
|
||||
|
||||
**Sofort — es blockiert oder gefährdet den Betrieb**
|
||||
1. ~~B1 Compaction-Paarung (bricht produktiv ab)~~ ✅ behoben
|
||||
2. ~~B3 `maxTokens`-Semantik (bricht produktiv ab)~~ ✅ behoben
|
||||
2b. ~~B14 System-Prompt-Duplikat~~ ✅ behoben
|
||||
3. ~~B2 Race Condition im Chat-Kontext~~ ✅ behoben
|
||||
4. ~~S2 yt-dlp-Injection~~ ✅ behoben
|
||||
5. ~~S3 API-Key-Leak~~ ✅ behoben
|
||||
|
||||
**Kurzfristig — größter Nutzen pro Aufwand**
|
||||
6. ~~T1 Prompt-Caching~~ ✅ umgesetzt (inkl. T9 `cached_tokens`)
|
||||
7. ~~T2 Tool-Ergebnisse kappen (= B5)~~ ✅ umgesetzt
|
||||
8. ~~T3 Günstiges Compaction-Modell~~ ✅ umgesetzt
|
||||
9. ~~B4 Kostenerfassung korrigieren~~ ✅ vollständig: Prompt/Completion getrennt
|
||||
erfasst, Preise kommen live vom `/models`-Endpunkt (`ModelPricingCatalog`),
|
||||
Modelle ohne Preisdaten werden sichtbar gemeldet statt still mit 0 gerechnet.
|
||||
10. ~~B12 Retry/Backoff~~ ✅ umgesetzt
|
||||
11. T4 Proaktiv statt reaktiv kompaktieren
|
||||
|
||||
**Mittelfristig — Fundament**
|
||||
11. ~~S1 DatabaseTool absichern~~ ✅ behoben (`SqlGuard`)
|
||||
12. Memory-Tool (K1)
|
||||
13. F-A1 Freigabe-Workflow + F-A2 Audit-Log
|
||||
14. K3 Testprojekt
|
||||
15. S4 PermissionGate ausbauen
|
||||
|
||||
**Danach**
|
||||
16. Restliche Bugs (B6–B10, B13), S4 PermissionGate, weitere Tools, Streaming
|
||||
(~~S5~~ ✅ SSRF, ~~S6~~ ✅ Pfadprüfung, ~~S7~~ ✅ Secrets sind behoben)
|
||||
|
||||
---
|
||||
|
||||
## 7. Umgesetzt
|
||||
|
||||
| Punkt | Was |
|
||||
|---|---|
|
||||
| B1, B14 | Compaction: sicherer Schnittpunkt, kein doppelter System-Prompt |
|
||||
| B2 | Chat-Läufe pro Agent serialisiert, `AbortChat` erreicht alle |
|
||||
| B3 | `maxCumulativeTokens` von `maxContextTokens` getrennt |
|
||||
| B4 | Prompt/Completion getrennt erfasst, Preise live vom Anbieter |
|
||||
| B12 | Retry mit Backoff für 429/5xx |
|
||||
| S1 | `SqlGuard` statt Teilzeichenketten-Prüfung |
|
||||
| S2 | `YouTubeUrl` + `ArgumentList` gegen Options-Injection |
|
||||
| S3 | `UrlSanitizer` gegen API-Key-Leak ins Modell |
|
||||
| S5 | `UrlGuard`, Redirects einzeln geprüft |
|
||||
| S6 | `WorkspacePath` auf Verzeichnisgrenzen |
|
||||
| S7 | `SecretProtector` (DPAPI) für Zugangsdaten |
|
||||
| T1, T9 | Prompt-Caching mit Breakpoints, `cached_tokens` gemessen |
|
||||
| T2 | Tool-Ergebnisse zentral gekappt |
|
||||
| T3 | Günstiges Modell für die Zusammenfassung |
|
||||
| K3 | Testfundament: 309 Tests, davon ~90 gezielte Angriffsfälle |
|
||||
@@ -0,0 +1,299 @@
|
||||
# Deploymentcenter 2.2 – 2.4: Was noch zu tun ist
|
||||
|
||||
Stand: 2026-08-13. Ergänzt [Deploymentcenter-Integration](../Deploymentcenter-Integration.md)
|
||||
(dort steht der Stand nach 2.1) um die drei neuen Ausbaustufen.
|
||||
|
||||
| Fassung | Was dazukam | Betrifft uns |
|
||||
|---|---|---|
|
||||
| **2.2** | Plattform-Dimension, signierte Releases, Anwenden mit Rollback, `preservePatterns` | Release-Strecke, Update-Anwendung |
|
||||
| **2.3** | Erstinstallation über `update-agent --action install`, `setup.json`, Installationskonto | Neu, siehe §4 |
|
||||
| **2.4** | Release-Ablage hinter HTTP-Basic-Auth, Zugang über den Lizenzschlüssel | **Erledigt**, siehe §1 |
|
||||
|
||||
---
|
||||
|
||||
## 1. Zugangsschutz (2.4) — erledigt
|
||||
|
||||
`CheckForUpdateAsync` übergibt jetzt `ReleaseCredentials.FromLicenseKey(...)`, und
|
||||
`UpdateCheckResult.Unauthorized` wird getrennt von einem Netzfehler behandelt.
|
||||
|
||||
**Warum das nicht warten konnte:** UPGRADE §16.1 empfiehlt „erst ausliefern, dann
|
||||
scharfschalten". Für ein Produkt, das noch nie veröffentlicht hat, geht diese Reihenfolge
|
||||
nicht auf. `ReleaseGuard::regenerateForProject` überspringt Verzeichnisse, die es nicht
|
||||
gibt — `/releases/clawddotnet/` liefert derzeit 404, es ist also nichts geschützt. Sobald
|
||||
wir das **erste** Release hochladen, entsteht das Verzeichnis, und der nächste
|
||||
`tick.php`-Lauf legt den Schutz an. Der erste ausgelieferte Build muss die Zugangsdaten
|
||||
also bereits mitbringen, sonst schließt sich die Tür hinter dem ersten Release.
|
||||
|
||||
---
|
||||
|
||||
## 2. Plattform (2.2) — Release-Strecke steht
|
||||
|
||||
Erstes Paket veröffentlicht: **0.1.0, Kanal `dev`, Plattform `win-x64`**, 116 Dateien,
|
||||
25 MB, Rückgabewert 0. Die Update-Prüfung antwortet korrekt (`0.0.9` → Update, `0.1.0` →
|
||||
keins).
|
||||
|
||||
```bash
|
||||
pack-and-deploy --config deploy/packager.config.json \
|
||||
--project clawddotnet --version 0.1.0 \
|
||||
--channel dev --platform win-x64 \
|
||||
--publish-dir <dotnet-publish-Ausgabe>
|
||||
```
|
||||
|
||||
- Zugangsdaten in `deploy/packager.config.json` (per `.gitignore` ausgeschlossen),
|
||||
Vorlage ohne Werte in [`packager.config.example.json`](../../deploy/packager.config.example.json).
|
||||
- **`ftpRemoteBaseDir` ist `/releases`**, nicht `/public_html/releases` wie in der
|
||||
Packager-Vorlage: Auf diesem Server liegt die Release-Ablage auf der FTP-Wurzel.
|
||||
- Der Packager veröffentlicht mit einem **Sub-Token**, das nur `updateservice:publish`
|
||||
trägt — gezogen über `/api/tokens/v1/provision`. Das Master-Token gehört nicht in eine
|
||||
Konfigurationsdatei.
|
||||
- **`deploy.py` ist dafür das falsche Werkzeug.** Es spiegelt den
|
||||
Deploymentcenter-Projektbaum in die FTP-Wurzel und hat mit dem Veröffentlichen eines
|
||||
Anwendungspakets nichts zu tun.
|
||||
- Die Versionsgegenprobe des Packagers greift und passt: `<Version>` aus
|
||||
[Directory.Build.props](../../Directory.Build.props) stimmt mit `clawddotnet.dll` überein.
|
||||
|
||||
Offen: `linux-x64` (erst nach der Avalonia-Portierung) und `prod`.
|
||||
|
||||
Clientseitig ist nichts zu tun: Das SDK schickt die Kennung des laufenden Systems von
|
||||
selbst.
|
||||
|
||||
### `preservePatterns` betrifft uns kaum
|
||||
|
||||
Unsere Konfiguration liegt seit der Linux-Portierung in `AppPaths.ConfigDirectory`
|
||||
(`%APPDATA%` bzw. XDG), **nicht** neben der Programmdatei. Ein Update kann sie also gar
|
||||
nicht überschreiben. Zu prüfen bleibt nur, dass keine leeren Arbeitsordner ins Paket
|
||||
wandern — die `CreateWorkingDirectories`-Targets in
|
||||
[ClawdDotNet.csproj](../../ClawdDotNet.csproj) legen `tools/`, `Logs/` und `Instances/`
|
||||
unter `OutputPath` an, und die sind mit AppPaths ohnehin überholt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Update anwenden — erledigt
|
||||
|
||||
Aus dem Hinweis ist eine Rückfrage geworden („Jetzt installieren" / „Später"), die den
|
||||
`update-agent` startet. Umgesetzt in
|
||||
[`DeploymentcenterService.StartUpdate`](../../src/ClawdDotNet.App/Services/DeploymentcenterService.cs)
|
||||
und `App.StartUpdateAsync`.
|
||||
|
||||
### Der Agent wird mitgeliefert — er muss es
|
||||
|
||||
Die Erstinstallation legt den Agenten **nicht** ins Zielverzeichnis: Sie läuft von dort,
|
||||
wo der Benutzer sie hingelegt hat. `ResolveAgentPath()` sucht ihn aber neben der
|
||||
Anwendung. Ohne Mitliefern fände die Anwendung nie einen Agenten und könnte sich nicht
|
||||
aktualisieren.
|
||||
|
||||
[`deploy/publish.py`](../../deploy/publish.py) holt das ausgelieferte Binary von
|
||||
`/installer/`, **prüft die SHA256 gegen `installer.json`** und legt es plattformrichtig
|
||||
ab (`update-agent.exe` bzw. `update-agent`). Bewusst das offizielle statt eines selbst
|
||||
gebauten: Es ist dasselbe, das die Erstinstallation verwendet, und wird zentral gepflegt.
|
||||
Ein ungeprüfter Download wäre ausgerechnet auf dem Pfad, der später fremden Code
|
||||
ausführt, die falsche Sparsamkeit.
|
||||
|
||||
Kosten: rund 28 MB im gepackten Paket (25 → 53 MB).
|
||||
|
||||
### Die Reihenfolge ist der eigentliche Inhalt
|
||||
|
||||
```
|
||||
1. AnnounceUpdate(version) → Watchdog meldet beim Beenden "maintenance"
|
||||
2. AppHost.DisposeAsync() → Datenbank, Scanner, Telegram, Abmeldung
|
||||
3. StartUpdate(...) → Agent starten, exitCurrentApp: false
|
||||
4. desktop.Shutdown() → wir beenden uns selbst
|
||||
```
|
||||
|
||||
Die Verlockung wäre, `LaunchUpdateAgent` das Beenden zu überlassen. Das tut es aber über
|
||||
`Environment.Exit` und übergeht damit Schritt 2 vollständig: keine Abmeldung, keine
|
||||
geschlossene Instanzdatenbank. Deshalb `exitCurrentApp: false` und
|
||||
`waitForCurrentProcess: true` — der Agent bekommt unsere Prozesskennung und wartet, bis
|
||||
wir wirklich weg sind, statt über gesperrte Dateien zu kopieren.
|
||||
|
||||
`maintenance` statt `stopped` ist kein Schönheitsfehler: `stopped` heißt „bewusst
|
||||
beendet" und lässt den Monitor liegen, bis jemand ihn anfasst. Beim Update kommt die
|
||||
Instanz aber wieder.
|
||||
|
||||
**Doppeltes Aufräumen** war die Falle dabei: Nach Schritt 2 ruft `desktop.Shutdown()` die
|
||||
Behandlung, die erneut aufräumt — und dabei den gerade gesetzten Wartungszustand mit
|
||||
einer zweiten Abmeldung überschrieben hätte. `AppHost.DisposeAsync` sperrt sich jetzt
|
||||
selbst gegen den zweiten Durchlauf.
|
||||
|
||||
---
|
||||
|
||||
## 4. Erstinstallation (2.3) — `setup.json` steht
|
||||
|
||||
Der Konfigurationsort war der Blocker: `setup.json`-Ziele waren „relativ zum
|
||||
Installationsverzeichnis", unsere Konfiguration liegt aber in `%APPDATA%` bzw.
|
||||
`$XDG_CONFIG_HOME` — weil `/opt/clawddotnet` unter Linux für den Dienstbenutzer nicht
|
||||
schreibbar ist ([Linux-Analyse](Linux-Portierung-Analyse.md)).
|
||||
|
||||
Das Deploymentcenter hat daraufhin `location` am Ziel ergänzt (`install`, `config`,
|
||||
`data`, `home`) samt Variablenersetzung in `file`. Damit ist der Weg frei;
|
||||
[`setup.json`](../../src/ClawdDotNet.Desktop/setup.json) liegt im Projekt und wird ins
|
||||
Ausgabeverzeichnis kopiert, landet also im Paket neben der `manifest.json`.
|
||||
|
||||
### Der Ordnername ist bewusst kleingeschrieben
|
||||
|
||||
`AppPaths` legt das Verzeichnis plattformabhängig unterschiedlich an:
|
||||
|
||||
| Plattform | Pfad |
|
||||
|---|---|
|
||||
| Windows | `%APPDATA%\ClawdDotNet` |
|
||||
| Linux | `$XDG_CONFIG_HOME/clawddotnet` (klein, Konvention) |
|
||||
|
||||
Eine `setup.json` kennt nur **eine** Schreibweise. `clawddotnet/Settings.json` trifft
|
||||
unter Linux exakt und unter Windows ebenfalls, weil NTFS Groß- und Kleinschreibung nicht
|
||||
unterscheidet. Andersherum ginge es nicht: `ClawdDotNet` wäre unter Linux ein zweites,
|
||||
leeres Verzeichnis neben dem, aus dem die Anwendung liest.
|
||||
|
||||
### Was dabei abfällt
|
||||
|
||||
Das Token stellt der Server aus (`source: "provision"`), die Server-Adresse kommt aus dem
|
||||
Installer (`detect:baseurl`). Damit entfällt der Absatz „bis die Avalonia-Einstellungs-
|
||||
ansicht steht, von Hand in `Settings.json`" aus der
|
||||
[Integrationsbeschreibung](../Deploymentcenter-Integration.md) — jedenfalls für frisch
|
||||
installierte Systeme.
|
||||
|
||||
Der Installer schreibt Lizenzschlüssel und Token **im Klartext**; er kennt unsere
|
||||
DPAPI-Hülle nicht. Das ist in Ordnung und abgesichert: `SecretProtector.Unprotect` gibt
|
||||
Klartext unverändert zurück, beim ersten Speichern wird verschlüsselt. Der Test dazu
|
||||
steht in `SecretProtectorTests` und nennt jetzt beide Gründe, damit ihn niemand als
|
||||
Altlast entfernt.
|
||||
|
||||
### Zwei Grenzen bleiben
|
||||
|
||||
- **`CLAWD_CONFIG_DIR` kennt der Installer nicht.** Wer den Ort per Umgebungsvariable
|
||||
verlegt, muss die Datei selbst verschieben.
|
||||
- **Wer installiert, entscheidet mit** (SETUP warnt selbst davor): `config` bezieht sich
|
||||
auf das Konto, unter dem der Installer läuft. Für einen systemd-Dienst mit eigenem
|
||||
Benutzer heißt das: als dieser Benutzer installieren, sonst landet die Konfiguration
|
||||
im falschen Profil.
|
||||
|
||||
### Durchgespielt (2026-08-15)
|
||||
|
||||
Anmeldung mit dem Installationskonto und der gesamte Ablauf gegen den echten Server:
|
||||
|
||||
| Schritt | Ergebnis |
|
||||
|---|---|
|
||||
| `POST /api/setup/v1/login` | 201, Rolle `installer`, Recht `setup:install`, Token 30 min gültig |
|
||||
| `GET /api/setup/v1/catalog?platform=win-x64` | `clawddotnet` (dev=0.1.2) erscheint. **Ohne `platform` leer** — wie die Update-Prüfung, der Agent schickt `PlatformId.Current` |
|
||||
| `POST /api/setup/v1/token` | Anwendungstoken mit genau `watchdog:ping` + `bugtracker:report` |
|
||||
| Rechteschranke | Das ausgestellte Token kann **kein** `updateservice:publish` nachziehen (403 `provision_denied`) |
|
||||
| `SetupPaths.Resolve` gegen unsere `setup.json` | löst unter Windows auf `%APPDATA%\ClawdDotNet\Settings.json` auf (`fileWindows` greift) |
|
||||
| **Round-Trip** SDK schreibt → `SettingsManager` liest | trägt: camelCase-Keys treffen, der Klartext-Lizenzschlüssel geht durch den Entschlüsselungspfad (der Klartext unverändert durchreicht) |
|
||||
|
||||
Damit ist der Weg vollständig: Ein frisch aufgesetztes System bekommt über den Installer
|
||||
Server-Adresse, Lizenzschlüssel und ein vom Server ausgestelltes Instanz-Token in die
|
||||
`Settings.json` geschrieben, die ClawdDotNet dann ohne Zutun lädt.
|
||||
|
||||
### Befund am Rande: alte Felder bleiben stehen
|
||||
|
||||
Der `SetupWriter` merged in eine vorhandene `Settings.json`, statt sie zu ersetzen —
|
||||
richtig so, sonst gingen Logging-Einstellungen und Ähnliches verloren. Auf einem System
|
||||
mit einer **alten** Datei bleiben dabei Felder stehen, die es in der aktuellen
|
||||
`AppSettings` nicht mehr gibt (`watchdogServerUrl`, `licensePublicKeyBase64` aus der
|
||||
LicenseLabrador-Zeit). Harmlos — `SettingsManager` ignoriert unbekannte Felder beim
|
||||
Laden —, aber tote Einträge in der Datei. Kein Handlungsbedarf; beim ersten `Save` der
|
||||
laufenden App verschwinden sie.
|
||||
|
||||
**Nicht enthalten** (SETUP §7): systemd-Unit und Windows-Dienst legt der Installer nicht
|
||||
an. Für den kopflosen Betrieb bleibt das unsere Aufgabe.
|
||||
|
||||
---
|
||||
|
||||
## 4a. Der Update-Weg ist durchgespielt
|
||||
|
||||
Am 2026-08-14 gegen den echten Server geprüft, nicht nur gebaut. Ausgangslage: das
|
||||
0.1.0-Paket mit Lizenzschlüssel geladen und entpackt — also eine Installation, wie sie
|
||||
beim Kunden aussieht — plus eine selbst angelegte Datei, die in keinem Manifest steht.
|
||||
|
||||
| Fall | Ergebnis |
|
||||
|---|---|
|
||||
| **0.1.0 → 0.1.1** | RC 0. SHA256 des Pakets und 117 Manifest-Hashes geprüft. Fremde Datei unangetastet, `setup.json` da, `update-agent.exe` neu im Ziel |
|
||||
| **Rücksprung 0.1.1 → 0.1.0** | RC 0. `update-agent.exe` als nicht mehr zum Release gehörig **entfernt** — und nur die, die fremde Datei blieb liegen |
|
||||
| **Ohne Lizenzschlüssel** | `UNAUTHORIZED: … Erwartet wird der Lizenzschluessel dieser Installation`, RC 2. Sauber von einem Netzfehler unterschieden |
|
||||
| **Abbruch mitten im Schreiben** (Datei exklusiv gesperrt) | RC 1, „Vorheriger Stand wurde wiederhergestellt". Version, Dateizahl und Inhalt unverändert — die Installation blieb lauffähig |
|
||||
|
||||
Damit trägt die Zusage aus §4B des UpdateService-Handbuchs: Ein Abbruch hinterlässt keine
|
||||
halbe Installation, und verwaiste Dateien werden aufgeräumt, ohne fremde anzufassen.
|
||||
|
||||
Zwei Kleinigkeiten am Rand:
|
||||
|
||||
- Nach dem gescheiterten Lauf blieb ein **leeres** `.dc-update-backup/` zurück. Kein
|
||||
Speicherverlust — der Rollback hatte alles zurückgeholt —, und der nächste erfolgreiche
|
||||
Lauf hat es entfernt. Ein leeres Verzeichnis dieses Namens sieht für einen Betreiber
|
||||
aber nach „Update hängt" aus.
|
||||
- Der Agent weist bei **jedem** Lauf auf das unsignierte Release hin. Das ist richtig so
|
||||
und wird erst still, wenn §5 erledigt ist.
|
||||
|
||||
---
|
||||
|
||||
## 5. Signierte Releases (2.2) — Schlüssel steht, Prüfung getestet
|
||||
|
||||
Der Signierschlüssel ist seit dem 2026-08-14 serverseitig hinterlegt (RSA-SHA256,
|
||||
`canonical-line-v1`). **0.1.2 ist das erste signierte Release**; 0.1.0 und 0.1.1 bleiben
|
||||
unsigniert, weil serverseitig beim Veröffentlichen signiert wird.
|
||||
|
||||
Am Testsystem durchgespielt:
|
||||
|
||||
| Fall | Ergebnis |
|
||||
|---|---|
|
||||
| Signiertes 0.1.2 mit `--require-signature` | RC 0, kein Unsigniert-Hinweis mehr |
|
||||
| Unsigniertes 0.1.1 mit `--require-signature` | **RC 1, Abbruch vor dem Herunterladen** |
|
||||
| Unsigniertes 0.1.1 ohne die Pflicht | RC 0 mit Hinweis — wie dokumentiert |
|
||||
|
||||
Der öffentliche Schlüssel wird beim ersten Lauf geholt und als
|
||||
`dc-release-pubkey.pem` neben dem Agenten festgehalten. Ein später abweichender Schlüssel
|
||||
fällt damit auf.
|
||||
|
||||
### Offen: die Pflicht ist aus der Anwendung heraus nicht erreichbar
|
||||
|
||||
`--require-signature` gibt es **nur als Kommandozeilenschalter**.
|
||||
`UpdateClient.LaunchUpdateAgent` — der vom Handbuch empfohlene Weg, den auch wir
|
||||
benutzen — hat dafür keinen Parameter, und der Agent liest keine Umgebungsvariable dafür
|
||||
(`Program.cs:66` liest ausschließlich `HasFlag(args, "--require-signature")`).
|
||||
|
||||
Damit läuft jede Anwendung, die den empfohlenen Weg geht, ohne Signaturprüfung, während
|
||||
derselbe Vorgang von Hand auf der Kommandozeile geschützt wäre. Wir könnten den Start
|
||||
selbst nachbauen — dann verlieren wir aber `--restart`, `--wait-for-pid` und
|
||||
`--wait-timeout`, also genau die Handgriffe, für die es die Hilfsmethode gibt.
|
||||
|
||||
**Gemeldet.** Sobald `LaunchUpdateAgent` einen Parameter dafür hat, setzen wir ihn:
|
||||
Alle unsere Releases ab 0.1.2 sind signiert, ein Rückschritt auf unsignierte Stände wäre
|
||||
danach kein Verlust.
|
||||
|
||||
---
|
||||
|
||||
## 6. Reihenfolge
|
||||
|
||||
1. **Zugangsschutz** (§1) — erledigt, muss im ersten Release drin sein.
|
||||
2. **`setup.json`** (§4) — erledigt, wird mit dem ersten Paket ausgeliefert.
|
||||
3. **Release-Strecke** (§2): `packager.config.json`, erster Testlauf nach `dev`.
|
||||
4. **Update anwenden** (§3): Agent mitliefern, `maintenance` beim Update melden.
|
||||
5. **Signatur scharf** (§5), sobald der Serverschlüssel steht.
|
||||
|
||||
Schritt 3 ist die Voraussetzung für alles Weitere: Solange kein Release veröffentlicht
|
||||
ist, lässt sich weder Update noch Erstinstallation erproben — und die `setup.json` wirkt
|
||||
erst, wenn sie in einem Paket steckt.
|
||||
|
||||
---
|
||||
|
||||
## 7. Befunde vom 2026-08-13 — alle behoben
|
||||
|
||||
Zur Nachvollziehbarkeit, weil einige unsere Umsetzung geformt haben:
|
||||
|
||||
| Befund | Behoben durch |
|
||||
|---|---|
|
||||
| `.htpasswd` enthielt alle Lizenzschlüssel im Klartext (Benutzernamenspalte wird nicht gehasht) | `ReleaseGuard::licenseUsername()` leitet `lic_<sha256[0..16]>` ab; `ReleaseCredentials.UsernameForLicenseKey` bildet dieselbe Ableitung nach. Die Datei enthält jetzt nur noch bcrypt über einen hochentropen Schlüssel |
|
||||
| Doku beschrieb Nginx, der Schutz greift nur unter Apache; WebUI meldete „GESCHÜTZT" allein anhand vorhandener Dateien | Echter HTTP-Selbsttest (`ReleaseGuard::selfTest`, erwartet 401), Warnhinweis und eigener Nginx-Abschnitt in der Doku |
|
||||
| UPDATESERVICE §7 dokumentierte `{"status":"ok"}`, der Code liefert `"success"` | Doku berichtigt |
|
||||
| `setup.json` schrieb nur ins Installationsverzeichnis | `location`-Angabe am Ziel plus Variablenersetzung in `file` |
|
||||
|
||||
### Offen aus dem ersten Release (2026-08-13)
|
||||
|
||||
| Befund | Wirkung |
|
||||
|---|---|
|
||||
| **Veröffentlichen löst `ReleaseGuard` nicht aus.** `regenerateForProject` läuft nur bei Lizenzänderungen, Projektlöschung, Kontoänderungen und im Sechs-Stunden-Turnus von `cli/tick.php`. Ein Produktverzeichnis entsteht aber erst beim ersten Upload | `/releases/clawddotnet/` war nach dem Upload **ohne `.htaccess`** — das frische Paket bis zum nächsten Turnuslauf für jeden ladbar. Der Turnus hat es inzwischen geschlossen (401 bestätigt). Ein Aufruf am Ende von `/api/updateservice/v1/publish` würde das Fenster ganz vermeiden; das Verzeichnis existiert dort bereits |
|
||||
| **Die Prüfvorschrift aus UPGRADE §16.4 meldet falsch grün.** `curl -I …/.htpasswd → 403` trifft auch dann zu, wenn die Datei gar nicht existiert: Apache sperrt `.ht*` global | Wir hatten 403 auf `.htpasswd` **und** 200 auf `package.tar.gz`. Aussagekräftig ist nur der Paket-Abruf ohne Zugangsdaten |
|
||||
| **Kein Signierschlüssel auf dem Server.** `security.release_private_key` ist nicht gesetzt (UPGRADE §15.2) | Releases sind unsigniert, der Agent kann die Herkunft nicht prüfen. `--require-signature` ist damit unbenutzbar |
|
||||
|
||||
Die Ableitung des Benutzernamens muss auf beiden Seiten zeichengenau übereinstimmen —
|
||||
`lic_` plus die ersten 16 Hexzeichen des SHA-256 über den getrimmten Schlüssel. Wer eine
|
||||
Seite ändert, sperrt die gesamte Installationsbasis aus.
|
||||
@@ -0,0 +1,308 @@
|
||||
# Deploymentcenter-Anbindung — Durchsicht
|
||||
|
||||
> **Nachtrag 2026-08-08 — die Anbindung ist umgestellt, Server und SDK stehen auf 2.1.**
|
||||
> Abschnitt 4 und 5 sind abgearbeitet; wie es jetzt aussieht, steht in
|
||||
> [Deploymentcenter-Integration](../Deploymentcenter-Integration.md).
|
||||
>
|
||||
> Mit **SDK 2.1 erledigt** (waren Befunde aus Abschnitt 3 bzw. aus der Durchsicht der
|
||||
> 2.0-Anbindung):
|
||||
>
|
||||
> - `HttpClient` ohne Zeitgrenze → intern 15 s. Unsere Umgehung (eigener Client mit
|
||||
> 8 s) ist zurückgebaut.
|
||||
> - HTTP 429/5xx entzogen die Lizenz, ohne den Zwischenspeicher zu befragen → jeder
|
||||
> Nicht-Erfolg führt jetzt in denselben Offline-Zweig, `IsTransient` macht den
|
||||
> Unterschied sichtbar. Unsere Behelfsprüfung auf `unknown_error` ist entfernt.
|
||||
> - `cache_ttl_hours` wurde ignoriert, die Gnadenfrist war faktisch unbegrenzt.
|
||||
> - `app_version` fest `"1.0.0"` → kommt jetzt aus `ReleaseInfo.Version`.
|
||||
> - `BuildInfo.targets` war nicht einbindbar (CS0433/CS0103) → erzeugt die Klasse im
|
||||
> eigenen Namensraum, ist eingebunden.
|
||||
> - `UpdateClient`: API-Zweig las snake_case in ein camelCase-Modell → eigenes Modell
|
||||
> `ApiReleaseInfo`, `is_critical` von der obersten Ebene.
|
||||
> - `DeactivateAsync` schickte den Shared Key zusätzlich als `X-Watchdog-Key`.
|
||||
> - Kein `CancellationToken` in der Lizenz-API.
|
||||
>
|
||||
> **Weiterhin offen** — betrifft das Deploymentcenter, nicht ClawdDotNet:
|
||||
>
|
||||
> - **2.1 (keine Signaturprüfung)** — unverändert. `LicenseInfo.PublicKeyBase64` ist
|
||||
> gestrichen, damit nichts Totes stehenbleibt und niemand Schutz vermutet, wo keiner
|
||||
> ist. Kommt die Signatur, kommt das Feld mit ihr zurück.
|
||||
> - **2.2 (v1-Ersatzhash)** und **2.3 (Klartext-Rückfall)** — unverändert, beides im
|
||||
> SDK zu beheben.
|
||||
> - **3 (HW-ID bei jedem Aufruf neu)** — clientseitig umgangen: einmal berechnet und
|
||||
> behalten.
|
||||
> - **`parent_source` ist nur eine `source`, kein Paar** — damit schließen sich „ein
|
||||
> Monitor je Instanz" und instanzweise Alarmunterdrückung gegenseitig aus.
|
||||
|
||||
Stand: 2026-08-06. Geprüft: `J:\Softwareprojekte\Deploymentcenter` (Client, Server,
|
||||
Schema, beide Integrationsleitfäden) gegen den
|
||||
[HW-ID-v2-Vorschlag](Lizenz-HardwareId-v2-Implementierungsvorschlag.md) und die
|
||||
[Linux-Analyse](Linux-Portierung-Analyse.md).
|
||||
|
||||
**Ergebnis vorweg: Die Lizenz blockiert den Linux-Umzug nicht mehr.** Alles, was
|
||||
an Hardware-ID v2 plattformrelevant war, ist da und richtig. Was hier steht, sind
|
||||
Punkte aus derselben Durchsicht — drei davon würden beim Ausrollen wehtun.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was erledigt ist
|
||||
|
||||
| Punkt aus dem Vorschlag | Umsetzung |
|
||||
|---|---|
|
||||
| Format `2:<plattform>:<hex>` | [HardwareId.cs:125](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs) |
|
||||
| **Kein `MachineName` im Hash** | `ComputeV2Hash`, `:138` — der wichtigste Punkt, sauber umgesetzt |
|
||||
| Quellenkette Windows/Linux | `:42–107`, inklusive `dmi-uuid` |
|
||||
| `IsPlausibleMachineId` (Länge, `uninitialized`, nur Nullen) | `:161` |
|
||||
| MAC-Filter über locally-administered-Bit | `:224` |
|
||||
| `/sys/class/net/<name>/device`-Prüfung | `:228` |
|
||||
| Erweiterte Stoppwortliste | `:23` — inkl. `br-`, `virbr`, `cni`, `cali` |
|
||||
| `machine.key` mit `0600` | `:290`, `SetUnixPermissions` mit `#if NET8_0_OR_GREATER` |
|
||||
| Vorgabe per Umgebungsvariable | `LicenseConfig.HardwareIdOverride`, beide Namen |
|
||||
| XDG-Auflösungskette, nie leerer Pfad | [LicenseConfig.cs:28](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseConfig.cs), mit `ValidateNonEmpty` |
|
||||
| Mehrfachziel `netstandard2.0;net8.0` | csproj, BouncyCastle nur im netstandard-Zweig |
|
||||
| `LLS2`-Hülle, AES-GCM, HKDF | [StateStore.cs:169](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) — Schlüssel aus HW-ID abgeleitet, bindet den Cache also echt an die Maschine |
|
||||
| `ILicensePrompt` + Konsolenfassung | vorhanden — genau das, was der kopflose Host braucht |
|
||||
| Servermigration v1→v2 | [LicenseService.php:108](../../../Deploymentcenter/src/Modules/License/LicenseService.php), mit Prüfprotokolleintrag `hwid_migrated` |
|
||||
| Schema `hwid_version`/`hwid_source`/`platform` | `sql/migrations/v2_hardware_id.sql`, rückwärtskompatibel |
|
||||
| Verwaltungsansicht zeigt Quelle/Plattform | `public/index.php:1227` |
|
||||
|
||||
`OperatingSystemHelpers` nutzt jetzt `RuntimeInformation`. Der Client hat auf dem
|
||||
Linux-Pfad keine Windows-Laufzeitabhängigkeit — `ProtectedData` wird nur unter
|
||||
`IsWindows()` aufgerufen.
|
||||
|
||||
**Für die Portierung heißt das:** Punkt 4 aus der Entscheidungsliste der
|
||||
Linux-Analyse („Erlaubt LicenseLabrador den Wechsel der Hardware-ID?") ist
|
||||
beantwortet. Der Aufwandsblock „Lizenz" schrumpft von 3–5 PT auf **2–3 PT** —
|
||||
das ist jetzt reine Anschlussarbeit in ClawdDotNet, keine Konzeptarbeit mehr.
|
||||
|
||||
---
|
||||
|
||||
## 2. Drei Befunde, die vor dem Ausrollen geklärt sein sollten
|
||||
|
||||
### 2.1 Es wird nichts signiert — die Lizenzprüfung ist eine Vertrauensfrage an DNS
|
||||
|
||||
[LicenseClient.cs:62](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs):
|
||||
|
||||
```csharp
|
||||
string status = root.TryGetProperty("status", out var sProp) ? sProp.GetString() ?? "unknown" : "unknown";
|
||||
if (status.Equals("valid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// → gültig
|
||||
}
|
||||
```
|
||||
|
||||
Das ist die vollständige Prüfung. Es gibt im neuen Client **kein `Signature.cs`,
|
||||
keinen hinterlegten öffentlichen Schlüssel, keine Hüllenprüfung** — die Dateien
|
||||
`Signature.cs`, `LicenseResult.cs` und `LicenseState.cs` aus dem alten
|
||||
LicenseLabrador-SDK sind beim Umzug nicht mitgekommen.
|
||||
|
||||
Folge: Wer die HTTP-Anfrage umlenken kann, hat eine gültige Lizenz. Ein Eintrag
|
||||
in `/etc/hosts`, ein Proxy, ein eigener DNS — die Antwort `{"status":"valid"}`
|
||||
genügt. Auf einem Linux-Server, den der Betreiber ohnehin vollständig
|
||||
kontrolliert, ist das kein Kunststück.
|
||||
|
||||
Serverseitig sieht es passend dazu aus. `public/index.php:45`:
|
||||
|
||||
```php
|
||||
'signature' => 'ED25519_SIG_' . base64_encode(hash('sha256', $lic['license_key'] . 'DC_OFFLINE_SECRET', true))
|
||||
```
|
||||
|
||||
Das ist ein SHA-256 über den Lizenzschlüssel plus eine fest verdrahtete
|
||||
Zeichenkette — keine Signatur, sondern ein Wert, der jeder erzeugen kann, der den
|
||||
Quelltext kennt. Und `public/index.php:1993` im JavaScript:
|
||||
|
||||
```javascript
|
||||
"ED25519_SIG_" + btoa(key + hwId).substring(0, 32)
|
||||
```
|
||||
|
||||
Base64 der Eingabe, abgeschnitten. Auch kein Hash.
|
||||
|
||||
Das ist erkennbar ein Platzhalter — nur trägt er einen Namen, der nach fertigem
|
||||
Verfahren klingt, und darauf verlässt sich [LicenseGate](Services/LicenseGate.cs)
|
||||
mit seiner harten Startsperre. **Es ist keine Portierungsfrage** (unter Windows
|
||||
gilt heute dasselbe) und auch kein Grund, den Linux-Umzug aufzuhalten — aber es
|
||||
sollte eine bewusste Entscheidung sein und nicht in dem Glauben untergehen, die
|
||||
Signaturprüfung sei bereits da.
|
||||
|
||||
Wenn das Verfahren zurückkommen soll: Ed25519 über die kanonisch serialisierte
|
||||
Antwort, öffentlicher Schlüssel im Client einkompiliert, `nonce` aus der Anfrage
|
||||
in der signierten Nutzlast gegenprüfen (gegen Wiedereinspielung). Der alte
|
||||
`Signer.php` und `Signature.cs` sind im LicenseLabrador-Repo noch vorhanden und
|
||||
lassen sich als Vorlage nehmen.
|
||||
|
||||
### 2.2 Der v1-Ersatzhash trifft die alten Aktivierungen nicht
|
||||
|
||||
Der Migrationsweg ist auf beiden Seiten korrekt gebaut — er wird nur nie
|
||||
auslösen, weil der Client eine andere v1-ID berechnet als die, die in der
|
||||
Datenbank steht.
|
||||
|
||||
Alt ([LicenseLabrador/HardwareId.cs:20](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)):
|
||||
|
||||
```csharp
|
||||
rawBuilder.Append(machineId); // MachineGuid, sonst MAC
|
||||
rawBuilder.Append(Environment.MachineName); // direkt angehängt, kein Trenner
|
||||
→ sha256(machineGuid + machineName)
|
||||
```
|
||||
|
||||
Neu ([HardwareId.cs:149](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs)):
|
||||
|
||||
```csharp
|
||||
string raw = $"{Environment.MachineName}:{firstMac}";
|
||||
→ sha256(machineName + ":" + mac)
|
||||
```
|
||||
|
||||
Andere Reihenfolge, anderer Trenner, und **MAC statt MachineGuid**. Auf jedem
|
||||
Windows-Rechner, auf dem `MachineGuid` lesbar war — also praktisch allen —
|
||||
stimmen die Hashes nicht überein. Der Server sucht die Altaktivierung, findet
|
||||
nichts und legt eine neue an: **genau der Platzverbrauch, den die Migration
|
||||
verhindern sollte.** Bei `max_activations = 2` ist danach ein Platz für den
|
||||
Linux-Server weniger da.
|
||||
|
||||
Auch `GetFirstPhysicalMacLegacy` (`:254`) weicht ab: keine Stoppwortfilterung,
|
||||
keine Sortierung, erste Schnittstelle in Aufzählungsreihenfolge. Die alte
|
||||
Fassung nahm die alphabetisch erste *gefilterte* MAC.
|
||||
|
||||
Zu tun: `GetLegacyHardwareId()` muss den v1-Algorithmus zeichengenau
|
||||
nachbilden — inklusive der alten Stichwortliste (`virtual`, `veth`, `docker`,
|
||||
`hyper-v`, `wsl`, `mullvad`, `wireguard`, `tap`, `tun`, `vpn`, `bluetooth`,
|
||||
`vmware`, `box`, `pseudo`, `loopback`, `npcap`, `pcap`), `OrderBy(…, Ordinal)`
|
||||
und `FirstOrDefault()`. Der Code steht im LicenseLabrador-Repo noch da und kann
|
||||
weitgehend übernommen werden.
|
||||
|
||||
Am besten mit einem Test absichern, der einen bekannten Eingabewert gegen den
|
||||
erwarteten v1-Hash prüft — sonst fällt eine Abweichung erst auf, wenn die
|
||||
Aktivierungsplätze schon verbraucht sind.
|
||||
|
||||
### 2.3 Der Klartext-Rückfall ist noch da, nur woanders
|
||||
|
||||
[StateStore.cs:79](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) —
|
||||
„Legacy Migration Check":
|
||||
|
||||
```csharp
|
||||
string legacyJson = Encoding.UTF8.GetString(payloadBytes);
|
||||
var legacyData = JsonSerializer.Deserialize<LocalCacheData>(legacyJson);
|
||||
if (legacyData != null)
|
||||
{
|
||||
legacyData.SchemaVersion = 2;
|
||||
Save(productSlug, hardwareId, legacyData);
|
||||
return legacyData;
|
||||
}
|
||||
```
|
||||
|
||||
Der LLS2-Zweig darüber ist genau richtig — Entschlüsselung fehlgeschlagen heißt
|
||||
Cache-Fehltreffer, kein Klartext. Der Zweig darunter hebt das wieder auf: Jede
|
||||
Datei ohne `LLS2`-Kennung wird als JSON gelesen und, wenn sie sich deserialisieren
|
||||
lässt, **übernommen und anschließend verschlüsselt neu geschrieben**.
|
||||
|
||||
Durchgespielt: Eine von Hand angelegte `state.dat` mit
|
||||
|
||||
```json
|
||||
{"SchemaVersion":2,"Status":"valid","ExpiresAt":99999999999,"MaxSeenTime":0}
|
||||
```
|
||||
|
||||
wird angenommen. In `ValidateAsync` greift bei fehlender Verbindung der
|
||||
Cache-Zweig (`:110`): `Status == "valid"` ✓, `now < MaxSeenTime` ✗, `now >
|
||||
ExpiresAt` ✗ → **`IsValid = true`**. Die Bindung an die Hardware, die
|
||||
`DeriveKey(hardwareId, …)` sonst herstellt, ist auf diesem Weg umgangen; die
|
||||
Datei ist zwischen Maschinen übertragbar.
|
||||
|
||||
Der Zweig hilft dabei nicht einmal beim eigentlichen Zweck. Die alte
|
||||
`LocalCacheData` hieß `last_envelope`, `max_seen_time`, `endpoints`,
|
||||
`last_license_key`; die neue `SchemaVersion`, `Status`, `ExpiresAt`, … Kein
|
||||
gemeinsames Feld, und `JsonSerializer` ist ohne
|
||||
`PropertyNameCaseInsensitive`/`JsonPropertyName` bei den Namen streng. Eine echte
|
||||
v1-Datei ergibt also ein Objekt mit lauter Vorgabewerten (`Status = "invalid"`)
|
||||
und ist als Cache wertlos.
|
||||
|
||||
**Empfehlung: den Zweig ersatzlos streichen.** Er kostet Sicherheit und leistet
|
||||
nichts. Alte Cachedateien sollen verworfen werden — eine einmalige
|
||||
Online-Prüfung ist der ganze Preis.
|
||||
|
||||
Nebenbei: `Checksum = hwInfo.HardwareId` ([LicenseClient.cs:79](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs))
|
||||
ist keine Prüfsumme, sondern eine Kopie der HW-ID. Das Feld ist damit ohne
|
||||
Funktion — entweder mit einem HMAC über die übrigen Felder füllen oder entfernen,
|
||||
damit niemand später Schutz vermutet, wo keiner ist.
|
||||
|
||||
---
|
||||
|
||||
## 3. Kleinere Punkte
|
||||
|
||||
| Fundstelle | Sache |
|
||||
|---|---|
|
||||
| [LicenseClient.cs:26](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs) | Eigener `HttpClient` je Instanz, nie freigegeben, **ohne Zeitgrenze** (Vorgabe 100 s). Der alte `LicenseConfig.HttpTimeout` war 6 s. In `LicenseGate.RunStartupCheck` bedeutet das bis zu 100 s Standbild beim Start, wenn der Server nicht antwortet. |
|
||||
| `:107` | `catch (Exception ex)` um den gesamten Block: Auch ein Fehler beim Auswerten einer *erfolgreichen* Antwort landet im Offline-Zweig. Ein defekter Server gilt dann als „offline". |
|
||||
| `:47` | `app_version = "1.0.0"` fest verdrahtet. ClawdDotNet hat `BuildInfo.Build` — sollte Parameter sein, sonst steht in der Verwaltungsansicht bei jeder Instanz dasselbe. |
|
||||
| `:32`, `:172` | `HardwareId.GetHardwareId()` bei jedem Aufruf neu: liest unter Linux Dateien und zählt Netzwerkschnittstellen auf. Einmal berechnen und halten. |
|
||||
| `HardwareId.cs:205` | MAC-Auswahl überspringt Schnittstellen, die nicht `Up` oder `Unknown` sind. Ein Kabel, das beim Start nicht steckt, ändert damit die Hardware-ID. Für die Ausweichlösung sollte der Betriebszustand keine Rolle spielen — sonst ist sie genau in dem Moment instabil, in dem sie gebraucht wird. |
|
||||
| `HardwareId.cs:231` | `/sys/class/net/<name>/device` ist ein Symlink. `Directory.Exists`/`File.Exists` folgen ihm — funktioniert, ist aber Zufall und sollte kommentiert sein. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Watchdog: die Anbindung passt noch nicht
|
||||
|
||||
Kein Linux-Thema, fällt aber in dieselbe Umbauarbeit.
|
||||
|
||||
[WatchdogClient.cs](src/ClawdDotNet.Core/Watchdog/WatchdogClient.cs) sendet an:
|
||||
|
||||
| ClawdDotNet | Deploymentcenter |
|
||||
|---|---|
|
||||
| `POST /api/heartbeat` | `POST /api/watchdog/v1/ping` (nimmt auch `/heartbeat`) |
|
||||
| `POST /api/event` | `POST /api/watchdog/v1/event` |
|
||||
| `POST /api/register` | **existiert nicht** |
|
||||
|
||||
Die Pfade sind also alle um `/watchdog/v1` zu ergänzen. Der Kopfzeilenname passt:
|
||||
`public/api/watchdog/v1/index.php:30` akzeptiert `X-Watchdog-Key`,
|
||||
`Authorization` und `X-Agent-Token`.
|
||||
|
||||
Der Selbstregistrierungsweg aus [Program.cs:334](Program.cs:334) — mit dem
|
||||
Master-Token einen eigenen Agent-Token holen und in der Instanzkonfiguration
|
||||
zwischenspeichern — hat serverseitig kein Gegenstück mehr. Zu klären: Tokens
|
||||
künftig von Hand in der Verwaltung anlegen und in die Instanzkonfiguration
|
||||
eintragen, oder `/register` im Deploymentcenter nachziehen. Für den ersten Weg
|
||||
spricht, dass er den Master-Token gar nicht erst auf die Instanzen verteilt.
|
||||
|
||||
Die Feldnamen des Ping-Rumpfs (`source`, `instance`, `type`, `status`, `message`,
|
||||
`interval`, `group`, `os`) sind gegen
|
||||
[InstanceHealthProvider](src/ClawdDotNet.Core/Watchdog/InstanceHealthProvider.cs)
|
||||
abzugleichen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Was in ClawdDotNet zu tun ist
|
||||
|
||||
| Datei | Was |
|
||||
|---|---|
|
||||
| [ClawdDotNet.csproj](ClawdDotNet.csproj) | Projektverweis von `..\LicenseLabrador\client-dotnet\…` auf `..\Deploymentcenter\client-dotnet\Deploymentcenter.Client\…` umhängen. Langfristig als Submodul unter `external/` — der Kommentar dazu steht schon im csproj. |
|
||||
| [Services/LicenseGate.cs](Services/LicenseGate.cs) | Neu gegen `LicenseValidationResult` schreiben. `LicenseState` gibt es nicht mehr, `Status` ist jetzt eine Zeichenkette — `DescribeProblem` (`:125`) muss auf `revoked`/`expired`/`activation_limit`/`not_found` umgestellt werden. `MessageBox` durch `ILicensePrompt` ersetzen; die Konsolenfassung bringt der Client mit. |
|
||||
| [Services/LicenseInfo.cs](Services/LicenseInfo.cs) | `PublicKeyBase64` hat ohne Signaturprüfung keine Funktion mehr — entweder mit 2.1 zurückholen oder streichen, damit nichts Totes stehenbleibt. |
|
||||
| [Program.cs:112](Program.cs:112) | Lizenzprüfung so verlagern, dass sie ohne Fenster auskommt (kopfloser Host). |
|
||||
| Host (neu) | `--license-status`, `--license-set-key`, `--license-deactivate` — der Client bringt alles Nötige mit. |
|
||||
| [WatchdogClient.cs](src/ClawdDotNet.Core/Watchdog/WatchdogClient.cs) | Pfade auf `/api/watchdog/v1/…`; Registrierungsweg klären (Abschnitt 4). |
|
||||
| `docs/Integrationsplan-WatchDog-LicenseLabrador.md` | Abgelöst durch [Deploymentcenter-Integration](../Deploymentcenter-Integration.md). |
|
||||
|
||||
---
|
||||
|
||||
## 6. Antwort auf die Ausgangsfrage
|
||||
|
||||
**Ja — Avalonia und Linux sind damit machbar.** Die einzige Frage, die ich als
|
||||
möglicher Blocker außerhalb unserer Hand markiert hatte, ist geklärt: Der Client
|
||||
läuft auf beiden Plattformen, zielt auf `net8.0` (von net10.0 problemlos
|
||||
verwendbar), löst seinen Ablageort auch ohne `HOME` auf, und die HW-ID ist
|
||||
container- und umbenennungsfest.
|
||||
|
||||
Der Lizenzblock in der Aufwandsschätzung fällt von 3–5 PT auf **2–3 PT**. Die
|
||||
Gesamtspanne bleibt bei **50–80 PT**, weil die Lizenz nie der große Posten war —
|
||||
das sind PropertyGrid und Chat-Ansicht.
|
||||
|
||||
Zwei Dinge sollten aber vor dem Ausrollen erledigt sein, unabhängig von Linux:
|
||||
|
||||
- **2.2 (v1-Ersatzhash)** — klein, aber wenn es beim Ausrollen falsch ist, sind
|
||||
Aktivierungsplätze verbraucht und man bekommt sie nur einzeln über die
|
||||
Verwaltung zurück. Das ist der Punkt mit dem schlechtesten Verhältnis von
|
||||
Aufwand zu Schaden.
|
||||
- **2.3 (Klartext-Rückfall)** — eine Zeile weniger Code, dafür wieder das
|
||||
Verhalten, das der `LLS2`-Umbau eigentlich herstellen sollte.
|
||||
|
||||
**2.1 (keine Signaturprüfung)** ist eine eigene Entscheidung mit eigenem Umfang
|
||||
und hält den Umzug nicht auf. Sie sollte nur getroffen und nicht übersehen
|
||||
werden — der Name `ED25519_SIG_` im Serverquelltext legt sonst nahe, dass die
|
||||
Sache erledigt sei.
|
||||
@@ -0,0 +1,296 @@
|
||||
# Drei Konzepte: Backup, Finanzumfeld, Leistungsanalyse
|
||||
|
||||
Diskussionsgrundlage, noch nicht umgesetzt.
|
||||
|
||||
---
|
||||
|
||||
# 1. Backup und Wiederherstellung
|
||||
|
||||
## Was überhaupt schützenswert ist
|
||||
|
||||
Nicht alles im Instanzverzeichnis ist gleich wertvoll. Entscheidend ist, was sich
|
||||
**nicht** wiederherstellen lässt:
|
||||
|
||||
| Was | Wert | Bemerkung |
|
||||
|---|---|---|
|
||||
| `Identity.md`, `Soul.md` | **hoch** | Die eigentliche Arbeit an einem Agenten |
|
||||
| `AgentSettings.json`, `InstanceSettings.json` | **hoch** | Tool-Zuweisungen, Budgets, Zugangsdaten |
|
||||
| `state.db` → Tabelle `Memories` | **hoch** | Das Langzeitgedächtnis — über Monate gewachsen |
|
||||
| `Workspace/`, `SharedWorkspace/` | hoch | Berichte, Wissensdatenbank |
|
||||
| `ChatHistory.json`, `ChatContext.json` | mittel | Laufender Arbeitsstand |
|
||||
| `state.db` → `RunUsage` | mittel | Kostenhistorie, Grundlage der Auswertung |
|
||||
| Telegram-Session | **hoch** | Ohne sie ist ein erneuter Login mit Code nötig |
|
||||
| `Logs/` | gering | Nachvollziehbarkeit, groß |
|
||||
| `bin/` | keiner | Wird gebaut |
|
||||
|
||||
## Problem 1: Verschlüsselte Zugangsdaten überleben den Rechner nicht ⚠️
|
||||
|
||||
Das ist eine direkte Folge von S7 und der wichtigste Punkt hier.
|
||||
|
||||
DPAPI verschlüsselt im Benutzerkontext — entschlüsseln kann nur derselbe
|
||||
Windows-Benutzer auf demselben Rechner. Ein Backup, das genau dann gebraucht wird,
|
||||
wenn der Rechner defekt ist, enthält damit **unbrauchbare Zugangsdaten**.
|
||||
|
||||
Ein Backup, das sich nicht auf einem anderen Rechner wiederherstellen lässt, erfüllt
|
||||
seinen Zweck nicht.
|
||||
|
||||
**Lösung:** Beim Backup werden Secrets umgeschlüsselt — von DPAPI auf eine
|
||||
Passphrase (PBKDF2 zur Schlüsselableitung, AES-GCM zur Verschlüsselung). Beim
|
||||
Wiederherstellen wird die Passphrase abgefragt und auf DPAPI des Zielrechners
|
||||
zurückgeschlüsselt.
|
||||
|
||||
Alternativ als bewusste Option: **Backup ohne Zugangsdaten**. Dann ist der Restore
|
||||
unvollständig, aber die Datei ist gefahrlos ablegbar — auch auf einem NAS oder in
|
||||
einer Cloud. Beide Varianten sollten anwählbar sein, mit deutlicher Kennzeichnung
|
||||
im Manifest.
|
||||
|
||||
## Problem 2: SQLite darf nicht einfach kopiert werden
|
||||
|
||||
Mit WAL (seit dem Speicher-Fundament aktiv) stehen die jüngsten Änderungen in
|
||||
`state.db-wal`, nicht in `state.db`. Wer nur die `.db` kopiert, sichert einen
|
||||
veralteten und womöglich inkonsistenten Stand.
|
||||
|
||||
**Richtig:** `VACUUM INTO 'ziel.db'` — erzeugt im laufenden Betrieb eine konsistente,
|
||||
in sich geschlossene Kopie. Ein einzelnes SQL-Kommando, keine zusätzliche
|
||||
Abhängigkeit.
|
||||
|
||||
## Problem 3: JSON-Dateien werden nicht atomar geschrieben ⚠️
|
||||
|
||||
Alle Schreibvorgänge laufen über `File.WriteAllText`
|
||||
([InstanceDirectoryManager.cs:439](../../Services/InstanceDirectoryManager.cs#L439),
|
||||
[AgentEngine.cs:783](../../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L783)). Ein Absturz
|
||||
oder Stromausfall mitten im Schreiben hinterlässt eine abgeschnittene Datei.
|
||||
|
||||
**Das ist bereits passiert:** In der Instanz `TradingTeam` liegt eine
|
||||
`TokenUsage.json.corrupt_2026…` — die Fehlerbehandlung hat sie gesichert und neu
|
||||
angefangen. Der Verbrauch bis dahin war weg.
|
||||
|
||||
**Lösung:** In eine temporäre Datei daneben schreiben, dann `File.Replace` — das ist
|
||||
auf NTFS atomar. Gehört unabhängig vom Backup repariert.
|
||||
|
||||
## Vorschlag
|
||||
|
||||
Ein `BackupService`, der ein ZIP mit Manifest erzeugt:
|
||||
|
||||
```
|
||||
backup_Instance-TradingTeam_2026-07-28_1400.zip
|
||||
├── manifest.json ← Version, Zeitpunkt, Instanz, Prüfsummen,
|
||||
│ ob Zugangsdaten enthalten sind
|
||||
├── state.db ← via VACUUM INTO, konsistent
|
||||
├── InstanceSettings.json
|
||||
└── Agents/…
|
||||
```
|
||||
|
||||
Eigenschaften:
|
||||
|
||||
- **Planbar** über den vorhandenen ToolJob-Mechanismus (Cron) — kein neuer Scheduler.
|
||||
- **Rotation**: die letzten N behalten, plus je ein wöchentliches/monatliches.
|
||||
- **Restore mit Vorschau**: erst anzeigen, was überschrieben würde, dann bestätigen.
|
||||
- **Prüfsummen im Manifest**, damit ein beschädigtes Archiv beim Wiederherstellen
|
||||
auffällt und nicht erst danach.
|
||||
|
||||
**Der einzige Test, der zählt:** Backup erzeugen → in ein leeres Verzeichnis
|
||||
wiederherstellen → vergleichen. Ein ungeprüftes Restore ist kein Backup, sondern eine
|
||||
Vermutung. Dazu ein Test für den Rechnerwechsel: Backup mit Passphrase, DPAPI-Kontext
|
||||
simuliert anders, Restore muss funktionieren.
|
||||
|
||||
---
|
||||
|
||||
# 2. Was für das Finanzumfeld noch fehlt
|
||||
|
||||
Vorhanden: `DirectAPI` (Kurse, Krypto, Forex), `WebFetch`, `WebMonitor`,
|
||||
`SocialMediaManager` (X, YouTube-Transkripte), `Telegram`, `Mail`, `Database`,
|
||||
`FileRW` mit `stock_add`, seit neuem `Memory`.
|
||||
|
||||
Nach Wirkung sortiert:
|
||||
|
||||
## 2.1 Marktkalender — spart sofort Geld ★★★
|
||||
|
||||
Agenten wissen nicht, ob die Börse offen ist. Ein `*/30`-Cron läuft auch Sonntag um
|
||||
3 Uhr, ruft Kurse ab, analysiert Freitagsdaten und schreibt einen Bericht. Das kostet
|
||||
Tokens und erzeugt Scheinaktivität.
|
||||
|
||||
Zwei Bausteine:
|
||||
|
||||
- **Scheduler-Erweiterung** `onlyWhenMarketOpen: "NYSE"` bzw. `"XETRA"` — der Lauf
|
||||
wird schlicht übersprungen. Wirkt ohne Zutun des Modells.
|
||||
- **Tool `MarketCalendar`** für Fragen des Agenten: Ist heute Handelstag? Wann
|
||||
öffnet/schließt? Vor-/Nachbörse? Nächster Feiertag?
|
||||
|
||||
Handelskalender ändern sich selten und lassen sich als Datei pflegen — keine externe
|
||||
Abhängigkeit nötig.
|
||||
|
||||
## 2.2 Deterministische Berechnung ★★★
|
||||
|
||||
Sprachmodelle rechnen unzuverlässig. Indikatoren vom Modell schätzen zu lassen ist
|
||||
gleich doppelt schlecht: Das Ergebnis stimmt oft nicht, und die Zahlenkolonnen
|
||||
müssen dafür durch den Kontext.
|
||||
|
||||
Ein Tool `Indicators`, das im Code rechnet: gleitende Durchschnitte, RSI, ATR,
|
||||
Volatilität, prozentuale Veränderung, Korrelation, Drawdown, Positionsgröße nach
|
||||
Risiko. Der Agent bekommt Ergebnisse statt Rohdaten.
|
||||
|
||||
Spart Tokens **und** verbessert die Qualität — die seltene Kombination.
|
||||
|
||||
## 2.3 Datenaktualität erzwingen ★★
|
||||
|
||||
`DirectAPI` liefert brav `dataAsOf` mit, aber nichts wertet es aus. Ein Agent kann
|
||||
ungehindert auf drei Tage alten Kursen argumentieren.
|
||||
|
||||
Vorschlag: `maxAgeSeconds` in der Tool-Konfiguration. Überschrittene Daten werden
|
||||
entweder abgelehnt oder mit einem unübersehbaren Hinweis geliefert — nicht
|
||||
stillschweigend durchgereicht.
|
||||
|
||||
## 2.4 Termine und Fundamentaldaten ★★
|
||||
|
||||
Für „Finanznachrichten" ist der Kalender oft wichtiger als der Kurs: Was steht diese
|
||||
Woche an? Aktuell gibt es dazu nichts.
|
||||
|
||||
- Earnings-Termine, Dividenden, Splits
|
||||
- SEC EDGAR: Filings (8-K, 10-Q, 13F) — frei zugänglich, gut strukturiert
|
||||
- Wirtschaftstermine (Zinsentscheide, Inflationsdaten)
|
||||
|
||||
## 2.5 Bestandsregister ★★
|
||||
|
||||
`stock_add` ist eine Wissenssammlung, kein Bestand. Aussagen wie „Wie ist mein Risiko
|
||||
verteilt?" oder „Wie lief die Position seit Einstieg?" sind damit nicht möglich.
|
||||
|
||||
Eine eigene Tabelle mit Positionen (Symbol, Menge, Einstand, Datum, Notiz) — auch
|
||||
rein zur Beobachtung, ohne Handelsanbindung. Sie ist zugleich die Grundlage für die
|
||||
Leistungsmessung aus Teil 3.
|
||||
|
||||
## 2.6 Nachrichten-Entdopplung ★★
|
||||
|
||||
Dieselbe Meldung läuft über zehn Quellen. Ohne Abgleich zahlt man zehnmal, und der
|
||||
Agent hält es für zehn unabhängige Signale — was die Einschätzung systematisch
|
||||
verzerrt.
|
||||
|
||||
Eine `SeenItems`-Tabelle mit Prüfsumme über den normalisierten Titel plus
|
||||
Ähnlichkeitsabgleich. Passt gut zum vorhandenen Speicher-Fundament.
|
||||
|
||||
## 2.7 Prompt-Injection ist hier keine Theorie ★★★
|
||||
|
||||
Finanzinhalte auf X und in Newslettern sind genau der Ort, an dem gezielt manipuliert
|
||||
wird. Ein präparierter Beitrag kann einen Agenten steuern, der Mail versenden und
|
||||
posten darf. K2 aus der Bestandsaufnahme ist in diesem Umfeld die dringlichste
|
||||
Konzeptlücke.
|
||||
|
||||
Konkret: Tool-Ergebnisse als Daten rahmen, im System-Prompt verankern, dass daraus
|
||||
keine Anweisungen befolgt werden, und irreversible Aktionen an eine Freigabe koppeln.
|
||||
|
||||
## Abgrenzung
|
||||
|
||||
Was hier beschrieben ist, sind Recherche- und Analysewerkzeuge. Automatische
|
||||
**Orderausführung** wäre eine andere Kategorie mit eigenen Anforderungen (Broker-API,
|
||||
Fehlerbehandlung bei Teilausführungen, Nachvollziehbarkeit, rechtlicher Rahmen). Das
|
||||
wäre eine bewusste Entscheidung und kein Nebenprodukt der Analyse-Agenten.
|
||||
|
||||
---
|
||||
|
||||
# 3. Kosten und Leistung auswerten
|
||||
|
||||
## Der Kern des Problems
|
||||
|
||||
Kosten sind seit K5 sauber erfasst. Leistung ist ungleich schwerer — und der ehrliche
|
||||
Grund ist:
|
||||
|
||||
> **Leistung ist nur messbar, wenn der Agent sich auf etwas Falsifizierbares festlegt.**
|
||||
|
||||
Ein Agent, der „interessante Beobachtungen" liefert, lässt sich nicht bewerten. Einer,
|
||||
der sagt „NVDA über 5 Handelstage +3 %, Konfidenz 0,7", schon.
|
||||
|
||||
Das Finanzumfeld ist dafür ein Glücksfall: Aussagen werden von der Realität
|
||||
beantwortet, ohne dass jemand sie bewerten muss.
|
||||
|
||||
## Stufe 1 — Betriebsmetriken (sofort möglich)
|
||||
|
||||
Aus vorhandenen Daten, ohne neues Konzept:
|
||||
|
||||
| Metrik | Quelle | Was sie verrät |
|
||||
|---|---|---|
|
||||
| Kosten je Agent/Tag/Modell | `RunUsage` | vorhanden |
|
||||
| Cache-Trefferquote | `CachedTokens / PromptTokens` | ob T1 wirkt |
|
||||
| Fehlerquote | Status `Failed`/`LoopLimitExceeded` | instabile Agenten |
|
||||
| **Leerlaufquote** | Läufe ohne Ergebnis | siehe unten |
|
||||
| Tool-Fehlerquote | braucht Audit-Log (S4) | kaputte Tool-Konfiguration |
|
||||
| Schritte je Lauf | `StepCount` | umständliche Arbeitsweise |
|
||||
|
||||
Die **Leerlaufquote** ist die wirksamste einfache Kennzahl: Ein Agent, der 40 % seiner
|
||||
Läufe ohne greifbares Ergebnis beendet, hat meist ein Zeitplan-Problem — genau das,
|
||||
was der Marktkalender aus Teil 2 löst. Kosten ohne Gegenwert, sofort abstellbar.
|
||||
|
||||
## Stufe 2 — Ergebnisregister
|
||||
|
||||
Bisher wird nirgends festgehalten, **was** ein Lauf hervorgebracht hat.
|
||||
|
||||
Eine Tabelle `AgentOutput`, verknüpft mit dem Lauf: Art (Bericht, Signal, Nachricht,
|
||||
Gedächtniseintrag), Betreff, Verweis. Damit wird aus „Kosten pro Lauf" die deutlich
|
||||
nützlichere Größe **„Kosten pro Ergebnis"**.
|
||||
|
||||
## Stufe 3 — Falsifizierbare Aussagen
|
||||
|
||||
Das eigentliche Leistungsmaß. Ein Agent hält eine Aussage fest:
|
||||
|
||||
```
|
||||
Subjekt: NVDA
|
||||
Aussage: Kurs steigt
|
||||
Horizont: 5 Handelstage
|
||||
Konfidenz: 0.7
|
||||
Begründung: …
|
||||
```
|
||||
|
||||
Ein Auflösungs-Job prüft nach Ablauf gegen die tatsächlichen Kurse — `DirectAPI` hat
|
||||
sie bereits. Kein Mensch muss bewerten.
|
||||
|
||||
Daraus fällt ab:
|
||||
|
||||
- **Trefferquote** je Agent, je Kategorie, je Horizont
|
||||
- **Brier-Score** — misst nicht nur, ob die Richtung stimmte, sondern ob die
|
||||
Konfidenz ehrlich war. Ein Agent, der bei 0,9 nur in 60 % der Fälle recht hat, ist
|
||||
überheblich; das bleibt bei reiner Trefferquote unsichtbar.
|
||||
- **Kosten je richtiger Aussage**
|
||||
- **Vergleich gegen eine Nulllinie** — etwa „der Index steigt immer" oder
|
||||
„Zufallsentscheidung". Ohne Nulllinie ist eine Trefferquote von 55 % nicht
|
||||
einzuordnen.
|
||||
|
||||
## Die vorgeschlagene Kennzahl
|
||||
|
||||
Keine einzelne Zahl, sondern ein Quotient mit Bezugspunkt:
|
||||
|
||||
```
|
||||
Nutzen = Brier-Skill-Score gegenüber Nulllinie
|
||||
Wert = Nutzen / Kosten pro Tag
|
||||
```
|
||||
|
||||
Die Betriebsmetriken aus Stufe 1 dienen der Diagnose: *warum* ist ein Agent teuer —
|
||||
zu viele Schritte, zu große Tool-Ergebnisse, Leerläufe, kein Cache-Treffer?
|
||||
|
||||
## Eine Warnung zur Ehrlichkeit
|
||||
|
||||
Bei 20 Aussagen sagt eine Trefferquote von 60 % statistisch nichts. Die Auswertung
|
||||
muss Fallzahl und Unsicherheitsbereich mit ausweisen, sonst optimiert man Rauschen —
|
||||
und schaltet einen guten Agenten ab, weil er eine schlechte Woche hatte.
|
||||
|
||||
Faustregel für die Anzeige: unter 30 aufgelösten Aussagen keine Rangliste, nur
|
||||
Rohzahlen.
|
||||
|
||||
---
|
||||
|
||||
# Vorgeschlagene Reihenfolge
|
||||
|
||||
> **Abgelöst durch die [Roadmap](../Roadmap.md)** (Juli 2026). Die offenen Punkte
|
||||
> laufen dort als C1–C8 weiter; S4 + K2 sind in den Vorhaben A2/A3
|
||||
> (Staging-Freigabe, Audit-Log) aufgegangen.
|
||||
|
||||
| # | Was | Warum zuerst |
|
||||
|---|---|---|
|
||||
| 1 | ~~Atomares Schreiben~~ ✅ | umgesetzt (`File.Replace`-Muster) |
|
||||
| 2 | ~~Backup + Restore mit Test~~ ✅ | umgesetzt inkl. Oberfläche im Settings-Tab |
|
||||
| 3 | Marktkalender | Spart sofort Kosten, verbessert Datenlage |
|
||||
| 4 | `Indicators` | Qualität hoch, Tokens runter |
|
||||
| 5 | Ergebnisregister (Stufe 2) | Grundlage jeder Bewertung |
|
||||
| 6 | Aussagen + Auflösung (Stufe 3) | Das eigentliche Leistungsmaß |
|
||||
| 7 | S4 + K2 | Voraussetzung für unbeaufsichtigten Betrieb |
|
||||
|
||||
Punkte 1 und 2 gehören zusammen: Ein Backup nicht-atomar geschriebener Dateien kann
|
||||
eine bereits beschädigte Datei sichern.
|
||||
@@ -0,0 +1,582 @@
|
||||
# Linux-Portierung — Analyse
|
||||
|
||||
Stand: 2026-08-06. Reine Bestandsaufnahme und Aufwandsschätzung, **kein** Umbau.
|
||||
|
||||
> **Nachtrag 2026-08-23:** Der teure Teil dieser Analyse hat sich erledigt. Die rund
|
||||
> 8.900 Zeilen Windows-Forms-Oberfläche samt WebView2 und den vier `PropertyGrid`-
|
||||
> Instanzen gibt es nicht mehr — sie sind durch `src/ClawdDotNet.Desktop` (Avalonia,
|
||||
> `net10.0`) ersetzt und beim Frühjahrsputz entfernt worden. Damit fällt der zweite
|
||||
> der beiden unten vorgeschlagenen Schnitte weg: Es gibt keine Windows-gebundene
|
||||
> Oberfläche mehr, die noch umzuziehen wäre. Offen bleiben die drei Kernstellen
|
||||
> (DPAPI, Pfadvergleiche, Zeitzonen-IDs) und ein `linux-x64`-Release (Roadmap DC3).
|
||||
|
||||
Frage: Was ist nötig, damit ClawdDotNet unter Linux läuft, und was kostet das?
|
||||
|
||||
---
|
||||
|
||||
## 0. Kurzfassung
|
||||
|
||||
Die gute Nachricht zuerst: **Der Kern ist bereits portabel.** Alle 16 Bibliotheks-
|
||||
und beide Testprojekte zielen auf `net10.0` (nicht `net10.0-windows`), es gibt im
|
||||
gesamten Repository **kein einziges `DllImport`, keinen Registry-Zugriff und keine
|
||||
`System.Drawing`-Nutzung** in `src/`. Windows steckt an genau drei Stellen im Kern:
|
||||
DPAPI-Verschlüsselung, Groß-/Kleinschreibung bei Pfadvergleichen und die
|
||||
Zeitzonen-IDs.
|
||||
|
||||
Die schlechte Nachricht: Die gesamte Bedienoberfläche — rund **8.900 Zeilen** in
|
||||
`frm_*.cs`, `UI/`, `Models/` und `Services/` — hängt an Windows Forms, an WebView2
|
||||
und, am unangenehmsten, an vier `PropertyGrid`-Instanzen, die praktisch die
|
||||
komplette Einstellungsverwaltung ausmachen. Dafür gibt es in Avalonia keine
|
||||
Eins-zu-eins-Entsprechung.
|
||||
|
||||
**Empfehlung: den Umzug in zwei Schnitte teilen.** Ein kopfloser Host (ohne GUI)
|
||||
auf Linux ist in etwa **12–18 Personentagen** erreichbar und liefert den
|
||||
eigentlichen Nutzen — Agenten laufen auf einem Server, nicht auf einem
|
||||
Windows-Desktop. Die Avalonia-Oberfläche ist ein davon unabhängiges Vorhaben
|
||||
von **32–52 Personentagen**, das man danach in Ruhe angehen kann.
|
||||
|
||||
Gesamt für „alles auf Linux, mit GUI": **50–80 Personentage.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Bestandsaufnahme
|
||||
|
||||
### 1.1 Was bereits portabel ist
|
||||
|
||||
| Bereich | Zeilen | Zielframework | Windows-Abhängigkeit |
|
||||
|---|---:|---|---|
|
||||
| `src/ClawdDotNet.Core` | 8.959 | `net10.0` | nur DPAPI (1 Datei) |
|
||||
| 15 Tool-Projekte | 6.415 | `net10.0` | nur `.exe`-Pfade im SocialMediaManager |
|
||||
| `tests/` (348 Tests, 39 Dateien) | 6.888 | `net10.0` | 3 Testfälle mit `C:\`-Pfaden |
|
||||
|
||||
Alle NuGet-Pakete laufen unter Linux: `Microsoft.Data.Sqlite` (bringt
|
||||
`e_sqlite3` nativ für linux-x64/arm64 mit), `MySqlConnector`, `Npgsql`,
|
||||
`Microsoft.Data.SqlClient`, `MongoDB.Driver`, `MailKit`, `FluentFTP`,
|
||||
`Telegram.Bot`, `WTelegramClient`, `SharpCompress`, `Snappier`,
|
||||
`Microsoft.Extensions.Logging`. Kein Paket muss ersetzt werden — mit zwei
|
||||
Ausnahmen (siehe 2.1 und 2.3).
|
||||
|
||||
Auch die Dinge, bei denen man Ärger erwarten würde, sind sauber gelöst:
|
||||
|
||||
- [AtomicFile.cs:167](src/ClawdDotNet.Core/Storage/AtomicFile.cs:167) — `Commit`
|
||||
prüft `File.Exists` und weicht auf `File.Move` aus. `File.Replace` verlangt
|
||||
unter Unix ebenfalls eine vorhandene Zieldatei; der Fall ist also schon
|
||||
abgedeckt. Die Wiederholschleife ist unter Linux überflüssig, aber harmlos.
|
||||
- [TaskFrontmatter.cs:27](src/ClawdDotNet.Core/Tasks/TaskFrontmatter.cs:27) —
|
||||
normalisiert `\r\n` und `\r` vor dem Zerlegen. Task-Dateien von einem
|
||||
Windows-Rechner werden unter Linux korrekt gelesen.
|
||||
- Textdateien werden durchgängig als **UTF-8 ohne BOM** geschrieben
|
||||
(`AtomicFile`, `FileLogWriter`, `AgentEditorTool`). Kein `Encoding.Default`,
|
||||
keine Codepage-Fallen.
|
||||
- Zeitstempel gehen als `DateTime.UtcNow` in die Datenbank und werden mit
|
||||
`DateTimeStyles.RoundtripKind` gelesen.
|
||||
|
||||
### 1.2 Was am Windows-Teil hängt
|
||||
|
||||
| Bereich | Zeilen | davon Designer |
|
||||
|---|---:|---:|
|
||||
| `frm_*.cs` (6 Formulare + Dialoge) | 4.946 | 1.865 |
|
||||
| `UI/` (BackupPanel, WebViewBridge, EmbeddedUiManager) | 1.087 | 428 |
|
||||
| `Models/` (PropertyGrid-ViewModels) | 1.257 | — |
|
||||
| `Services/` (4 Dienste, an WinForms-Timer gekoppelt) | 1.501 | — |
|
||||
| `Properties/` | 123 | — |
|
||||
| **Summe** | **8.914** | **2.293** |
|
||||
|
||||
Dazu drei `.resx`-Dateien à ~272 KB (eingebettete Symbole/Bilder) und eine
|
||||
`frm_main.en.resx` für die englische Lokalisierung über den
|
||||
WinForms-Resx-Mechanismus.
|
||||
|
||||
Steuerelement-Inventar aus den Designer-Dateien: 24 `Label`, 19
|
||||
`ToolStripButton`, 12 `TabPage`, 12 `Button`, 9 `TextBox`, 6 `DataGridView`, 6
|
||||
`ToolStrip`, 5 `ComboBox`, **4 `PropertyGrid`**, 4 `TableLayoutPanel`, 4
|
||||
`FlowLayoutPanel`, 3 `TabControl`, 3 `SplitContainer`, 1 `RichTextBox`, 1
|
||||
`ListView`, 1 `NotifyIcon`, 1 `DateTimePicker`, 1 `NumericUpDown`.
|
||||
|
||||
Tabs in `frm_main`: Chat, Logs, Settings (mit Unter-Tabs App-Settings,
|
||||
Instance-Settings), Agent Settings, Jobs/Services (mit Unter-Tabs Jobs,
|
||||
Services, Job History), Info, Backup.
|
||||
|
||||
---
|
||||
|
||||
## 2. Die harten Brocken
|
||||
|
||||
### 2.1 WebView2 → kein Linux (Chat- und Übersichts-Ansicht)
|
||||
|
||||
`Microsoft.Web.WebView2` ist die einzige Windows-only-Paketabhängigkeit des
|
||||
Hauptprojekts und trägt die zwei sichtbarsten Ansichten:
|
||||
[frm_main.cs:235](frm_main.cs:235) und [frm_chat.cs:44](frm_chat.cs:44) laden
|
||||
`overview.html` bzw. `chat.html` aus `EmbeddedUI/` über
|
||||
`SetVirtualHostNameToFolderMapping` unter `https://ui.clwd.internal/`. Die
|
||||
Kommunikation läuft über [WebViewBridge.cs](UI/WebViewBridge.cs) —
|
||||
`WebMessageReceived` in die eine, `ExecuteScriptAsync` in die andere Richtung.
|
||||
|
||||
Drei Wege, jeder mit einem eigenen Preis:
|
||||
|
||||
| Variante | Was passiert | Aufwand | Risiko |
|
||||
|---|---|---:|---|
|
||||
| **A — Avalonia.WebView** | HTML/JS bleiben. Unter Linux rendert WebKitGTK, unter Windows weiterhin WebView2. Die Bridge wird auf die Abstraktion der Bibliothek umgeschrieben. | 4–6 PT | Bibliothek ist deutlich weniger reif als WebView2; WebKitGTK-Abhängigkeit muss auf dem Zielserver vorhanden sein; Verhalten unterscheidet sich je Plattform. |
|
||||
| **B — nativ neu in Avalonia** | Chat als echte Avalonia-Ansicht mit `ItemsControl` und einem Markdown-Renderer. `EmbeddedUI/` entfällt. | 8–12 PT | Kein Fremdrisiko, aber Neuentwicklung. Am Ende deutlich wartbarer als HTML-in-Container. |
|
||||
| **C — lokaler HTTP-Server + Systembrowser** | Die App liefert `EmbeddedUI/` über `http://localhost:port` aus, der Nutzer öffnet den Browser. | 3–4 PT | Bricht die Ein-Fenster-Anmutung. Passt aber ausgezeichnet zum kopflosen Betrieb — dort **ist** der Browser die Oberfläche. |
|
||||
|
||||
**Empfehlung:** C für den kopflosen Host (fällt dort ohnehin an), B für die
|
||||
Desktop-Oberfläche. Variante A koppelt uns an eine Bibliothek, die weniger stabil
|
||||
ist als alles andere im Projekt.
|
||||
|
||||
### 2.2 PropertyGrid → es gibt keinen Ersatz von der Stange
|
||||
|
||||
Vier `PropertyGrid`-Instanzen in [frm_main.Designer.cs](frm_main.Designer.cs)
|
||||
bilden App-Settings, Instance-Settings, Agent-Settings und Tool-Settings ab. Sie
|
||||
werden vollständig durch Attribute gesteuert — **246 `[Category]`,
|
||||
`[DisplayName]`, `[Description]`-Angaben** verteilt auf vier Dateien:
|
||||
|
||||
- [Models/ToolSettingsViewModels.cs](Models/ToolSettingsViewModels.cs) — 108
|
||||
- [Models/AgentSettingsViewModel.cs](Models/AgentSettingsViewModel.cs) — 54
|
||||
- [Models/AppSettings.cs](Models/AppSettings.cs) — 51
|
||||
- [Models/InstanceSettingsViewModel.cs](Models/InstanceSettingsViewModel.cs) — 33
|
||||
|
||||
Dazu kommen `[TypeConverter(typeof(ExpandableObjectConverter))]` für
|
||||
verschachtelte Objekte, `[PasswordPropertyText(true)]` für Geheimnisse und ein
|
||||
eigener [ModelTypeConverter](Models/ModelTypeConverter.cs), der das
|
||||
Modell-Auswahlfeld dynamisch aus der OpenRouter-Modellliste füllt.
|
||||
|
||||
Avalonia hat kein `PropertyGrid`. Zwei Möglichkeiten:
|
||||
|
||||
1. **`Avalonia.PropertyGrid`** (Community, MIT). Versteht `Category`,
|
||||
`DisplayName`, `Description`, `Browsable`, `ReadOnly` und
|
||||
`ExpandableObjectConverter`. Die ViewModels und ihre Attribute könnten
|
||||
weitgehend unverändert bleiben — das spart am meisten. Zu prüfen ist, ob der
|
||||
dynamische `ModelTypeConverter` mit `GetStandardValues` unterstützt wird; das
|
||||
ist der Punkt, an dem so etwas erfahrungsgemäß hakt. **Aufwand 6–8 PT**, plus
|
||||
dauerhafte Abhängigkeit an ein Ein-Personen-Projekt.
|
||||
2. **Von Hand gebaute Einstellungsformulare.** Mehr Arbeit, aber wir bekommen
|
||||
eine Oberfläche, die man Nutzern zumuten kann — das `PropertyGrid` ist
|
||||
ehrlicherweise eine Entwickleransicht. Passwörter, Verzeichnisauswahl,
|
||||
Validierung und die Modell-Auswahl werden dabei richtig statt behelfsmäßig.
|
||||
**Aufwand 10–14 PT.**
|
||||
|
||||
Das ist der größte Einzelposten der GUI-Portierung. Die Entscheidung kann und
|
||||
sollte man verschieben, bis das Grundgerüst steht.
|
||||
|
||||
### 2.3 DPAPI → Geheimnisse liegen unter Linux im Klartext
|
||||
|
||||
[SecretProtector.cs:42](src/ClawdDotNet.Core/Security/SecretProtector.cs:42):
|
||||
|
||||
```csharp
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return plainText;
|
||||
```
|
||||
|
||||
Unter Linux verschlüsselt `Protect` **stillschweigend nicht**. OpenRouter-Key,
|
||||
Datenbank-Verbindungszeichenfolgen mit Passwort, Mail-Zugangsdaten und das
|
||||
Telegram-2FA-Passwort lägen im Klartext in `InstanceConfig.json` und
|
||||
`AgentSettings.json` — genau der Zustand, den S7 behoben hat. Auf einem Server,
|
||||
der per SSH erreichbar ist und gesichert wird, ist das schlechter als auf einem
|
||||
Einzelplatz-Windows.
|
||||
|
||||
Dasselbe gilt für den Lizenz-Zustandsspeicher:
|
||||
`LicenseLabrador/client-dotnet/.../StateStore.cs:92` schützt seine Datei ebenfalls
|
||||
nur unter Windows per DPAPI.
|
||||
|
||||
Zu klären ist also ein plattformübergreifendes Verfahren. Realistisch:
|
||||
|
||||
- **AES-GCM mit Schlüssel aus einer Datei mit `0600`** neben der Konfiguration
|
||||
(Linux) bzw. weiterhin DPAPI (Windows). Einfach, wirkt gegen versehentliche
|
||||
Weitergabe und Backups, nicht gegen einen Angreifer mit demselben Benutzer —
|
||||
dieselbe Schutzstufe wie DPAPI heute.
|
||||
- Optional zusätzlich `libsecret`/Schlüsselbund, wenn eine Desktop-Sitzung da
|
||||
ist. Auf einem Server gibt es die nicht, also braucht es den Dateiweg ohnehin.
|
||||
|
||||
Nebenwirkung, die man einplanen muss: **Konfigurationen sind nicht mehr zwischen
|
||||
Betriebssystemen austauschbar.** Ein `enc:v1:`-Wert von Windows ist unter Linux
|
||||
nicht lesbar und umgekehrt. `Unprotect` wirft dann korrekterweise eine
|
||||
`SecretProtectionException` ([SecretProtector.cs:81](src/ClawdDotNet.Core/Security/SecretProtector.cs:81)) —
|
||||
für den Umzug einer Instanz braucht es einen Migrationsweg (Präfix `enc:v2:`,
|
||||
Werte neu eintragen oder ein Export/Import-Kommando).
|
||||
|
||||
**Aufwand 3–5 PT** inklusive Tests und Migration.
|
||||
|
||||
### 2.4 Zeitzonen → das ist die stillste Fehlerquelle
|
||||
|
||||
[TaskSchedule.cs:161](src/ClawdDotNet.Core/Tasks/TaskSchedule.cs:161):
|
||||
|
||||
```csharp
|
||||
try { return TimeZoneInfo.FindSystemTimeZoneById(id); }
|
||||
catch { return TimeZoneInfo.Utc; }
|
||||
```
|
||||
|
||||
Und [SchedulerTaskMigration.cs:32](src/ClawdDotNet.Core/Tasks/SchedulerTaskMigration.cs:32)
|
||||
schreibt `TimeZoneInfo.Local.Id` in die Task-Frontmatter. Auf dem
|
||||
Entwicklungsrechner ergibt das `"W. Europe Standard Time"`, unter Linux
|
||||
`"Europe/Berlin"`.
|
||||
|
||||
Task-Dateien sind Markdown im `SharedWorkspace` und wandern zwischen Rechnern.
|
||||
Trifft eine Windows-ID auf ein System ohne die Umsetzungsdaten, greift das
|
||||
`catch` — und der Task läuft ab sofort nach **UTC statt Ortszeit**, also im
|
||||
Sommer zwei Stunden zu früh. Ohne Fehlermeldung, ohne Logeintrag. Ein Task, der
|
||||
um 08:00 die Marktübersicht holen soll, läuft um 06:00.
|
||||
|
||||
.NET 6+ kann Windows-IDs unter Linux über ICU auflösen, aber nur wenn ICU
|
||||
vorhanden ist. In einem schlanken Container (Alpine ohne `icu-libs`, distroless)
|
||||
oder bei `InvariantGlobalization=true` ist es das nicht — dann schlägt jede
|
||||
Auflösung fehl und alles fällt auf UTC.
|
||||
|
||||
Was zu tun ist:
|
||||
|
||||
- Beim Schreiben auf **IANA normalisieren**
|
||||
(`TimeZoneInfo.TryConvertWindowsIdToIanaId`), beim Lesen beide Formen
|
||||
akzeptieren.
|
||||
- Das `catch` **nicht mehr still schlucken** — eine unbekannte Zeitzone muss
|
||||
protokolliert werden, besser noch den Task als fehlerhaft markieren.
|
||||
- Das Zielsystem muss `tzdata` haben. Für Container explizit installieren.
|
||||
|
||||
Verwandt: **82 Vorkommen von `DateTime.Now`/`UtcNow`**. Die meisten sind
|
||||
unkritisch, zwei fallen auf:
|
||||
[TaskboardService.cs:80](src/ClawdDotNet.Core/Tasks/TaskboardService.cs:80)
|
||||
schreibt `DateTime.Now`-Zeitstempel in Task-Dateien, und
|
||||
[LiveLogViewerService.cs:98](Services/LiveLogViewerService.cs:98) sucht die
|
||||
Logdatei des Tages über `DateTime.Now`. Server laufen üblicherweise mit `TZ=UTC`
|
||||
— dort wechselt die Logdatei dann um 02:00 Ortszeit statt um Mitternacht, und
|
||||
Task-Zeitstempel bekommen eine andere Bedeutung als bisher. Kein Fehler, aber
|
||||
eine Verhaltensänderung, die man kennen sollte.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 2.5 Groß-/Kleinschreibung bei Pfaden → sicherheitsrelevant
|
||||
|
||||
Linux-Dateisysteme unterscheiden Groß- und Kleinschreibung, Windows nicht. An
|
||||
vier Stellen wird das Gegenteil angenommen — und drei davon bewachen eine
|
||||
Sandbox-Grenze:
|
||||
|
||||
- [WorkspacePath.cs:72](src/ClawdDotNet.Tools.FileRW/WorkspacePath.cs:72) —
|
||||
`normalizedCandidate.StartsWith(normalizedRoot, OrdinalIgnoreCase)`. Das ist
|
||||
die Prüfung, die Agenten daran hindert, aus ihrem Arbeitsverzeichnis
|
||||
auszubrechen.
|
||||
- [FileRWTool.cs:174](src/ClawdDotNet.Tools.FileRW/FileRWTool.cs:174) —
|
||||
Abgleich gegen die Liste geschützter Pfade.
|
||||
- [FtpTool.cs:155](src/ClawdDotNet.Tools.FTP/FtpTool.cs:155) — dieselbe
|
||||
Einschließungsprüfung.
|
||||
- [BackupService.cs:384](src/ClawdDotNet.Core/Backup/BackupService.cs:384).
|
||||
|
||||
Unter Linux sind `/home/x/Workspace` und `/home/x/workspace` **zwei
|
||||
verschiedene Verzeichnisse**. Der Vergleich mit `OrdinalIgnoreCase` würde einen
|
||||
Pfad im zweiten als „innerhalb" des ersten durchwinken. Genauso liefe die
|
||||
Sperrliste in `FileRWTool` ins Leere, sobald jemand die Schreibweise ändert.
|
||||
|
||||
Nötig ist ein Vergleichsverfahren, das die Plattform berücksichtigt — ein
|
||||
`PathComparer`, der unter Windows `OrdinalIgnoreCase` und unter Unix `Ordinal`
|
||||
verwendet, konsequent an allen vier Stellen.
|
||||
|
||||
Ebenfalls betroffen, aber harmlos:
|
||||
[AtomicFile.cs:35](src/ClawdDotNet.Core/Storage/AtomicFile.cs:35) schlüsselt
|
||||
seine Sperren mit `fullPath.ToLowerInvariant()`. Unter Linux teilen sich damit
|
||||
zwei verschiedene Dateien eine Sperre — das serialisiert zu viel, gefährdet aber
|
||||
nichts.
|
||||
|
||||
**Aufwand 2–3 PT**, davon der größere Teil Tests.
|
||||
|
||||
### 2.6 Prozessaufrufe und `.exe`-Annahmen
|
||||
|
||||
- **`Process.Start("explorer.exe", …)`** — 4 Stellen
|
||||
([frm_main.cs:1552](frm_main.cs:1552), [frm_main.cs:1557](frm_main.cs:1557),
|
||||
[frm_main.cs:1562](frm_main.cs:1562), [BackupPanel.cs:338](UI/BackupPanel.cs:338)).
|
||||
Ersatz: `Process.Start(new ProcessStartInfo(path) { UseShellExecute = true })`
|
||||
bzw. `xdg-open`. Die Variante `explorer.exe /select,"…"` hat unter Linux kein
|
||||
Gegenstück — dort öffnet man nur den Ordner.
|
||||
- **`Microsoft.VisualBasic.Interaction.InputBox`** — 3 Stellen
|
||||
([Program.cs:280](Program.cs:280), [Program.cs:291](Program.cs:291),
|
||||
[frm_main.cs:658](frm_main.cs:658)), zwei davon für den interaktiven
|
||||
Telegram-Login (Code und 2FA-Passwort). Braucht einen eigenen Dialog. Für den
|
||||
kopflosen Betrieb ohnehin problematisch: **ein Login, der ein Eingabefenster
|
||||
öffnet, blockiert einen Dienst.** Dort muss der Telegram-Login anders gelöst
|
||||
werden (vorab per CLI, oder über die Weboberfläche).
|
||||
- **`yt-dlp.exe` / `ffmpeg.exe`** —
|
||||
[SocialMediaManagerTool.cs:782](src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs:782)
|
||||
und `:825`. Die PATH-Suche davor funktioniert unter Linux bereits; nur die
|
||||
Ausweichpfade sind fest auf `.exe` verdrahtet und laufen dort ins Leere.
|
||||
Kleine Änderung, aber sie fällt sonst erst zur Laufzeit auf.
|
||||
|
||||
**Aufwand zusammen 1–2 PT.**
|
||||
|
||||
### 2.7 WinForms-Timer in der Dienstschicht
|
||||
|
||||
`Services/` ist logisch kein UI-Code, hängt aber an
|
||||
`System.Windows.Forms.Timer`:
|
||||
|
||||
- [BackupScheduler.cs:41](Services/BackupScheduler.cs:41)
|
||||
- [LiveLogViewerService.cs:38](Services/LiveLogViewerService.cs:38) — schreibt
|
||||
zusätzlich direkt in eine `RichTextBox`
|
||||
- [OpenRouterStatusService.cs:43](Services/OpenRouterStatusService.cs:43)
|
||||
- [frm_main.License.cs:41](frm_main.License.cs:41)
|
||||
|
||||
Der Backup-Zeitplan und die Lizenzprüfung gehören in den kopflosen Host und
|
||||
müssen dafür auf `System.Threading.PeriodicTimer` umgestellt werden. Der
|
||||
Log-Betrachter ist echte Oberfläche und wird ohnehin neu gebaut.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 2.8 Lizenzierung — erledigt (Stand 2026-08-06)
|
||||
|
||||
> **Nachtrag.** LicenseLabrador und WatchDog sind im **Deploymentcenter**
|
||||
> zusammengefasst, Hardware-ID v2 ist dort umgesetzt. Der Client
|
||||
> (`Deploymentcenter.Client`, `netstandard2.0;net8.0`) läuft auf beiden
|
||||
> Plattformen, die HW-ID ist container- und umbenennungsfest, der Ablageort
|
||||
> löst sich auch ohne `HOME` auf, und der Zustandsspeicher ist mit AES-GCM
|
||||
> plattformübergreifend verschlüsselt.
|
||||
>
|
||||
> **Damit ist die einzige potenziell blockierende Frage dieser Analyse
|
||||
> beantwortet.** Details und offene Punkte der Anbindung:
|
||||
> [Deploymentcenter-Anbindung-Review.md](Deploymentcenter-Anbindung-Review.md).
|
||||
|
||||
Es bleibt reine Anschlussarbeit in ClawdDotNet: Projektverweis umhängen,
|
||||
[LicenseGate](Services/LicenseGate.cs) gegen die neue Ergebnisklasse schreiben
|
||||
(`LicenseState` ist entfallen, `Status` ist jetzt eine Zeichenkette), `MessageBox`
|
||||
durch das mitgelieferte `ILicensePrompt` ersetzen und die Lizenzprüfung aus
|
||||
[Program.cs:112](Program.cs:112) fensterfrei machen.
|
||||
|
||||
**Aufwand 2–3 PT** (vorher 3–5).
|
||||
|
||||
---
|
||||
|
||||
## 3. Kleinere Punkte, die trotzdem beißen
|
||||
|
||||
### 3.1 Kultur- und Zahlenformatierung
|
||||
|
||||
Nur 16 Stellen im gesamten Projekt nennen eine Kultur explizit. Das heißt
|
||||
umgekehrt: fast alles formatiert mit `CurrentCulture`. Auf dem
|
||||
Entwicklungsrechner ist das `de-DE`, auf einem Server mit unbesetztem `LANG`
|
||||
ist es `InvariantCulture`. Aus `1,25` wird `1.25`.
|
||||
|
||||
Wo das folgenlos bleibt:
|
||||
- **JSON** — `System.Text.Json` schreibt Zahlen immer invariant. Alle
|
||||
Konfigurationen, Zustandsdateien und API-Aufrufe sind sicher.
|
||||
- **SQLite** — Werte gehen typisiert über Parameter, nicht als Text.
|
||||
|
||||
Wo hinzuschauen ist:
|
||||
- Zeichenkettenverkettung in Logeinträgen und Prompts (`$"{cost:F4}"`). Wenn
|
||||
eine Zahl mit deutschem Dezimalkomma in einen Prompt gerät, muss das Modell
|
||||
raten.
|
||||
- Anzeigewerte in der Oberfläche — dort ist Ortsformat gewünscht, aber es sollte
|
||||
bewusst gesetzt sein, nicht zufällig.
|
||||
|
||||
**Empfehlung:** einmal alle Formatierungen durchgehen und trennen — invariant
|
||||
für alles Maschinenlesbare, `CurrentCulture` nur für die Anzeige. Am besten mit
|
||||
einem Analyzer (`CA1305`, `CA1304`, `CA1310`) als Warnung im Build, damit es so
|
||||
bleibt.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 3.2 Globalisierungsmodus festlegen
|
||||
|
||||
`InvariantGlobalization=true` macht das Publikat kleiner und ICU überflüssig —
|
||||
kostet aber `TimeZoneInfo.FindSystemTimeZoneById` (siehe 2.4), kulturabhängige
|
||||
Vergleiche und korrektes `ToLower()` für Umlaute. Für dieses Projekt mit
|
||||
zeitzonenabhängiger Planung ist das **keine Option**; die Entscheidung sollte im
|
||||
Projekt dokumentiert und ICU/tzdata als Voraussetzung festgehalten werden.
|
||||
|
||||
Nebenbemerkung: `COLLATE NOCASE` in
|
||||
[SqliteMemoryRepository.cs:143](src/ClawdDotNet.Core/Memory/SqliteMemoryRepository.cs:143)
|
||||
und [SqliteTaskRepository.cs:105](src/ClawdDotNet.Core/Tasks/SqliteTaskRepository.cs:105)
|
||||
ist ASCII-beschränkt — `Ä` und `ä` gelten SQLite als verschieden. Das ist heute
|
||||
schon so und ändert sich beim Umzug nicht, ist also kein Portierungsthema,
|
||||
sondern eine bestehende Eigenheit.
|
||||
|
||||
### 3.3 Zeilenenden
|
||||
|
||||
1.230 Stellen verwenden `Environment.NewLine` oder `\r\n`. Für Logdateien ist
|
||||
das egal. Bei **Task-Dateien** und Agenten-erzeugten Dateien im geteilten
|
||||
Arbeitsverzeichnis führt es zu Rauschen: Eine Datei, die unter Windows
|
||||
geschrieben und unter Linux angefasst wird, ändert komplett ihre Zeilenenden.
|
||||
Wenn der Arbeitsbereich unter Git liegt oder synchronisiert wird, sieht jede
|
||||
Änderung wie eine Vollumschreibung aus. Der Parser kommt damit klar (siehe 1.1)
|
||||
— es ist eine Frage der Ordnung, kein Fehler. Empfehlung: für Task- und
|
||||
Konfigurationsdateien fest `\n` schreiben.
|
||||
|
||||
### 3.4 Dateinamen
|
||||
|
||||
`Path.GetInvalidFileNameChars()` liefert unter Windows 41 Zeichen, unter Linux
|
||||
genau zwei (`\0` und `/`). [FileLogWriter.cs:95](src/ClawdDotNet.Core/Logging/FileLogWriter.cs:95)
|
||||
säubert Modulnamen damit — unter Linux entstehen also Dateinamen, die auf
|
||||
Windows nicht mehr lesbar sind. Betrifft Sicherungen, die zwischen Systemen
|
||||
wandern. Ebenso die Windows-Sonderfälle `CON`, `PRN`, `AUX` und Namen mit
|
||||
abschließendem Punkt: unter Linux erlaubt, beim Rückspielen auf Windows nicht.
|
||||
Für den Sicherungs-/Wiederherstellungsweg über Systemgrenzen hinweg relevant.
|
||||
|
||||
### 3.5 Ablageorte
|
||||
|
||||
[SettingsManager.cs:24](Services/SettingsManager.cs:24) legt `AppSettings.json`
|
||||
neben die Programmdatei (`AppDomain.CurrentDomain.BaseDirectory`). Unter Windows
|
||||
in einem Benutzerverzeichnis geht das; unter Linux liegt die Anwendung typisch
|
||||
in `/opt/…` oder `/usr/lib/…` und ist für den Dienstbenutzer **nicht
|
||||
schreibbar**. Dasselbe gilt für die Zielordner `tools/`, `Logs/` und
|
||||
`Instances/`, die die Build-Ziele in `ClawdDotNet.csproj` neben der
|
||||
Programmdatei anlegen.
|
||||
|
||||
Nötig ist eine Trennung von Programm und Daten nach XDG-Konvention:
|
||||
`$XDG_CONFIG_HOME` bzw. `/etc/clawddotnet` für die Konfiguration,
|
||||
`$XDG_DATA_HOME` bzw. `/var/lib/clawddotnet` für Instanzen und Datenbanken,
|
||||
`/var/log/clawddotnet` für Logs. Dazu Dateirechte: Instanzverzeichnisse mit
|
||||
Geheimnissen gehören auf `0700`, Konfigurationsdateien auf `0600` — unter
|
||||
Windows regelt das die ACL des Benutzerprofils, unter Linux muss man es setzen.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 3.6 Tests
|
||||
|
||||
Von 348 Tests sind fast alle portabel. Auffällig ist
|
||||
[WorkspacePathTests.cs:49](tests/ClawdDotNet.Tools.Tests/FileRW/WorkspacePathTests.cs:49):
|
||||
|
||||
```csharp
|
||||
[InlineData(@"C:\Windows\System32\config\SAM")]
|
||||
[InlineData(@"\\server\share\evil.txt")]
|
||||
[InlineData(@"C:\temp\datei.txt")]
|
||||
```
|
||||
|
||||
Unter Linux liefert `Path.IsPathRooted(@"C:\temp\datei.txt")` **`false`** — das
|
||||
ist ein gewöhnlicher relativer Dateiname mit Doppelpunkt und Backslashes darin.
|
||||
Der Test prüft dort also etwas anderes als beabsichtigt. Und da er die
|
||||
Sandbox-Grenze absichert, ist das keine Kleinigkeit: Er muss
|
||||
betriebssystemabhängig aufgeteilt werden, mit einer eigenen Linux-Fassung
|
||||
(`/etc/passwd`, `../../etc/passwd`, Symlinks). Symlinks sind überhaupt ein
|
||||
Prüfpunkt, den es unter Windows so nicht gab — `Path.GetFullPath` löst sie
|
||||
**nicht** auf, `File.ResolveLinkTarget` schon. Ein Agent könnte im
|
||||
Arbeitsverzeichnis einen Symlink nach `/etc` anlegen und die Prüfung ginge
|
||||
durch.
|
||||
|
||||
Ebenso in [YouTubeUrlTests.cs:118](tests/ClawdDotNet.Tools.Tests/SocialMedia/YouTubeUrlTests.cs:118)
|
||||
(harmlos, nur Beispieldaten).
|
||||
|
||||
**Aufwand 2–4 PT**, inklusive Symlink-Absicherung in `WorkspacePath` selbst.
|
||||
|
||||
### 3.7 Bau und Auslieferung
|
||||
|
||||
[Deploy-Build.ps1](Deploy-Build.ps1) setzt PowerShell 5.1 voraus, verwendet
|
||||
Backslash-Pfade und den festen Ausgabepfad `bin\Release\net10.0-windows`. Für
|
||||
Linux braucht es entweder eine `pwsh`-taugliche Fassung oder — besser — einen
|
||||
schlichten `dotnet publish -r linux-x64 --self-contained` mit einer
|
||||
systemd-Unit-Datei. Dazu:
|
||||
|
||||
- systemd-Unit mit eigenem Dienstbenutzer, `Restart=on-failure`
|
||||
- Prüfen, ob der bestehende Watchdog-Heartbeat
|
||||
([Program.cs:367](Program.cs:367)) mit `systemd-notify` zusammenspielen soll
|
||||
- optional `.deb` oder AppImage für den Desktop-Fall
|
||||
|
||||
**Aufwand 3–5 PT.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Der empfohlene Schnitt
|
||||
|
||||
Der entscheidende Befund dieser Analyse: **Die Oberfläche ist nicht der Grund,
|
||||
warum wir Linux wollen.** Der Grund ist, dass Agenten auf einem Server laufen
|
||||
sollen. [Program.cs](Program.cs) baut bereits alles — Speicher, Engine,
|
||||
Taskboard-Scanner, Watchdog, Lizenzprüfung — vollständig auf, **bevor**
|
||||
`frm_main` überhaupt entsteht (Zeilen 36–385 gegen 388–401). Diese Trennung
|
||||
existiert faktisch schon; sie muss nur formalisiert werden.
|
||||
|
||||
### Stufe 1 — Kern Linux-fest und kopfloser Host (12–18 PT)
|
||||
|
||||
| Schritt | PT |
|
||||
|---|---:|
|
||||
| Geheimnisse plattformübergreifend (2.3) | 3–5 |
|
||||
| Zeitzonen normalisieren, Fehler nicht mehr schlucken (2.4) | 2–3 |
|
||||
| Pfadvergleiche plattformabhängig + Symlink-Prüfung (2.5, 3.6) | 3–5 |
|
||||
| Prozessaufrufe, `.exe`-Pfade (2.6) | 1–2 |
|
||||
| Ablageorte und Dateirechte nach XDG (3.5) | 2–3 |
|
||||
| `ClawdDotNet.Host` — Startlogik aus `Program.cs` herauslösen, `PeriodicTimer` statt WinForms-Timer, Telegram-Login ohne Dialog | 4–6 |
|
||||
| Tests auf Linux grün, CI-Lauf für linux-x64 | 2–3 |
|
||||
|
||||
**Ergebnis:** Die Anwendung läuft als systemd-Dienst auf einem Linux-Server. Die
|
||||
Windows-GUI bleibt unverändert bestehen und wird weiter benutzt. Das ist der
|
||||
Punkt, an dem der Nutzen anfällt.
|
||||
|
||||
### Stufe 2 — Avalonia-Oberfläche (32–52 PT)
|
||||
|
||||
| Schritt | PT |
|
||||
|---|---:|
|
||||
| Grundgerüst: Avalonia-Projekt, DI, Dispatcher, Shell mit Tabs, MVVM-Schicht | 5–7 |
|
||||
| Logs-Tab (`RichTextBox` → `SelectingItemsControl` mit Filterung) | 2–3 |
|
||||
| Agent-Settings: Liste, Werkzeugauswahl, Aktionsschaltflächen | 5–8 |
|
||||
| Einstellungs-Tabs — PropertyGrid-Ersatz (2.2) | 6–10 |
|
||||
| Jobs / Services / Job History (4 `DataGridView`) | 4–6 |
|
||||
| Backup-Panel | 3–4 |
|
||||
| Instance-Manager und die fünf Dialoge | 4–6 |
|
||||
| Chat-Ansicht (Variante B, siehe 2.1) | 8–12 |
|
||||
| Info, Statusleiste, Werkzeugleisten, Menü, Lokalisierung de/en | 3–4 |
|
||||
|
||||
Die Spanne ist breit, weil zwei Entscheidungen noch offen sind (PropertyGrid-Ersatz
|
||||
und Chat-Variante). Sind die getroffen, lässt sich das auf etwa ±15 % genau
|
||||
angeben.
|
||||
|
||||
### Stufe 3 — Auslieferung und Härtung (6–10 PT)
|
||||
|
||||
Publish-Pipeline, systemd-Unit, Paketierung, Abnahme auf echter Hardware,
|
||||
Dokumentation, Umzugsweg für bestehende Instanzen.
|
||||
|
||||
### Gesamt
|
||||
|
||||
| | PT | bei Vollzeit |
|
||||
|---|---:|---|
|
||||
| Stufe 1 | 12–18 | 2,5–3,5 Wochen |
|
||||
| Stufe 2 | 32–52 | 6,5–10,5 Wochen |
|
||||
| Stufe 3 | 6–10 | 1,5–2 Wochen |
|
||||
| **Summe** | **50–80** | **10–16 Wochen** |
|
||||
|
||||
---
|
||||
|
||||
## 5. LiveCharts2
|
||||
|
||||
Zur Einordnung: **Das Projekt enthält heute keine einzige Diagrammdarstellung.**
|
||||
Die Suche nach `Chart`, `Series` oder `Plot` findet nur JSON-Feldnamen der
|
||||
Yahoo-Finance-Abfrage in
|
||||
[DirectAPITool.cs:126](src/ClawdDotNet.Tools.DirectAPI/DirectAPITool.cs:126).
|
||||
|
||||
LiveCharts2 ist damit **keine Portierung, sondern neue Funktionalität** — sie
|
||||
gehört zum Trading-Teil, nicht zum Linux-Umzug, und ist in den 50–80 PT oben
|
||||
nicht enthalten. Wenn die Kursansichten kommen, sind dafür grob 5–10 PT
|
||||
zusätzlich zu rechnen. Das passt zu dem, was in
|
||||
[docs/Roadmap.md](../Roadmap.md) und der Notiz „Basis vor Trading härten"
|
||||
festgehalten ist: Erst die Basis, dann die Handelsansichten.
|
||||
|
||||
Ein Punkt, der jetzt schon zählt: LiveCharts2 setzt auf SkiaSharp, genau wie
|
||||
Avalonia. Das spricht zusätzlich dafür, die Diagramme erst **nach** der
|
||||
Avalonia-Portierung zu bauen — sonst entstehen sie zweimal.
|
||||
|
||||
---
|
||||
|
||||
## 6. Was vor dem ersten Handgriff zu entscheiden ist
|
||||
|
||||
1. **Ist das Ziel Server oder Desktop?** Bei „Server" reicht Stufe 1, und Stufe 2
|
||||
kann entfallen oder durch eine Weboberfläche ersetzt werden. Das ändert die
|
||||
Schätzung um den Faktor drei.
|
||||
2. **Chat-Ansicht: HTML behalten oder nativ neu bauen?** (2.1)
|
||||
3. **PropertyGrid: Fremdbibliothek oder eigene Formulare?** (2.2)
|
||||
4. ~~**Erlaubt LicenseLabrador den Wechsel der Hardware-ID?**~~ — **geklärt**,
|
||||
siehe 2.8 und
|
||||
[Deploymentcenter-Anbindung-Review.md](Deploymentcenter-Anbindung-Review.md).
|
||||
5. **Bleibt Windows als Zielplattform bestehen?** Wenn ja, muss alles doppelt
|
||||
getestet werden, und die Geheimnis-Verschlüsselung braucht beide Wege plus
|
||||
Umzugspfad. Wenn nein, wird 2.3 deutlich einfacher.
|
||||
|
||||
Frage 1 und 5 beantworten sich vermutlich schnell; 2 und 3 kann man bis zum
|
||||
Beginn von Stufe 2 offenlassen, ohne Stufe 1 zu blockieren. Damit liegt nichts
|
||||
mehr außerhalb unserer Hand — **Stufe 1 kann beginnen.**
|
||||
|
||||
---
|
||||
|
||||
## Anhang — Vollständige Fundstellenliste
|
||||
|
||||
| Thema | Datei:Zeile |
|
||||
|---|---|
|
||||
| DPAPI | [SecretProtector.cs:42](src/ClawdDotNet.Core/Security/SecretProtector.cs:42), `:66`, `:90`, `:94` |
|
||||
| DPAPI (Lizenz) | `LicenseLabrador/client-dotnet/.../StateStore.cs:43`, `:92` |
|
||||
| Zeitzone | [TaskSchedule.cs:161](src/ClawdDotNet.Core/Tasks/TaskSchedule.cs:161), [SchedulerTaskMigration.cs:32](src/ClawdDotNet.Core/Tasks/SchedulerTaskMigration.cs:32) |
|
||||
| Pfad-Groß-/Kleinschreibung | [WorkspacePath.cs:72](src/ClawdDotNet.Tools.FileRW/WorkspacePath.cs:72), [FileRWTool.cs:174](src/ClawdDotNet.Tools.FileRW/FileRWTool.cs:174), [FtpTool.cs:155](src/ClawdDotNet.Tools.FTP/FtpTool.cs:155), [BackupService.cs:384](src/ClawdDotNet.Core/Backup/BackupService.cs:384), [AtomicFile.cs:35](src/ClawdDotNet.Core/Storage/AtomicFile.cs:35) |
|
||||
| `explorer.exe` | [frm_main.cs:1552](frm_main.cs:1552), `:1557`, `:1562`, [BackupPanel.cs:338](UI/BackupPanel.cs:338) |
|
||||
| `VisualBasic.InputBox` | [Program.cs:280](Program.cs:280), `:291`, [frm_main.cs:658](frm_main.cs:658) |
|
||||
| `.exe`-Werkzeugpfade | [SocialMediaManagerTool.cs:782](src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs:782), `:825` |
|
||||
| WinForms-Timer | [BackupScheduler.cs:41](Services/BackupScheduler.cs:41), [LiveLogViewerService.cs:38](Services/LiveLogViewerService.cs:38), [OpenRouterStatusService.cs:43](Services/OpenRouterStatusService.cs:43), [frm_main.License.cs:41](frm_main.License.cs:41) |
|
||||
| WebView2 | [frm_main.cs:235](frm_main.cs:235), [frm_chat.cs:44](frm_chat.cs:44), [WebViewBridge.cs](UI/WebViewBridge.cs), [ClawdDotNet.csproj](ClawdDotNet.csproj) |
|
||||
| PropertyGrid | [frm_main.Designer.cs](frm_main.Designer.cs) (4×), [Models/](Models/) (246 Attribute) |
|
||||
| Datenablage | [SettingsManager.cs:24](Services/SettingsManager.cs:24), [ClawdDotNet.csproj](ClawdDotNet.csproj) (Build-Ziele) |
|
||||
| Testdaten mit Windows-Pfaden | [WorkspacePathTests.cs:49](tests/ClawdDotNet.Tools.Tests/FileRW/WorkspacePathTests.cs:49) |
|
||||
| Build-Skript | [Deploy-Build.ps1](Deploy-Build.ps1) |
|
||||
@@ -0,0 +1,520 @@
|
||||
# Hardware-ID v2 — Implementierungsvorschlag
|
||||
|
||||
Stand: 2026-08-06. Betrifft `LicenseLabrador` (Client + Server) und die
|
||||
Aufrufseite in ClawdDotNet ([Services/LicenseGate.cs](Services/LicenseGate.cs)).
|
||||
|
||||
Anlass: Für den [Linux-Umzug](Linux-Portierung-Analyse.md) muss die
|
||||
Hardware-Bindung auf beiden Plattformen funktionieren. Bei der Durchsicht sind
|
||||
dabei zwei Probleme aufgefallen, die **nichts mit Linux zu tun haben**, aber
|
||||
denselben Code betreffen — die sollten in einem Zug mit erledigt werden.
|
||||
|
||||
---
|
||||
|
||||
## 1. Befund
|
||||
|
||||
### 1.1 Der Rechnername steckt im Hash — das ist das eigentliche Problem
|
||||
|
||||
[HardwareId.cs:43](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs):
|
||||
|
||||
```csharp
|
||||
rawBuilder.Append(Environment.MachineName); // "for system isolation"
|
||||
```
|
||||
|
||||
Folge: **Ein umbenannter Rechner ist eine neue Maschine.** Er verbraucht einen
|
||||
weiteren Aktivierungsplatz, und der alte bleibt für immer belegt
|
||||
(`max_activations` ist standardmäßig 2 — nach zwei Umbenennungen ist die Lizenz
|
||||
dicht). Das gilt bereits heute unter Windows.
|
||||
|
||||
Unter Linux wird daraus ein Totalausfall: In einem Container ist der Hostname
|
||||
standardmäßig die gekürzte Container-ID, also **bei jedem Start ein anderer**.
|
||||
Die Lizenz wäre nach dem zweiten `docker run` verbraucht.
|
||||
|
||||
Die Absicht („system isolation") ist auch nicht erfüllt: Der Rechnername steht
|
||||
ohnehin im Feld `hostname`, das der Server bei jeder Prüfung mitschreibt
|
||||
([LicenseService.php:112](../../../LicenseLabrador/server/src/LicenseService.php)).
|
||||
Diagnostisch verlieren wir nichts, wenn er aus dem Hash verschwindet.
|
||||
|
||||
### 1.2 Die MAC-Ausweichlösung ist unter Linux instabil
|
||||
|
||||
[HardwareId.cs:86](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)
|
||||
nimmt die alphabetisch erste physische MAC. Unter Linux:
|
||||
|
||||
- Die Stoppwortliste kennt `docker` und `veth`, aber **nicht** `br-` (Bridges),
|
||||
`virbr` (libvirt), `cni`, `flannel`, `cali` (Kubernetes), `zt` (ZeroTier).
|
||||
- `NetworkInterfaceType` meldet unter Linux für die meisten virtuellen Geräte
|
||||
schlicht `Ethernet` — die Typprüfung greift also nicht.
|
||||
- Bridge- und veth-MACs werden von systemd **je Boot neu zufällig** vergeben.
|
||||
|
||||
Sortiert man solche Adressen mit, wechselt die Hardware-ID beim Neustart. Die
|
||||
Ausweichlösung ist damit unter Linux schlimmer als keine.
|
||||
|
||||
### 1.3 Der Zustandsspeicher fällt still auf Klartext zurück
|
||||
|
||||
[StateStore.cs:41](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/StateStore.cs)
|
||||
beim Lesen und `:90` beim Schreiben:
|
||||
|
||||
```csharp
|
||||
try { decryptedData = ProtectedData.Unprotect(rawData, null, ...); }
|
||||
catch { decryptedData = rawData; } // ← Klartext wird akzeptiert
|
||||
```
|
||||
|
||||
Unter Linux wirft DPAPI immer, also läuft alles über den Klartextzweig. Zwei
|
||||
Folgen:
|
||||
|
||||
- `SECURITY.md` behauptet, der Cache sei „strikt an die `hardware_id` gebunden".
|
||||
Das stimmt für die *Hülle* (die Prüfung in
|
||||
[LicenseClient.cs:198](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseClient.cs)),
|
||||
nicht für die Cache-Datei selbst.
|
||||
- Ernster: `max_seen_time` ist die Uhr-Rückdreh-Sperre
|
||||
([StateStore.cs:107](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/StateStore.cs)).
|
||||
Wer eine `state.dat` von Hand schreiben kann, setzt den Wert auf 0 und stellt
|
||||
die Systemuhr zurück. Der Klartext-Rückfall beim **Lesen** macht das möglich,
|
||||
und zwar auf jeder Plattform, auf der DPAPI nicht greift.
|
||||
|
||||
### 1.4 Ablageort bricht bei einem systemd-Dienst weg
|
||||
|
||||
[LicenseConfig.cs:22](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseConfig.cs)
|
||||
verwendet `Environment.GetFolderPath(SpecialFolder.ApplicationData)`. Läuft der
|
||||
Dienst unter `User=clawd` ohne Heimatverzeichnis, ist `HOME` nicht gesetzt und
|
||||
`GetFolderPath` liefert einen **leeren String**. `Path.Combine("", slug,
|
||||
"license")` ergibt einen relativen Pfad — die Lizenz landet im Arbeitsverzeichnis
|
||||
oder gar nicht.
|
||||
|
||||
### 1.5 Kein Formatkennzeichen, keine Plattformangabe
|
||||
|
||||
Die Hardware-ID ist heute ein nackter SHA-256-Hex-String. Es gibt keine
|
||||
Möglichkeit, im Server zu erkennen, aus welcher Quelle oder von welchem
|
||||
Betriebssystem eine Aktivierung stammt — und keinen Weg, das Format je zu
|
||||
wechseln, ohne alle bestehenden Aktivierungen zu verlieren.
|
||||
|
||||
**Randnotiz:** `OperatingSystemHelpers.IsWindows()` nutzt
|
||||
`Environment.OSVersion.Platform == PlatformID.Win32NT`. Das funktioniert
|
||||
zufällig richtig (Linux liefert `Unix`), ist aber die veraltete API.
|
||||
`RuntimeInformation.IsOSPlatform(OSPlatform.Windows)` ist in netstandard2.0
|
||||
verfügbar und der korrekte Weg.
|
||||
|
||||
---
|
||||
|
||||
## 2. Zielbild: das Format
|
||||
|
||||
```
|
||||
2:<plattform>:<64 Hex-Zeichen>
|
||||
|
||||
Beispiele:
|
||||
2:win:9f3ab7c1… (Windows, MachineGuid)
|
||||
2:lin:41e0d5aa… (Linux, /etc/machine-id)
|
||||
2:lin:7c9182ff… (Linux, Vorgabe per Umgebungsvariable)
|
||||
```
|
||||
|
||||
68 Zeichen — passt in `activations.hardware_id VARCHAR(128)` ohne
|
||||
Schemaänderung. Der Doppelpunkt ist unproblematisch, die Spalte ist
|
||||
`utf8mb4_unicode_ci` und wird nur verglichen.
|
||||
|
||||
Der Hash selbst:
|
||||
|
||||
```
|
||||
sha256( "LicenseLabrador-HWID-v2" ‖ "\n" ‖ plattform ‖ "\n" ‖ quelle ‖ "\n" ‖ rohwert )
|
||||
```
|
||||
|
||||
- **Kein `MachineName`.** (1.1)
|
||||
- Die Domänenzeichenkette verhindert, dass derselbe Rohwert in anderem
|
||||
Zusammenhang wiederverwendbar ist.
|
||||
- `quelle` geht mit in den Hash: Findet der Client später eine bessere Quelle,
|
||||
ändert sich die ID bewusst und nachvollziehbar, statt zufällig.
|
||||
|
||||
Zusätzlich gehen drei neue Felder mit in die Anfrage — **nicht** in den Hash,
|
||||
nur zur Diagnose und für die Migration:
|
||||
|
||||
| Feld | Beispiel | Zweck |
|
||||
|---|---|---|
|
||||
| `hwid_version` | `2` | Formaterkennung serverseitig |
|
||||
| `hwid_source` | `machine-id` | Admin sieht, wie stabil die Bindung ist |
|
||||
| `legacy_hardware_id` | `<v1-Hash>` | Migration ohne Platzverlust (Abschnitt 4) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Quellen je Plattform
|
||||
|
||||
Reihenfolge = Priorität. Die erste Quelle, die einen nichtleeren, plausiblen Wert
|
||||
liefert, gewinnt.
|
||||
|
||||
### 3.1 Vorgabe (alle Plattformen, höchste Priorität)
|
||||
|
||||
```
|
||||
LicenseConfig.HardwareIdOverride (Code)
|
||||
LICENSELABRADOR_HWID (Umgebungsvariable)
|
||||
```
|
||||
|
||||
Quelle: `override`. Der Rohwert wird trotzdem gehasht, damit das Format
|
||||
einheitlich bleibt.
|
||||
|
||||
**Das ist der ehrliche Weg für Container und Serverbetrieb.** Heuristik kann dort
|
||||
nicht gewinnen — in einem Container gibt es keine Hardware, an die man binden
|
||||
könnte. Der Betreiber setzt einen stabilen Wert, hinterlegt ihn im
|
||||
Deployment-Geheimnis, und die Bindung ist so verlässlich wie dieser Wert. Eine
|
||||
Zeile in der systemd-Unit statt eines Ratespiels.
|
||||
|
||||
### 3.2 Windows
|
||||
|
||||
| # | Quelle | `hwid_source` |
|
||||
|---|---|---|
|
||||
| 1 | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (Registry64) | `machine-guid` |
|
||||
| 2 | Stabile physische MAC (Abschnitt 3.4) | `mac` |
|
||||
| 3 | Erzeugte Datei (Abschnitt 3.5) | `keyfile` |
|
||||
|
||||
Unverändert zu heute — nur ohne `MachineName` im Hash.
|
||||
|
||||
### 3.3 Linux
|
||||
|
||||
| # | Quelle | `hwid_source` | Anmerkung |
|
||||
|---|---|---|---|
|
||||
| 1 | `/etc/machine-id` | `machine-id` | Von systemd bei der Installation erzeugt, überlebt Neustarts und Kernel-Updates. Die richtige Wahl auf einem echten System. |
|
||||
| 2 | `/var/lib/dbus/machine-id` | `dbus-machine-id` | Ältere Systeme ohne systemd. |
|
||||
| 3 | `/sys/class/dmi/id/product_uuid` | `dmi-uuid` | SMBIOS-UUID, echte Hardware-Bindung. **Meist nur für root lesbar** (`0400`) — Versuch in `try` einpacken, kein Fehler wenn nicht lesbar. Bei VMs vom Hypervisor gesetzt und dort stabil. |
|
||||
| 4 | Stabile physische MAC (Abschnitt 3.4) | `mac` | |
|
||||
| 5 | Erzeugte Datei (Abschnitt 3.5) | `keyfile` | |
|
||||
|
||||
Zwei Fallen bei `/etc/machine-id`, die geprüft werden müssen:
|
||||
|
||||
- **Leer oder nur Zeilenumbruch.** Auf Systemen mit `systemd-firstboot` oder in
|
||||
manchen Images existiert die Datei, ist aber leer. Muss als „nicht vorhanden"
|
||||
behandelt werden, nicht als gültiger Wert — sonst haben *alle* diese
|
||||
Installationen dieselbe ID.
|
||||
- **Der Wert `uninitialized`.** Genau diese Zeichenkette schreibt systemd, wenn
|
||||
die ID im laufenden Betrieb noch nicht festgelegt ist. Ebenfalls verwerfen.
|
||||
|
||||
```csharp
|
||||
private static bool IsPlausibleMachineId(string? v)
|
||||
=> !string.IsNullOrWhiteSpace(v)
|
||||
&& v.Trim().Length >= 16
|
||||
&& !v.Trim().Equals("uninitialized", StringComparison.OrdinalIgnoreCase)
|
||||
&& v.Trim().Trim('0').Length > 0; // nicht alles Nullen
|
||||
```
|
||||
|
||||
### 3.4 MAC-Ausweichlösung, überarbeitet
|
||||
|
||||
Die heutige Fassung nimmt `FirstOrDefault()` der sortierten Liste. Wenn eine
|
||||
Schnittstelle dazukommt oder wegfällt, kann sich damit die gewählte MAC ändern.
|
||||
Besser: **alle** gültigen MACs sortiert verketten — dann ändert sich der Wert
|
||||
nur, wenn sich die Netzwerkausstattung wirklich ändert, und nicht schon, weil
|
||||
eine Adresse hinzukommt, die vorher sortiert davor lag.
|
||||
|
||||
Stoppwortliste erweitern um: `br-`, `virbr`, `cni`, `flannel`, `cali`, `weave`,
|
||||
`zt`, `tailscale`, `ipsec`, `sit`, `gre`, `dummy`, `bond`, `macvlan`, `ovs`.
|
||||
|
||||
Zusätzlich hart ausschließen (unabhängig vom Namen):
|
||||
|
||||
- Schnittstellen mit gesetztem **„locally administered"-Bit** (zweites Bit des
|
||||
ersten Oktetts, `mac[0] & 0x02`). Genau das setzt systemd bei zufällig
|
||||
erzeugten MACs für veth und Bridges. Ein sauberer, namensunabhängiger Filter —
|
||||
und der wirksamste von allen.
|
||||
- Unter Linux zusätzlich prüfen: existiert
|
||||
`/sys/class/net/<name>/device`? Fehlt das Verzeichnis, hat die Schnittstelle
|
||||
kein physisches Gerät und ist virtuell. Das ist zuverlässiger als jede
|
||||
Namensliste.
|
||||
|
||||
```csharp
|
||||
// Namensunabhängig: zufällig erzeugte MACs tragen dieses Bit.
|
||||
private static bool IsLocallyAdministered(PhysicalAddress addr)
|
||||
{
|
||||
var b = addr.GetAddressBytes();
|
||||
return b.Length > 0 && (b[0] & 0x02) != 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Erzeugte Datei als letzte Stufe
|
||||
|
||||
`<StorageDirectory>/machine.key` — 32 Zufallsbytes, Base64, Dateirechte `0600`.
|
||||
Wird nur angelegt, wenn keine Quelle davor greift.
|
||||
|
||||
Das ist eine **Installations-** und keine Hardware-Bindung. Für einen Container
|
||||
ohne Vorgabe ist das aber die Wahrheit, und mit einem gemounteten Datenverzeichnis
|
||||
bleibt sie über Container-Neustarts stabil. `hwid_source` = `keyfile` macht dem
|
||||
Admin sichtbar, dass diese Aktivierung schwächer gebunden ist als die anderen.
|
||||
|
||||
Wichtig: Die Datei gehört ins **Datenverzeichnis**, nicht neben die
|
||||
Programmdatei. Sonst ist sie bei jedem Deployment weg.
|
||||
|
||||
---
|
||||
|
||||
## 4. Migration v1 → v2 ohne Platzverlust
|
||||
|
||||
Der Kern: Der Client kennt **beide** IDs und schickt beide mit. Der Server zieht
|
||||
die alte Aktivierung auf die neue ID um, statt eine zweite anzulegen.
|
||||
|
||||
**Client** — `HardwareId` bekommt neben `GetHardwareId()` (v2) ein
|
||||
`GetLegacyHardwareId()`, das die heutige v1-Berechnung *unverändert* beibehält
|
||||
(inklusive `MachineName`, damit sie zu bestehenden Aktivierungen passt). Beides
|
||||
geht in die Anfrage:
|
||||
|
||||
```csharp
|
||||
hardware_id = "2:lin:41e0…",
|
||||
legacy_hardware_id = "8fa2…", // nur solange v1-Aktivierungen existieren
|
||||
hwid_version = 2,
|
||||
hwid_source = "machine-id",
|
||||
```
|
||||
|
||||
**Server** — in `LicenseService::validate`, an der Stelle der heutigen Suche
|
||||
([LicenseService.php:100](../../../LicenseLabrador/server/src/LicenseService.php)):
|
||||
|
||||
```
|
||||
1. Aktivierung mit hardware_id = <v2> suchen
|
||||
→ gefunden: normaler Weg (last_seen, hostname, app_version aktualisieren)
|
||||
|
||||
2. nicht gefunden, und legacy_hardware_id ist gesetzt:
|
||||
Aktivierung mit hardware_id = <v1> suchen
|
||||
→ gefunden: UPDATE activations SET hardware_id = <v2>, hwid_version = 2,
|
||||
hwid_source = <quelle> WHERE id = …
|
||||
+ audit_log-Eintrag 'hwid_migrated'
|
||||
→ weiter wie unter 1. KEIN neuer Platz verbraucht.
|
||||
|
||||
3. weder noch: neue Aktivierung anlegen, max_activations prüfen (wie heute)
|
||||
```
|
||||
|
||||
Damit wandern alle bestehenden Windows-Installationen beim ersten Start nach dem
|
||||
Update lautlos auf v2 — niemand merkt etwas, kein Aktivierungsplatz geht
|
||||
verloren. Das `legacy_hardware_id`-Feld kann nach einer Übergangszeit (etwa zwei
|
||||
Veröffentlichungen) aus dem Client fallen.
|
||||
|
||||
### 4.1 Der lokale Cache muss einmal verworfen werden
|
||||
|
||||
Nicht übersehen: Die Hardware-ID geht in zwei weitere Berechnungen ein —
|
||||
`CalculateHmac(state, licenseKey, _hardwareId)` für die Prüfsumme in
|
||||
`LicenseResult`
|
||||
([LicenseClient.cs:272](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/LicenseClient.cs))
|
||||
und den Seed des Speicherschutzes (`:287`). Nach dem Formatwechsel schlägt
|
||||
`VerifyChecksum` für jede zwischengespeicherte Hülle fehl.
|
||||
|
||||
Das ist kein Fehler, sondern erwartet — muss aber als **Cache-Fehltreffer**
|
||||
behandelt werden (einmal online neu prüfen), nicht als
|
||||
`TamperSuspected`. Sonst sperrt sich jede bestehende Installation beim ersten
|
||||
Start nach dem Update selbst aus. Der Weg dorthin: Cache-Version im
|
||||
`LocalCacheData` mitführen (`schema_version: 2`) und einen Datensatz mit
|
||||
abweichender Version verwerfen, bevor die Prüfsumme überhaupt geprüft wird.
|
||||
|
||||
### 4.2 Schemaerweiterung
|
||||
|
||||
```sql
|
||||
ALTER TABLE activations
|
||||
ADD COLUMN hwid_version TINYINT NOT NULL DEFAULT 1 AFTER hardware_id,
|
||||
ADD COLUMN hwid_source VARCHAR(32) NULL AFTER hwid_version,
|
||||
ADD COLUMN platform VARCHAR(8) NULL AFTER hwid_source;
|
||||
```
|
||||
|
||||
Alles mit Vorgabewerten, also rückwärtskompatibel — ein alter Client, der die
|
||||
Felder nicht schickt, funktioniert unverändert weiter.
|
||||
|
||||
---
|
||||
|
||||
## 5. Umzug Windows → Linux
|
||||
|
||||
Das ist etwas anderes als die Formatmigration: hier wechselt die Maschine
|
||||
wirklich, die ID muss sich also ändern. Drei Wege, alle drei sinnvoll parallel:
|
||||
|
||||
### 5.1 Der Normalfall braucht gar nichts
|
||||
|
||||
`max_activations` ist standardmäßig **2**. Ein Windows-Entwicklungsrechner und
|
||||
ein Linux-Server passen also ohne jeden Eingriff hinein. Für den anstehenden
|
||||
Umzug ist das wahrscheinlich die ganze Antwort — die anderen beiden Punkte sind
|
||||
für den Fall danach.
|
||||
|
||||
### 5.2 Abmelden vor dem Umzug (existiert, aber nicht erreichbar)
|
||||
|
||||
`LicenseService::deactivate` löscht die Aktivierungszeile und gibt den Platz frei
|
||||
([LicenseService.php:153](../../../LicenseLabrador/server/src/LicenseService.php)),
|
||||
und `LicenseClient.DeactivateAsync` ruft es auf. In ClawdDotNet ist die Methode
|
||||
aber nur über [LicenseGate.cs:39](Services/LicenseGate.cs) erreichbar und dort
|
||||
an die GUI gebunden.
|
||||
|
||||
Nachzuliefern: ein Kommandozeilenschalter am Host, damit das auch ohne
|
||||
Oberfläche geht.
|
||||
|
||||
```bash
|
||||
clawddotnet --license-deactivate
|
||||
```
|
||||
|
||||
Das braucht der kopflose Betrieb ohnehin (siehe
|
||||
[Linux-Portierung-Analyse.md](Linux-Portierung-Analyse.md), 2.6 — der
|
||||
Lizenzdialog ist ein `MessageBox`, der einen Dienst blockieren würde).
|
||||
|
||||
### 5.3 Umbinden aus der Verwaltung (fehlt noch)
|
||||
|
||||
Für den Fall, dass die alte Maschine schon weg ist: In
|
||||
`public/admin/license_detail.php` je Aktivierungszeile eine Schaltfläche
|
||||
**„Aktivierung freigeben"** (löscht die Zeile, gibt den Platz frei). Ein echtes
|
||||
„Umbinden" auf eine bekannte neue ID ist unnötig — Freigeben plus Neuaktivierung
|
||||
auf dem Zielsystem ist derselbe Vorgang mit weniger Code und einer klareren
|
||||
Spur im Prüfprotokoll.
|
||||
|
||||
Beides sollte in `audit_log` landen, mit altem und neuem Wert.
|
||||
|
||||
---
|
||||
|
||||
## 6. Zustandsspeicher härten
|
||||
|
||||
Zusammen mit dem HW-ID-Umbau, weil dieselbe Datei betroffen ist und die
|
||||
Verschlüsselung den HW-ID als Schlüsselmaterial braucht.
|
||||
|
||||
**Format** — feste Hülle statt „mal so, mal so":
|
||||
|
||||
```
|
||||
Magic "LLS2" (4 Byte) │ Nonce (12) │ Ciphertext │ GCM-Tag (16)
|
||||
```
|
||||
|
||||
- **AES-GCM**, Schlüssel abgeleitet aus HW-ID + `ProductSlug` per HKDF-SHA256.
|
||||
- Auf Windows das Ergebnis **zusätzlich** in DPAPI wickeln (Gürtel und
|
||||
Hosenträger, kostet nichts).
|
||||
- Dateirechte `0600` auf Unix.
|
||||
|
||||
**Der entscheidende Punkt: den Klartext-Rückfall beim Lesen entfernen.** Eine
|
||||
Datei, die sich nicht entschlüsseln oder nicht authentifizieren lässt, ist
|
||||
**kein Cache** — sie wird verworfen und der Client prüft online. Nicht als
|
||||
Klartext akzeptieren. Genau dieser Rückfall macht heute die
|
||||
Uhr-Rückdreh-Sperre umgehbar (1.3).
|
||||
|
||||
Einmalig weiterhin lesbar bleiben muss das alte Format (Datei ohne `LLS2`-Magic):
|
||||
einlesen, in v2 neu schreiben, fertig. Nach einer Veröffentlichung kann der Pfad
|
||||
weg.
|
||||
|
||||
### 6.1 netstandard2.0 hat kein AesGcm — Empfehlung: mehrfach zielen
|
||||
|
||||
`System.Security.Cryptography.AesGcm` gibt es erst ab .NET Core 3.0, `HKDF` erst
|
||||
ab .NET 5, `File.SetUnixFileMode` erst ab .NET 7. Das Projekt zielt heute auf
|
||||
`netstandard2.0`.
|
||||
|
||||
Zwei Wege:
|
||||
|
||||
1. **`<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>`** —
|
||||
*empfohlen*. ClawdDotNet (net10.0) zieht automatisch das net8.0-Ziel und
|
||||
bekommt `AesGcm`, `HKDF` und `File.SetUnixFileMode` ohne Umwege. Der
|
||||
netstandard2.0-Zweig bleibt für andere Abnehmer erhalten und nutzt dort
|
||||
BouncyCastle. Kosten: ein paar `#if NET8_0_OR_GREATER`-Blöcke an genau drei
|
||||
Stellen.
|
||||
2. **Durchgängig BouncyCastle** (`GcmBlockCipher`, `HkdfBytesGenerator`) — die
|
||||
Bibliothek ist mit `BouncyCastle.Cryptography` bereits als Abhängigkeit da,
|
||||
also kein neues Paket. Kein Mehrfachziel nötig, aber die Dateirechte bleiben
|
||||
ein Problem: `chmod` müsste per P/Invoke laufen.
|
||||
|
||||
Weg 1 ist sauberer, weil er nebenbei das Dateirechte-Problem löst.
|
||||
|
||||
---
|
||||
|
||||
## 7. Ablageort (1.4)
|
||||
|
||||
Auflösungskette in `LicenseConfig.StorageDirectory`, erste nutzbare gewinnt:
|
||||
|
||||
1. Explizit gesetzter Wert (ClawdDotNet setzt ihn künftig — der Host hat ohnehin
|
||||
eine eigene XDG-Auflösung).
|
||||
2. `LICENSELABRADOR_STORAGE_DIR`.
|
||||
3. Unix: `$XDG_CONFIG_HOME/<slug>/license`, sonst `$HOME/.config/<slug>/license`.
|
||||
4. Windows: `SpecialFolder.ApplicationData` wie heute.
|
||||
5. Letzter Ausweg: `<AppContext.BaseDirectory>/license`.
|
||||
|
||||
**Und in jedem Fall: nie einen leeren Pfad durchlassen.** Der heutige Code kann
|
||||
`Path.Combine("", …)` erzeugen, ohne dass es auffällt. Ein `if
|
||||
(string.IsNullOrEmpty(...)) throw` an dieser Stelle ist besser als eine
|
||||
Lizenzdatei, die im Arbeitsverzeichnis landet und beim nächsten Start nicht mehr
|
||||
gefunden wird.
|
||||
|
||||
---
|
||||
|
||||
## 8. Änderungsliste
|
||||
|
||||
### LicenseLabrador — Client
|
||||
|
||||
| Datei | Was |
|
||||
|---|---|
|
||||
| `HardwareId.cs` | Neuschreiben: v2-Format, Quellenkette je Plattform, `GetLegacyHardwareId()`, `HwidSource`/`Platform` als Eigenschaften, MAC-Filter (locally-administered-Bit, `/sys/class/net/*/device`), Plausibilitätsprüfung für machine-id, `machine.key`-Erzeugung |
|
||||
| `LicenseConfig.cs` | `HardwareIdOverride`, Auflösungskette für `StorageDirectory`, leeren Pfad ausschließen |
|
||||
| `StateStore.cs` | `LLS2`-Hülle, AES-GCM, `schema_version`, **Klartext-Rückfall beim Lesen entfernen**, v1-Einmalmigration, `0600` |
|
||||
| `LicenseClient.cs` | Neue Felder in `validate`/`deactivate` senden; Cache mit abweichender `schema_version` als Fehltreffer behandeln, **nicht** als `TamperSuspected` |
|
||||
| `OperatingSystemHelpers` | `RuntimeInformation.IsOSPlatform`, dazu `IsLinux()`/`IsMacOs()` |
|
||||
| `LicenseLabrador.Client.csproj` | `netstandard2.0;net8.0` |
|
||||
|
||||
### LicenseLabrador — Server
|
||||
|
||||
| Datei | Was |
|
||||
|---|---|
|
||||
| `sql/schema.sql` + Migrationsskript | `hwid_version`, `hwid_source`, `platform` |
|
||||
| `src/LicenseService.php` | `legacy_hardware_id` entgegennehmen; Migrationssuche (Abschnitt 4); neue Felder speichern |
|
||||
| `src/Audit.php` | Ereignisart `hwid_migrated`, `activation_released` |
|
||||
| `public/admin/license_detail.php` | Quelle/Plattform je Aktivierung anzeigen, „Aktivierung freigeben" |
|
||||
| `docs/SECURITY.md` | Aussage zur Cache-Bindung korrigieren (1.3) |
|
||||
|
||||
### ClawdDotNet
|
||||
|
||||
| Datei | Was |
|
||||
|---|---|
|
||||
| [Services/LicenseGate.cs](Services/LicenseGate.cs) | `StorageDirectory` explizit setzen; `MessageBox`/`frm_License` hinter eine Schnittstelle (`ILicensePrompt`) legen, damit der kopflose Host eine Konsolenfassung einsetzen kann |
|
||||
| Host (neu) | `--license-deactivate`, `--license-set-key`, `--license-status` |
|
||||
| `docs/Integrationsplan-WatchDog-LicenseLabrador.md` | Format v2 und Migrationsweg nachtragen |
|
||||
|
||||
---
|
||||
|
||||
## 9. Testplan
|
||||
|
||||
Das Wichtigste zuerst — die Fälle, die heute schiefgehen würden:
|
||||
|
||||
| Fall | Erwartung |
|
||||
|---|---|
|
||||
| Rechner umbenennen | **ID unverändert** (Kern von 1.1) |
|
||||
| Container zweimal starten, `/etc/machine-id` im Abbild | beide Male dieselbe ID |
|
||||
| Container ohne `machine-id`, Datenverzeichnis gemountet | ID über Neustarts stabil, `hwid_source = keyfile` |
|
||||
| Container ohne `machine-id`, **ohne** Mount | ID wechselt — muss so sein, und im Protokoll erkennbar |
|
||||
| `LICENSELABRADOR_HWID` gesetzt | gewinnt gegen alles, `hwid_source = override` |
|
||||
| `/etc/machine-id` leer bzw. `uninitialized` | wird verworfen, nächste Quelle greift |
|
||||
| Docker-Bridge und veth vorhanden, keine machine-id | MAC-Wahl ignoriert sie, ID über Neustart stabil |
|
||||
| Bestehende v1-Windows-Aktivierung, Client aktualisiert | Zeile wird auf v2 umgeschrieben, `max_activations` unverändert, Prüfprotokolleintrag |
|
||||
| v1-Cache-Datei nach dem Update | einmal online geprüft, dann v2-Cache — **kein** `TamperSuspected` |
|
||||
| `state.dat` von Hand mit `max_seen_time = 0` | Datei wird verworfen, Uhr-Rückdreh-Sperre bleibt wirksam |
|
||||
| systemd-Dienst ohne `HOME` | Ablageort auflösbar, keine Datei im Arbeitsverzeichnis |
|
||||
| Alter Client gegen neuen Server | funktioniert unverändert (Felder haben Vorgabewerte) |
|
||||
| Neuer Client gegen alten Server | funktioniert, Zusatzfelder werden ignoriert |
|
||||
|
||||
Die letzten beiden Zeilen sind nicht optional: Client und Server werden nicht
|
||||
gleichzeitig ausgerollt.
|
||||
|
||||
---
|
||||
|
||||
## 10. Aufwand
|
||||
|
||||
| Block | PT |
|
||||
|---|---:|
|
||||
| `HardwareId` v2 samt Quellenkette, MAC-Filter, `machine.key` | 2–3 |
|
||||
| Client mehrfach zielen + `StateStore`-Härtung | 2–3 |
|
||||
| Server: Migrationssuche, Schema, Prüfprotokoll, Verwaltungsansicht | 2–3 |
|
||||
| ClawdDotNet: `ILicensePrompt`, Lizenz-Kommandozeile | 1–2 |
|
||||
| Tests (Container-Fälle brauchen echtes Docker) und Abnahme | 1–2 |
|
||||
| **Summe** | **8–13** |
|
||||
|
||||
Das ist mehr als die 3–5 PT, die in der Linux-Analyse für „Lizenz" standen —
|
||||
weil dort nur die Plattformverträglichkeit gerechnet war. Die Punkte 1.1 und 1.3
|
||||
sind bestehende Fehler, die unabhängig vom Umzug behoben werden sollten; sie
|
||||
machen den Unterschied aus.
|
||||
|
||||
Der Block ist **unabhängig vom übrigen Linux-Umzug** und kann sofort beginnen —
|
||||
er hängt an keiner der offenen GUI-Entscheidungen.
|
||||
|
||||
---
|
||||
|
||||
## 11. Was ich anders machen würde als heute — kurz begründet
|
||||
|
||||
Drei Entscheidungen im Vorschlag verdienen eine Begründung, weil sie vom
|
||||
bisherigen Ansatz abweichen:
|
||||
|
||||
**Rechnername raus.** Er ist der Grund, warum die heutige Bindung fragiler ist
|
||||
als nötig, und er trägt nichts bei, was `activations.hostname` nicht schon
|
||||
festhält. Eine Bindung, die bei einer Umbenennung bricht, bindet nicht an
|
||||
Hardware, sondern an eine Konfiguration.
|
||||
|
||||
**Vorgabe per Umgebungsvariable statt besserer Heuristik für Container.** Man
|
||||
kann eine Container-Umgebung nicht sinnvoll erraten — es gibt dort keine
|
||||
Hardware. Jede zusätzliche Heuristik verschiebt nur, wo es falsch wird. Eine
|
||||
explizite Vorgabe ist ein bewusster Betreiberentscheid, in der Unit-Datei
|
||||
sichtbar, im Prüfprotokoll nachvollziehbar.
|
||||
|
||||
**Kein Klartext-Rückfall, auch nicht „zur Sicherheit".** Der heutige Rückfall
|
||||
sollte Robustheit bringen, kostet aber genau die Eigenschaft, für die der Cache
|
||||
existiert. Ein verworfener Cache bedeutet: einmal online prüfen. Das ist der
|
||||
mildere Schaden — und wer keine Verbindung hat, hat immer noch die
|
||||
Offline-Gnadenfrist aus der signierten Hülle, die von dieser Datei nicht abhängt.
|
||||
+34
-20
@@ -1,27 +1,41 @@
|
||||
# Archiv
|
||||
|
||||
Die vier Dokumente hier sind die **Entwicklungs-Prompts aus der Anfangszeit** des
|
||||
Projekts (Mai–Juli 2026). Sie haben ClawdDotNet aufgebaut und beschreiben deshalb
|
||||
den Stand von damals — unter anderem eine WinForms-Oberfläche mit WebView2, die es
|
||||
seit dem Frühjahrsputz vom 2026-08-23 nicht mehr gibt.
|
||||
Was hier liegt, ist **abgeschlossen und wird nicht mehr fortgeschrieben.** Der aktuelle
|
||||
Stand steht ausschließlich in der [Roadmap](../Roadmap.md); ihr Abschnitt 7 hält fest,
|
||||
was aus jedem dieser Dokumente dorthin übernommen wurde.
|
||||
|
||||
**Sie werden nicht mehr fortgeschrieben.** Wer den heutigen Stand sucht, findet ihn
|
||||
in [Roadmap](../Roadmap.md), [Bestandsaufnahme](../Bestandsaufnahme-2026-07.md) und
|
||||
den Konzept-Dokumenten daneben.
|
||||
Aufgehoben werden sie, weil sie die **Begründung** tragen: warum das System so
|
||||
geschnitten ist, wie es geschnitten ist, und welche Befunde die Entscheidungen geformt
|
||||
haben. Diese Herleitung lässt sich in einer Vorhabenliste nicht unterbringen, ohne sie
|
||||
unlesbar zu machen.
|
||||
|
||||
Aufgehoben werden sie aus zwei Gründen:
|
||||
> **Verweise auf Quelltext können ins Leere gehen.** Viele dieser Papiere zeigen auf
|
||||
> Dateien der WinForms-Fassung (`frm_*.cs`, `UI/`, `Models/`, `Program.cs`) oder auf die
|
||||
> alten Scheduler — beides wurde inzwischen entfernt. Wer die Stellen sehen will, findet
|
||||
> sie im Tag `vor-fruehjahrsputz-2026-08`. Die Zeilennummern wurden bewusst **nicht**
|
||||
> nachgeführt: Ein archiviertes Dokument beschreibt den Stand seines Datums.
|
||||
|
||||
1. **Herkunft.** Sie erklären, warum das System so geschnitten ist, wie es
|
||||
geschnitten ist — der Schichtschnitt Core/Tools/App stammt von hier.
|
||||
2. **Eine Regel gilt weiter.** `ClawdDotNet_Prompt_InternetTools.md` enthält die
|
||||
Pflichtfelder `fetchedAt` / `dataAsOf` / `source`, an die sich alle Internet-Tools
|
||||
halten. Der [WebSearch-Umsetzungsplan](../umsetzungsplaene/UMSETZUNGSPLAN-WebSearch-Tool.md)
|
||||
verweist darauf. Zieht diese Regel eines Tages in ein eigenes Dokument um, kann
|
||||
die Datei ganz weg.
|
||||
---
|
||||
|
||||
| Datei | Was darin steht | Was davon noch gilt |
|
||||
## Befunde und Konzepte
|
||||
|
||||
| Dokument | Datum | Was es war | Warum archiviert |
|
||||
|---|---|---|---|
|
||||
| [Bestandsaufnahme-2026-07](Bestandsaufnahme-2026-07.md) | Juli 2026 | Vollständiges Review von Engine, Sicherheit, Tools, Scheduling und Oberfläche. Quelle der Kürzel S1–S7, B1–B14, K1–K6, T1–T9, F-A1…F-A7 | Erledigtes steht in Roadmap 5, Offenes in 3.1/3.2/3.7. Die Kürzel leben in der Herkunft-Spalte weiter |
|
||||
| [Konzepte-Backup-Finanz-Analyse](Konzepte-Backup-Finanz-Analyse.md) | Juli 2026 | Drei Konzepte: Sicherung, Finanzumfeld, Leistungsmessung | Sicherung ist gebaut; der Rest läuft als C1–C8 weiter |
|
||||
| [Linux-Portierung-Analyse](Linux-Portierung-Analyse.md) | 2026-08-06 | Was kostet der Umzug nach Linux | Der teure Teil — 8.900 Zeilen WinForms — ist mit der Avalonia-Portierung entfallen. Übrig bleiben drei Kernstellen (DPAPI, Pfadvergleiche, Zeitzonen-IDs), sie stehen in Roadmap 3.5 |
|
||||
| [Lizenz-HardwareId-v2](Lizenz-HardwareId-v2-Implementierungsvorschlag.md) | 2026-08-06 | Überarbeitung der Hardware-Erkennung für LicenseLabrador | **Gegenstandslos.** LicenseLabrador ist durch das Deploymentcenter ersetzt. Lesenswert bleibt Abschnitt 1: warum der Rechnername nicht in eine Hardware-Kennung gehört |
|
||||
| [Deploymentcenter-Anbindung-Review](Deploymentcenter-Anbindung-Review.md) | 2026-08-08 | Review der ersten Anbindung | Befunde behoben; die Verdrahtung beschreibt heute [Deploymentcenter-Integration](../Deploymentcenter-Integration.md) |
|
||||
| [Deploymentcenter-2.4-Integrationsplan](Deploymentcenter-2.4-Integrationsplan.md) | 2026-08-15 | Zugangsschutz, Release-Strecke, Update, Erstinstallation, Signatur — durchgearbeitet | Abgearbeitet bis auf DC8 und zwei Betreiberpunkte; die stehen in Roadmap 3.5. Enthält die Messprotokolle der live durchgespielten Setup- und Update-Kette |
|
||||
|
||||
## Entwicklungs-Prompts der Anfangszeit
|
||||
|
||||
Mai–Juli 2026. Sie haben ClawdDotNet aufgebaut und beschreiben deshalb den Stand von
|
||||
damals — unter anderem eine WinForms-Oberfläche mit WebView2, die es nicht mehr gibt.
|
||||
|
||||
| Dokument | Was darin steht | Was davon noch gilt |
|
||||
|---|---|---|
|
||||
| `ClawdDotNet_StartPrompt.md` | Gesamtentwurf, Kernklassen, Beispielkonfiguration | Schichtschnitt und Tool-Vertrag; Oberfläche und `configs/*.json` überholt |
|
||||
| `ClawdDotNet_Prompt_WebviewChatWinForms.md` | WinForms-Oberfläche mit WebView2-Chat | nichts — ersetzt durch `src/ClawdDotNet.Desktop` |
|
||||
| `ClawdDotNet_Prompt_TelegramClient.md` | Entwurf des TelegramClient-Tools | umgesetzt in `src/ClawdDotNet.Tools.TelegramClient` |
|
||||
| `ClawdDotNet_Prompt_InternetTools.md` | WebFetch, DirectAPI, WebMonitor | die Pflichtfelder-Regel (siehe oben) |
|
||||
| [ClawdDotNet_StartPrompt](ClawdDotNet_StartPrompt.md) | Gesamtentwurf, Kernklassen, Beispielkonfiguration | Der Schichtschnitt Core/Tools/App und der Tool-Vertrag stammen von hier. Oberfläche und `configs/*.json` sind überholt |
|
||||
| [ClawdDotNet_Prompt_WebviewChatWinForms](ClawdDotNet_Prompt_WebviewChatWinForms.md) | WinForms-Oberfläche mit WebView2-Chat | nichts — ersetzt durch `src/ClawdDotNet.Desktop` |
|
||||
| [ClawdDotNet_Prompt_TelegramClient](ClawdDotNet_Prompt_TelegramClient.md) | Entwurf des TelegramClient-Tools | umgesetzt in `src/ClawdDotNet.Tools.TelegramClient` |
|
||||
| [ClawdDotNet_Prompt_InternetTools](ClawdDotNet_Prompt_InternetTools.md) | WebFetch, DirectAPI, WebMonitor | **Eine Regel gilt weiter:** die Pflichtfelder `fetchedAt` / `dataAsOf` / `source` in jedem Tool-Ergebnis mit externen Daten. Der [WebSearch-Umsetzungsplan](../umsetzungsplaene/UMSETZUNGSPLAN-WebSearch-Tool.md) verweist darauf. Zieht diese Regel in ein eigenes Dokument um, kann die Datei ganz weg |
|
||||
|
||||
Reference in New Issue
Block a user