Compare commits

..
Author SHA1 Message Date
RichardandClaude Opus 5 740649789e 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>
2026-08-23 18:31:06 +02:00
RichardandClaude Opus 5 2853541629 Fruehjahrsputz: WinForms-Altlast entfernt, Dokumentation nachgezogen
Die Avalonia-Portierung ist abgeschlossen, damit ist die in ClawdDotNet.slnx
angekuendigte Aufgabe "WinForms-Oberflaeche entfernen" faellig. Der Stand
davor liegt unter dem Tag vor-fruehjahrsputz-2026-08.

Entfernt (56 Dateien, seit dem Herausloesen der Anwendungsschicht nicht mehr
Teil des Builds): ClawdDotNet.csproj, Program.cs, sieben frm_*-Formulare,
UI/, Models/, EmbeddedUI/, Properties/, Resources/, Services/, das alte
Anwendungssymbol und Deploy-Build.ps1 (ersetzt durch deploy/publish.py).
Dazu configs/*.json - Beispielkonfigurationen aus der Zeit vor dem
Instanzverzeichnis, auf die nur noch die alten Prompts verwiesen.

Die vier Entwicklungs-Prompts der Anfangszeit ziehen nach docs/archiv/ um,
mit README, das ihren Stand einordnet. Eine Regel darin gilt weiter - die
Pflichtfelder fetchedAt/dataAsOf/source der Internet-Tools -, deshalb
Archiv statt Loeschen; der WebSearch-Plan verweist auf den neuen Pfad.

Toter Code
- PlaceholderPageViewModel samt Ansicht: Es gibt keinen Platzhalter-Bereich
  mehr, seit alle neun Seiten portiert sind.
- Snappier als direkter Paketverweis: MongoDB.Driver loest es ohnehin auf
  dieselbe Fassung auf, der Verweis hob nichts an.

Zwei Fehler, die dabei sichtbar wurden
- Die taegliche Sicherung lief ins Leere. Die Oberflaeche bot sie an und
  schrieb Uhrzeit, Zielordner und Anzahl in die Einstellungen, aber der
  BackupScheduler wurde nirgends erzeugt. Jetzt am AppHost verdrahtet und
  in den geordneten Abbau aufgenommen.
- SettingsPageViewModel hielt die vier Sicherungs-Einstellungen doppelt.
  Aus der Ansicht waren sie laengst verschwunden, gelesen und beim
  Speichern zurueckgeschrieben wurden sie weiter: Wer die Uhrzeit auf der
  Sicherungs-Seite aenderte und danach die Einstellungen speicherte, bekam
  den alten Wert zurueck.

Pakete: keine bekannten Sicherheitsluecken mehr
- SQLitePCLRaw.bundle_e_sqlite3 auf 2.1.13 angehoben. Microsoft.Data.Sqlite
  bringt 2.1.11 mit, darin steckt GHSA-2m69-gcr7-jv3q (NU1903, hoch).
- SharpCompress bleibt als direkter Verweis stehen. Beim Aufraeumen erst
  als ungenutzt entfernt - dabei kam die von MongoDB.Driver gezogene
  Fassung 0.30.1 mit GHSA-6c8g-7p36-r338 zurueck. Der Verweis ist eine
  Anhebung, kein Ballast; das steht jetzt als Kommentar dabei.

Dokumentation
- Roadmap mit Statusblock: A1 und A3 erledigt, A2 nur zur Haelfte - Gate,
  Policy und Dienst greifen, aber keine Ansicht ruft ApproveAsync auf, ein
  gestagter Aufruf liegt unbeantwortet. Das ist jetzt Punkt 1 der Reihung.
  Rocket.Chat steht und kollidiert mit A5 (Matrix) - Entscheidung faellig.
- Avalonia-Portierungsleitfaden -> Oberflaechen-Leitfaden: kein Auftrag mehr,
  sondern Beschreibung des Stands.
- Bestandsaufnahme und Linux-Analyse als datierte Befunde gekennzeichnet;
  der teure Teil der Linux-Analyse (8.900 Zeilen WinForms) ist hinfaellig.
- Verweise auf frm_*, WebView2 und ClawdDotNet.csproj in den lebenden
  Dokumenten richtiggestellt.

Build fehlerfrei, 585 Tests gruen (6 uebersprungen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:26:14 +02:00
90 changed files with 556 additions and 18714 deletions
-124
View File
@@ -1,124 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ClawdDotNet</RootNamespace>
<ApplicationIcon>Martin-Berube-Flat-Animal-Crab.ico</ApplicationIcon>
<!-- ── Sauberes Build-Verzeichnis ── -->
<!-- Nur deutsche + englische Satelliten-Assemblies (Sprachpakete) ausgeben -->
<SatelliteResourceLanguages>de;en</SatelliteResourceLanguages>
<!-- Keine XML-Dokumentationsdateien von NuGet-Paketen kopieren -->
<PublishDocumentationFiles>false</PublishDocumentationFiles>
<!-- PDB-Dateien nur im Debug-Build ausgeben -->
<DebugSymbols Condition="'$(Configuration)' == 'Release'">false</DebugSymbols>
<CopyOutputSymbolsToOutputDirectory Condition="'$(Configuration)' == 'Release'">false</CopyOutputSymbolsToOutputDirectory>
</PropertyGroup>
<!-- Unterordner mit eigenen Projekten aus dem Glob des Hauptprojekts nehmen.
Ohne das kompiliert die WinForms-App die Test-Quellen mit. -->
<ItemGroup>
<Compile Remove="src\**" />
<None Remove="src\**" />
<EmbeddedResource Remove="src\**" />
<Compile Remove="tests\**" />
<None Remove="tests\**" />
<EmbeddedResource Remove="tests\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3967.48" />
</ItemGroup>
<!-- Uebergang: SettingsManager und AppSettings sind nach ClawdDotNet.App gewandert.
Das globale Using haelt die WinForms-Fassung baubar, solange sie waehrend der
Avalonia-Portierung noch als Vorlage und Vergleich dient. Faellt mit dem Projekt
weg (Aufgabe „WinForms-Oberflaeche entfernen"). -->
<ItemGroup>
<Using Include="ClawdDotNet.App.Settings" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="EmbeddedUI\**\*" />
</ItemGroup>
<ItemGroup>
<Content Include="Martin-Berube-Flat-Animal-Crab.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="src\ClawdDotNet.App\ClawdDotNet.App.csproj" />
<ProjectReference Include="src\ClawdDotNet.Core\ClawdDotNet.Core.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.FileRW\ClawdDotNet.Tools.FileRW.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.Telegram\ClawdDotNet.Tools.Telegram.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.Mail\ClawdDotNet.Tools.Mail.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.Database\ClawdDotNet.Tools.Database.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.FTP\ClawdDotNet.Tools.FTP.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.DirectAPI\ClawdDotNet.Tools.DirectAPI.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.WebFetch\ClawdDotNet.Tools.WebFetch.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.WebMonitor\ClawdDotNet.Tools.WebMonitor.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.AgentComm\ClawdDotNet.Tools.AgentComm.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.AgentSpawn\ClawdDotNet.Tools.AgentSpawn.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.SocialMediaManager\ClawdDotNet.Tools.SocialMediaManager.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.AgentEditor\ClawdDotNet.Tools.AgentEditor.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.Memory\ClawdDotNet.Tools.Memory.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.Taskboard\ClawdDotNet.Tools.Taskboard.csproj" />
<ProjectReference Include="src\ClawdDotNet.Tools.TelegramClient\ClawdDotNet.Tools.TelegramClient.csproj" />
<!-- Der Verweis auf das LicenseLabrador-SDK ist entfallen: Lizenz, Watchdog, Updates,
Fehler-Stream und Bugtracker laufen jetzt ueber das Deploymentcenter. Das SDK
dafuer haengt an src\ClawdDotNet.App und kommt von dort transitiv mit. -->
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- ═══════════════════════════════════════════════
Sauberes Programmverzeichnis
═══════════════════════════════════════════════ -->
<!-- XML-Dokumentation von NuGet-Paketen nicht ins Build-Verzeichnis kopieren -->
<Target Name="RemoveNuGetXmlDocs" AfterTargets="ResolveReferences">
<ItemGroup>
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
Condition="'%(Extension)' == '.xml'" />
</ItemGroup>
</Target>
<!-- WebView2-WPF-Assemblies entfernen (nur WinForms wird verwendet) -->
<Target Name="RemoveWebView2Wpf" AfterTargets="ResolveReferences">
<ItemGroup>
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
Condition="$([System.String]::Copy('%(Filename)').Contains('WebView2.Wpf'))" />
</ItemGroup>
</Target>
<!-- Arbeitsordner (tools/, Logs/, Instances/) anlegen -->
<Target Name="CreateWorkingDirectories" AfterTargets="Build">
<MakeDir Directories="$(OutputPath)tools" />
<MakeDir Directories="$(OutputPath)Logs" />
<MakeDir Directories="$(OutputPath)Instances" />
</Target>
<Target Name="CreatePublishDirectories" AfterTargets="Publish">
<MakeDir Directories="$(PublishDir)tools" />
<MakeDir Directories="$(PublishDir)Logs" />
<MakeDir Directories="$(PublishDir)Instances" />
</Target>
</Project>
-11
View File
@@ -24,15 +24,4 @@
<Project Path="tests/ClawdDotNet.Core.Tests/ClawdDotNet.Core.Tests.csproj" />
<Project Path="tests/ClawdDotNet.Tools.Tests/ClawdDotNet.Tools.Tests.csproj" />
</Folder>
<!--
Die WinForms-Fassung (ClawdDotNet.csproj, frm_*.cs, UI/, Models/, EmbeddedUI/) ist
seit dem Herausloesen der Anwendungsschicht nicht mehr Teil des Builds.
Die Dateien bleiben bis zum Abschluss der Portierung liegen: Sie sind die Vorlage
fuer Aufbau und Verdrahtung der Avalonia-Ansichten — dafuer muessen sie lesbar sein,
nicht uebersetzbar. Entfernt werden sie mit der Aufgabe
„WinForms-Oberflaeche entfernen".
Oberflaeche ist jetzt src/ClawdDotNet.Desktop.
-->
</Solution>
-256
View File
@@ -1,256 +0,0 @@
#Requires -Version 5.1
<#
.SYNOPSIS
ClawdDotNet Deployment-Paket erstellen.
Baut Release, packt alles Noetige in ein ZIP das per AnyDesk auf den Server kopiert wird.
.DESCRIPTION
Zwei Modi:
-Full Komplettes Deployment (alle Dateien, ~300 MB) — fuer Erstinstallation oder Dependency-Updates
-Quick Nur ClawdDotNet-eigene Binaries (~2 MB) — fuer normale Code-Aenderungen (DEFAULT)
.EXAMPLE
.\Deploy-Build.ps1 # Quick-Deploy (nur eigene DLLs)
.\Deploy-Build.ps1 -Full # Alles inkl. Dependencies
.\Deploy-Build.ps1 -SkipBuild # ZIP ohne vorher zu bauen (wenn Build schon aktuell)
#>
param(
[switch]$Full,
[switch]$SkipBuild,
[switch]$IncludeAgentConfigs
)
$ErrorActionPreference = 'Stop'
# ─── Pfade ───
$projectRoot = $PSScriptRoot
$buildOutput = Join-Path $projectRoot "bin\Release\net10.0-windows"
$deployDir = Join-Path $projectRoot "deploy"
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$mode = if ($Full) { "full" } else { "quick" }
$zipName = "ClawdDotNet_${mode}_${timestamp}.zip"
$zipPath = Join-Path $deployDir $zipName
# ─── 1. Build ───
if (-not $SkipBuild) {
Write-Host "`n=== Building Release ===" -ForegroundColor Cyan
Push-Location $projectRoot
dotnet build -c Release --no-restore
if ($LASTEXITCODE -ne 0) {
Write-Host "BUILD FAILED!" -ForegroundColor Red
Pop-Location
exit 1
}
Pop-Location
Write-Host "Build OK" -ForegroundColor Green
} else {
Write-Host "`n=== Build uebersprungen (SkipBuild) ===" -ForegroundColor Yellow
}
# ─── 2. Staging-Ordner vorbereiten ───
$stagingDir = Join-Path $deployDir "staging_$timestamp"
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
if ($Full) {
# ── Full Deploy: Alles ausser Instances/, Logs/, PDBs ──
Write-Host "`n=== Full Deploy: Kopiere alle Dateien ===" -ForegroundColor Cyan
# Dateien im Root
Get-ChildItem $buildOutput -File | Where-Object {
$_.Extension -ne '.pdb'
} | ForEach-Object {
Copy-Item $_.FullName $stagingDir
}
# Unterordner (ohne Instances und Logs)
Get-ChildItem $buildOutput -Directory | Where-Object {
$_.Name -notin @('Instances', 'Logs')
} | ForEach-Object {
Copy-Item $_.FullName (Join-Path $stagingDir $_.Name) -Recurse
}
} else {
# ── Quick Deploy: Nur ClawdDotNet-eigene Dateien ──
Write-Host "`n=== Quick Deploy: Nur eigene Binaries ===" -ForegroundColor Cyan
Get-ChildItem $buildOutput -File | Where-Object {
$_.Name -match '^ClawdDotNet\.' -and $_.Extension -ne '.pdb'
} | ForEach-Object {
Copy-Item $_.FullName $stagingDir
}
}
# ─── 2b. Optional: Agent-Configs mitkopieren ───
if ($IncludeAgentConfigs) {
Write-Host "=== Agent-Configs werden mitgepackt ===" -ForegroundColor Yellow
$instancesSource = Join-Path $buildOutput "Instances"
if (Test-Path $instancesSource) {
$instancesDest = Join-Path $stagingDir "Instances"
# Agent-Configs + Shared-Website-Templates kopieren
Get-ChildItem $instancesSource -Recurse -File -Include "Identity.md","Soul.md","AgentSettings.json","AgentList.json","InstanceSettings.json","style.css","script.js" | ForEach-Object {
$relPath = $_.FullName.Substring($instancesSource.Length)
$destPath = Join-Path $instancesDest $relPath
$destDir = Split-Path $destPath -Parent
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
Copy-Item $_.FullName $destPath
}
}
}
# ─── 3. Install-Skript beilegen ───
$installScript = @'
#Requires -Version 5.1
<#
.SYNOPSIS
ClawdDotNet Update auf dem Server installieren.
Stoppt die laufende Instanz, kopiert neue Dateien, startet neu.
.EXAMPLE
.\Deploy-Install.ps1 # Standard: ClawdDotNet.exe automatisch suchen
.\Deploy-Install.ps1 -TargetDir "D:\ClawdDotNet" # Zielordner explizit angeben
#>
param(
[string]$TargetDir
)
$ErrorActionPreference = 'Stop'
# Zielordner bestimmen
if (-not $TargetDir) {
$candidates = @(
"C:\ClawdDotNet",
"D:\ClawdDotNet",
"$env:ProgramFiles\ClawdDotNet",
"$env:LOCALAPPDATA\ClawdDotNet"
)
foreach ($c in $candidates) {
if (Test-Path (Join-Path $c "ClawdDotNet.exe")) {
$TargetDir = $c
break
}
}
if (-not $TargetDir) {
Write-Host "ClawdDotNet-Installation nicht gefunden!" -ForegroundColor Red
Write-Host 'Bitte mit -TargetDir angeben, z.B.:'
Write-Host ' .\Deploy-Install.ps1 -TargetDir "D:\ClawdDotNet"'
exit 1
}
}
Write-Host ""
Write-Host "=== ClawdDotNet Update ===" -ForegroundColor Cyan
Write-Host "Ziel: $TargetDir"
# 1. Prozess stoppen
$proc = Get-Process -Name "ClawdDotNet" -ErrorAction SilentlyContinue
if ($proc) {
Write-Host "Stoppe ClawdDotNet..." -ForegroundColor Yellow
$proc | Stop-Process -Force -Confirm:$false
Start-Sleep -Seconds 2
$timeout = 15
while ((Get-Process -Name "ClawdDotNet" -ErrorAction SilentlyContinue) -and $timeout -gt 0) {
Start-Sleep -Seconds 1
$timeout--
}
if ($timeout -eq 0) {
Write-Host "WARNUNG: Prozess konnte nicht gestoppt werden!" -ForegroundColor Red
exit 1
}
Write-Host "Gestoppt." -ForegroundColor Green
} else {
Write-Host "ClawdDotNet laeuft nicht - kein Stopp noetig." -ForegroundColor Gray
}
# 2. Backup erstellen (nur eigene DLLs)
$backupDir = Join-Path $TargetDir ("_backup_" + (Get-Date -Format "yyyyMMdd_HHmmss"))
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
Get-ChildItem $TargetDir -File | Where-Object { $_.Name -match '^ClawdDotNet\.' } | ForEach-Object {
Copy-Item $_.FullName $backupDir
}
Write-Host "Backup erstellt: $backupDir" -ForegroundColor Gray
# 3. Neue Dateien kopieren
$sourceDir = $PSScriptRoot
$sourceFull = (Resolve-Path $sourceDir).Path.TrimEnd('\')
$targetFull = (Resolve-Path $TargetDir).Path.TrimEnd('\')
if ($sourceFull -eq $targetFull) {
# ZIP wurde direkt im Zielordner entpackt - Dateien sind schon da
Write-Host "ZIP wurde direkt im Zielordner entpackt - Dateien bereits vorhanden." -ForegroundColor Yellow
$fileCount = (Get-ChildItem $sourceDir -File | Where-Object { $_.Name -notin @('Deploy-Install.ps1') }).Count
} else {
$fileCount = 0
Get-ChildItem $sourceDir -File | Where-Object { $_.Name -notin @('Deploy-Install.ps1') } | ForEach-Object {
Copy-Item $_.FullName $TargetDir -Force
$fileCount++
}
# Unterordner kopieren (falls vorhanden, z.B. bei Full-Deploy)
Get-ChildItem $sourceDir -Directory | Where-Object { $_.Name -notmatch '^_backup' } | ForEach-Object {
$destSubDir = Join-Path $TargetDir $_.Name
Copy-Item $_.FullName $destSubDir -Recurse -Force
$fileCount += (Get-ChildItem $_.FullName -Recurse -File).Count
}
}
Write-Host "$fileCount Dateien aktualisiert." -ForegroundColor Green
# 4. Neu starten
Write-Host "Starte ClawdDotNet..." -ForegroundColor Cyan
$exePath = Join-Path $TargetDir "ClawdDotNet.exe"
Start-Process $exePath -WorkingDirectory $TargetDir
Start-Sleep -Seconds 3
if (Get-Process -Name "ClawdDotNet" -ErrorAction SilentlyContinue) {
Write-Host ""
Write-Host "=== Update erfolgreich! ClawdDotNet laeuft. ===" -ForegroundColor Green
} else {
Write-Host ""
Write-Host "=== WARNUNG: Prozess nicht gefunden. Bitte manuell pruefen. ===" -ForegroundColor Yellow
}
# 5. Alte Backups aufraeumen (behalte die letzten 5)
$oldBackups = Get-ChildItem $TargetDir -Directory | Where-Object { $_.Name -match '^_backup_' } | Sort-Object Name -Descending | Select-Object -Skip 5
foreach ($old in $oldBackups) {
Remove-Item $old.FullName -Recurse -Force
Write-Host "Altes Backup entfernt: $($old.Name)" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Fertig." -ForegroundColor Green
'@
$installScriptPath = Join-Path $stagingDir "Deploy-Install.ps1"
# UTF-8 OHNE BOM - wichtig fuer PowerShell 5.1 auf Windows Server
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($installScriptPath, $installScript, $utf8NoBom)
# ─── 4. ZIP erstellen ───
if (-not (Test-Path $deployDir)) { New-Item -ItemType Directory -Path $deployDir -Force | Out-Null }
Write-Host "`n=== Erstelle ZIP: $zipName ===" -ForegroundColor Cyan
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
Compress-Archive -Path "$stagingDir\*" -DestinationPath $zipPath -CompressionLevel Optimal
# Staging aufraeumen
Remove-Item $stagingDir -Recurse -Force
# ─── 5. Zusammenfassung ───
$zipSize = (Get-Item $zipPath).Length
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " Deployment-Paket erstellt!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host " Modus: $( if ($Full) { 'FULL (alle Dateien)' } else { 'QUICK (nur eigene DLLs)' } )"
Write-Host " Datei: $zipPath"
Write-Host " Groesse: $([math]::Round($zipSize/1KB, 0)) KB ($([math]::Round($zipSize/1MB, 1)) MB)"
Write-Host ""
Write-Host " Naechste Schritte:" -ForegroundColor Yellow
Write-Host " 1. ZIP per AnyDesk auf den Server kopieren"
Write-Host " 2. Auf dem Server entpacken"
Write-Host " 3. Deploy-Install.ps1 als Admin ausfuehren"
Write-Host ""
# ZIP-Ordner im Explorer oeffnen
Start-Process "explorer.exe" "/select,`"$zipPath`""
-9
View File
@@ -1,9 +0,0 @@
window.__bridge = {
receive(msg) {
document.dispatchEvent(
new CustomEvent('bridge:' + msg.type, { detail: msg }));
},
send(msg) {
window.chrome.webview.postMessage(JSON.stringify(msg));
}
};
-72
View File
@@ -1,72 +0,0 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-dark: #1e1e1e;
--bg-sidebar: #252526;
--bg-input: #2d2d2d;
--bg-bubble-user: #264f78;
--bg-bubble-agent: #333333;
--text: #cccccc;
--text-bright: #e0e0e0;
--text-dim: #888888;
--accent: #569cd6;
--border: #3e3e3e;
--hover: #2a2d2e;
}
html, body { height: 100%; font-family: 'Segoe UI', sans-serif; background: var(--bg-dark); color: var(--text); }
#app { display: flex; flex-direction: column; height: 100%; }
#chat-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 20px; border-bottom: 1px solid var(--border);
background: var(--bg-sidebar);
}
#chat-agent-name { font-size: 15px; font-weight: 600; color: var(--text-bright); }
#chat-header-actions { display: flex; gap: 8px; }
#chat-header-actions button {
background: transparent; border: 1px solid var(--border); color: var(--text);
padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;
}
#chat-header-actions button:hover { background: var(--hover); border-color: var(--accent); }
#chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
.chat-bubble {
max-width: 75%; padding: 10px 14px; border-radius: 10px;
font-size: 13px; line-height: 1.5; word-wrap: break-word;
white-space: pre-wrap;
}
.chat-bubble.user { background: var(--bg-bubble-user); color: var(--text-bright); align-self: flex-end; border-bottom-right-radius: 2px; }
.chat-bubble.assistant { background: var(--bg-bubble-agent); color: var(--text); align-self: flex-start; border-bottom-left-radius: 2px; }
.chat-bubble .timestamp { display: block; font-size: 10px; color: var(--text-dim); margin-top: 4px; }
.typing-indicator { align-self: flex-start; padding: 10px 14px; background: var(--bg-bubble-agent); border-radius: 10px; }
.typing-indicator span { display: inline-block; width: 6px; height: 6px; background: var(--text-dim); border-radius: 50%; margin: 0 2px; animation: typing 1.4s infinite; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-4px); } }
#chat-input-area {
display: flex; align-items: flex-end; gap: 8px;
padding: 12px 20px; border-top: 1px solid var(--border);
background: var(--bg-sidebar);
}
#chat-input {
flex: 1; resize: none; border: 1px solid var(--border); border-radius: 6px;
background: var(--bg-input); color: var(--text-bright); padding: 10px 12px;
font-family: inherit; font-size: 13px; line-height: 1.4;
max-height: 120px; outline: none;
}
#chat-input:focus { border-color: var(--accent); }
#btn-send {
background: var(--accent); color: #fff; border: none; border-radius: 6px;
padding: 10px 18px; cursor: pointer; font-size: 13px; font-weight: 500;
}
#btn-send:hover { opacity: 0.85; }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #555; }
-27
View File
@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ClawdDotNet Chat</title>
<link rel="stylesheet" href="chat.css">
</head>
<body>
<div id="app">
<div id="chat-header">
<span id="chat-agent-name">Agent</span>
<div id="chat-header-actions">
<button id="btn-run-now" title="Jetzt ausführen">&#x25B6;</button>
<button id="btn-abort" title="Abbrechen" style="display:none;">&#x25A0;</button>
</div>
</div>
<div id="chat-messages"></div>
<div id="chat-input-area">
<textarea id="chat-input" placeholder="Nachricht eingeben..." rows="1"></textarea>
<button id="btn-send">Senden</button>
</div>
</div>
<script src="bridge.js"></script>
<script src="chat.js"></script>
</body>
</html>
-135
View File
@@ -1,135 +0,0 @@
(function () {
'use strict';
const agentId = new URLSearchParams(location.search).get('agent');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const btnSend = document.getElementById('btn-send');
const chatAgentName = document.getElementById('chat-agent-name');
const btnRunNow = document.getElementById('btn-run-now');
const btnAbort = document.getElementById('btn-abort');
// ─── Bridge Events ───
document.addEventListener('bridge:agent_list_update', e => {
const data = e.detail.extra;
const list = Array.isArray(data) ? data : (data?.agents ?? []);
const agent = list.find(a => a.agentId === agentId);
if (agent) chatAgentName.textContent = agent.displayName;
});
document.addEventListener('bridge:chat_history', e => {
if (e.detail.agentId !== agentId) return;
chatMessages.innerHTML = '';
const history = e.detail.extra;
if (Array.isArray(history)) {
history.forEach(entry => appendBubble(entry.role, entry.content, entry.timestamp));
}
});
document.addEventListener('bridge:chat_message', e => {
if (e.detail.agentId !== agentId) return;
removeTypingIndicator();
const extra = e.detail.extra ?? {};
appendBubble(extra.role ?? 'assistant', e.detail.content, extra.timestamp);
});
document.addEventListener('bridge:chat_typing', e => {
if (e.detail.agentId !== agentId) return;
showTypingIndicator();
});
document.addEventListener('bridge:run_started', e => {
if (e.detail.agentId !== agentId) return;
btnAbort.style.display = 'inline-block';
});
document.addEventListener('bridge:run_finished', e => {
if (e.detail.agentId !== agentId) return;
btnAbort.style.display = 'none';
removeTypingIndicator();
});
// ─── Chat Bubbles ───
function appendBubble(role, content, timestamp) {
const bubble = document.createElement('div');
bubble.className = 'chat-bubble ' + (role === 'user' ? 'user' : 'assistant');
bubble.textContent = content || '';
if (timestamp) {
const ts = document.createElement('span');
ts.className = 'timestamp';
ts.textContent = formatTime(timestamp);
bubble.appendChild(ts);
}
chatMessages.appendChild(bubble);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function showTypingIndicator() {
removeTypingIndicator();
const indicator = document.createElement('div');
indicator.className = 'typing-indicator';
indicator.id = 'typing';
indicator.innerHTML = '<span></span><span></span><span></span>';
chatMessages.appendChild(indicator);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function removeTypingIndicator() {
document.getElementById('typing')?.remove();
}
// ─── Input ───
btnSend.addEventListener('click', sendMessage);
chatInput.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
chatInput.addEventListener('input', () => {
chatInput.style.height = 'auto';
chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
});
function sendMessage() {
const text = chatInput.value.trim();
if (!text || !agentId) return;
chatInput.value = '';
chatInput.style.height = 'auto';
window.__bridge.send({
type: 'user_message',
agentId: agentId,
content: text
});
}
// ─── Header Buttons ───
btnRunNow.addEventListener('click', () => {
if (!agentId) return;
window.__bridge.send({ type: 'run_now', agentId: agentId });
});
btnAbort.addEventListener('click', () => {
if (!agentId) return;
window.__bridge.send({ type: 'abort_run', agentId: agentId });
});
// ─── Helpers ───
function formatTime(ts) {
try {
const d = new Date(ts);
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
} catch { return ''; }
}
})();
-117
View File
@@ -1,117 +0,0 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-dark: #1e1e1e;
--bg-sidebar: #252526;
--bg-chat: #1e1e1e;
--bg-input: #2d2d2d;
--bg-bubble-user: #264f78;
--bg-bubble-agent: #333333;
--text: #cccccc;
--text-bright: #e0e0e0;
--text-dim: #888888;
--accent: #569cd6;
--border: #3e3e3e;
--hover: #2a2d2e;
--agent-active: #37373d;
--status-running: #4ec9b0;
--status-idle: #4ec94e;
--status-offline: #888888;
--status-error: #f44747;
}
html, body { height: 100%; font-family: 'Segoe UI', sans-serif; background: var(--bg-dark); color: var(--text); }
#app { display: flex; height: 100%; }
/* ─── Sidebar ─── */
#sidebar { width: 280px; min-width: 220px; background: var(--bg-sidebar); border-right: 1px solid var(--border); display: flex; flex-direction: column; }
#sidebar-header { padding: 16px; border-bottom: 1px solid var(--border); }
#sidebar-header h2 { font-size: 14px; font-weight: 600; color: var(--text-bright); text-transform: uppercase; letter-spacing: 1px; }
#agent-list { flex: 1; overflow-y: auto; padding: 8px; }
.agent-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; margin-bottom: 4px; border-radius: 6px;
cursor: pointer; transition: background 0.15s;
}
.agent-item:hover { background: var(--hover); }
.agent-item.active { background: var(--agent-active); border-left: 3px solid var(--accent); }
.agent-status-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
background: var(--status-idle);
}
.agent-status-dot.running { background: var(--status-running); animation: pulse 1.5s infinite; }
.agent-status-dot.offline { background: var(--status-offline); }
.agent-status-dot.error { background: var(--status-error); }
.agent-info { flex: 1; min-width: 0; }
.agent-name { font-size: 13px; font-weight: 500; color: var(--text-bright); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-model { font-size: 11px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
/* ─── Chat Area ─── */
#chat-area { flex: 1; display: flex; flex-direction: column; background: var(--bg-chat); }
#chat-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 20px; border-bottom: 1px solid var(--border);
background: var(--bg-sidebar);
}
#chat-agent-name { font-size: 15px; font-weight: 600; color: var(--text-bright); }
#chat-header-actions { display: flex; gap: 8px; }
#chat-header-actions button {
background: transparent; border: 1px solid var(--border); color: var(--text);
padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;
}
#chat-header-actions button:hover { background: var(--hover); border-color: var(--accent); }
#chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
#empty-state { display: flex; align-items: center; justify-content: center; height: 100%; }
#empty-state p { color: var(--text-dim); font-size: 14px; }
.chat-bubble {
max-width: 75%; padding: 10px 14px; border-radius: 10px;
font-size: 13px; line-height: 1.5; word-wrap: break-word;
white-space: pre-wrap;
}
.chat-bubble.user { background: var(--bg-bubble-user); color: var(--text-bright); align-self: flex-end; border-bottom-right-radius: 2px; }
.chat-bubble.assistant { background: var(--bg-bubble-agent); color: var(--text); align-self: flex-start; border-bottom-left-radius: 2px; }
.chat-bubble .timestamp { display: block; font-size: 10px; color: var(--text-dim); margin-top: 4px; }
.typing-indicator { align-self: flex-start; padding: 10px 14px; background: var(--bg-bubble-agent); border-radius: 10px; }
.typing-indicator span { display: inline-block; width: 6px; height: 6px; background: var(--text-dim); border-radius: 50%; margin: 0 2px; animation: typing 1.4s infinite; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-4px); } }
/* ─── Input ─── */
#chat-input-area {
display: flex; align-items: flex-end; gap: 8px;
padding: 12px 20px; border-top: 1px solid var(--border);
background: var(--bg-sidebar);
}
#chat-input {
flex: 1; resize: none; border: 1px solid var(--border); border-radius: 6px;
background: var(--bg-input); color: var(--text-bright); padding: 10px 12px;
font-family: inherit; font-size: 13px; line-height: 1.4;
max-height: 120px; outline: none;
}
#chat-input:focus { border-color: var(--accent); }
#btn-send {
background: var(--accent); color: #fff; border: none; border-radius: 6px;
padding: 10px 18px; cursor: pointer; font-size: 13px; font-weight: 500;
white-space: nowrap;
}
#btn-send:hover { opacity: 0.85; }
#btn-send:disabled { opacity: 0.4; cursor: default; }
/* ─── Scrollbar ─── */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #555; }
-39
View File
@@ -1,39 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ClawdDotNet Agent Chat</title>
<link rel="stylesheet" href="overview.css">
</head>
<body>
<div id="app">
<aside id="sidebar">
<div id="sidebar-header">
<h2>Agenten</h2>
</div>
<div id="agent-list"></div>
</aside>
<main id="chat-area">
<div id="chat-header">
<span id="chat-agent-name">Wähle einen Agenten</span>
<div id="chat-header-actions">
<button id="btn-open-window" title="In eigenem Fenster öffnen" style="display:none;">&#x2197;</button>
<button id="btn-run-now" title="Jetzt ausführen" style="display:none;">&#x25B6;</button>
</div>
</div>
<div id="chat-messages">
<div id="empty-state">
<p>Wähle einen Agenten aus der Liste, um den Chat zu starten.</p>
</div>
</div>
<div id="chat-input-area" style="display:none;">
<textarea id="chat-input" placeholder="Nachricht eingeben..." rows="1"></textarea>
<button id="btn-send">Senden</button>
</div>
</main>
</div>
<script src="bridge.js"></script>
<script src="overview.js"></script>
</body>
</html>
-191
View File
@@ -1,191 +0,0 @@
(function () {
'use strict';
let selectedAgentId = null;
let agents = [];
const agentList = document.getElementById('agent-list');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const btnSend = document.getElementById('btn-send');
const chatAgentName = document.getElementById('chat-agent-name');
const chatInputArea = document.getElementById('chat-input-area');
const emptyState = document.getElementById('empty-state');
const btnOpenWindow = document.getElementById('btn-open-window');
const btnRunNow = document.getElementById('btn-run-now');
// ─── Bridge Events ───
document.addEventListener('bridge:agent_list_update', e => {
const data = e.detail.extra;
agents = Array.isArray(data) ? data : (data?.agents ?? []);
renderSidebar();
});
document.addEventListener('bridge:agent_status', e => {
const d = e.detail;
const agent = agents.find(a => a.agentId === d.agentId);
if (agent) {
agent.status = d.status;
renderSidebar();
}
});
document.addEventListener('bridge:chat_history', e => {
if (e.detail.agentId !== selectedAgentId) return;
const history = e.detail.extra;
chatMessages.innerHTML = '';
if (Array.isArray(history)) {
history.forEach(entry => appendBubble(entry.role, entry.content, entry.timestamp));
}
});
document.addEventListener('bridge:chat_message', e => {
if (e.detail.agentId !== selectedAgentId) return;
removeTypingIndicator();
const extra = e.detail.extra ?? {};
appendBubble(extra.role ?? 'assistant', e.detail.content, extra.timestamp);
});
document.addEventListener('bridge:chat_typing', e => {
if (e.detail.agentId !== selectedAgentId) return;
showTypingIndicator();
});
document.addEventListener('bridge:run_started', e => {
const agent = agents.find(a => a.agentId === e.detail.agentId);
if (agent) { agent.status = 'running'; renderSidebar(); }
});
document.addEventListener('bridge:run_finished', e => {
const agent = agents.find(a => a.agentId === e.detail.agentId);
if (agent) { agent.status = 'idle'; renderSidebar(); }
removeTypingIndicator();
});
// ─── Sidebar ───
function renderSidebar() {
agentList.innerHTML = '';
agents.forEach(agent => {
const item = document.createElement('div');
item.className = 'agent-item' + (agent.agentId === selectedAgentId ? ' active' : '');
item.innerHTML = `
<div class="agent-status-dot ${agent.status || 'idle'}"></div>
<div class="agent-info">
<div class="agent-name">${escapeHtml(agent.displayName)}</div>
<div class="agent-model">${escapeHtml(agent.model || '')}</div>
</div>`;
item.addEventListener('click', () => selectAgent(agent.agentId));
agentList.appendChild(item);
});
}
function selectAgent(agentId) {
selectedAgentId = agentId;
const agent = agents.find(a => a.agentId === agentId);
chatAgentName.textContent = agent ? agent.displayName : agentId;
chatInputArea.style.display = 'flex';
emptyState?.remove();
btnOpenWindow.style.display = 'inline-block';
btnRunNow.style.display = 'inline-block';
chatMessages.innerHTML = '';
renderSidebar();
window.__bridge.send({
type: 'select_agent',
agentId: agentId
});
}
// ─── Chat Bubbles ───
function appendBubble(role, content, timestamp) {
const bubble = document.createElement('div');
bubble.className = 'chat-bubble ' + (role === 'user' ? 'user' : 'assistant');
bubble.textContent = content || '';
if (timestamp) {
const ts = document.createElement('span');
ts.className = 'timestamp';
ts.textContent = formatTime(timestamp);
bubble.appendChild(ts);
}
chatMessages.appendChild(bubble);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function showTypingIndicator() {
removeTypingIndicator();
const indicator = document.createElement('div');
indicator.className = 'typing-indicator';
indicator.id = 'typing';
indicator.innerHTML = '<span></span><span></span><span></span>';
chatMessages.appendChild(indicator);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function removeTypingIndicator() {
document.getElementById('typing')?.remove();
}
// ─── Input Handling ───
btnSend.addEventListener('click', sendMessage);
chatInput.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
chatInput.addEventListener('input', () => {
chatInput.style.height = 'auto';
chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
});
function sendMessage() {
const text = chatInput.value.trim();
if (!text || !selectedAgentId) return;
chatInput.value = '';
chatInput.style.height = 'auto';
window.__bridge.send({
type: 'user_message',
agentId: selectedAgentId,
content: text
});
}
// ─── Header Buttons ───
btnOpenWindow.addEventListener('click', () => {
if (!selectedAgentId) return;
window.__bridge.send({ type: 'open_agent_chat', agentId: selectedAgentId });
});
btnRunNow.addEventListener('click', () => {
if (!selectedAgentId) return;
window.__bridge.send({ type: 'run_now', agentId: selectedAgentId });
});
// ─── Helpers ───
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function formatTime(ts) {
try {
const d = new Date(ts);
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
} catch { return ''; }
}
})();
-8
View File
@@ -1,8 +0,0 @@
Agenten sollen den Hinweis bekommen, das sie mich informieren sollen, wenn sie der Meinung sind das ssie ein weiteres Tool benötigen, das ihnen derzeit nicht zur Verfügung steht.
SharedWorkspace für die gesamte Instanz hinzufügen ?
Ein Ort im Dateisystem, wo sie gemeinsam an verschiedenen Dateien / Projekten arbeiten können?
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

-200
View File
@@ -1,200 +0,0 @@
using System.ComponentModel;
using ClawdDotNet.Core.Config;
namespace ClawdDotNet.Models;
/// <summary>Bietet die drei gültigen Prompt-Caching-Werte als Auswahlliste an.</summary>
public sealed class PromptCachingConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
=> new(new[] { "auto", "on", "off" });
}
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class AgentSettingsViewModel
{
private readonly AgentConfig _config;
public AgentSettingsViewModel(AgentConfig config)
{
_config = config;
}
// ──────────────── Identity ────────────────
[Category("1 - Identity")]
[DisplayName("Agent-ID")]
[Description("Eindeutige ID des Agenten. Wird intern und in Logs verwendet.")]
[ReadOnly(true)]
public string AgentId => _config.AgentId;
[Category("1 - Identity")]
[DisplayName("Anzeigename")]
[Description("Freundlicher Name des Agenten (z.B. 'Marktanalyst').")]
public string DisplayName
{
get => _config.DisplayName;
set => _config.DisplayName = value;
}
[Category("1 - Identity")]
[DisplayName("Modell")]
[Description("LLM-Modell das dieser Agent verwendet. Dropdown zeigt verfügbare Modelle von OpenRouter.")]
[TypeConverter(typeof(ModelTypeConverter))]
public string Model
{
get => _config.Model;
set => _config.Model = value;
}
[Category("1 - Identity")]
[DisplayName("Identity")]
[Description("Aus Identity.md geladen definiert WER der Agent ist. Bearbeitung über den Toolbar-Button 'Identity bearbeiten'.")]
[ReadOnly(true)]
public string IdentityStatus =>
string.IsNullOrWhiteSpace(_config.Identity) ? "(nicht definiert)" : $"✔ {_config.Identity.Split('\n').Length} Zeilen";
[Category("1 - Identity")]
[DisplayName("Soul")]
[Description("Aus Soul.md geladen definiert WIE der Agent denkt. Bearbeitung über den Toolbar-Button 'Soul bearbeiten'.")]
[ReadOnly(true)]
public string SoulStatus =>
string.IsNullOrWhiteSpace(_config.Soul) ? "(nicht definiert)" : $"✔ {_config.Soul.Split('\n').Length} Zeilen";
// ──────────────── Loop-Schutz ────────────────
[Category("2 - Loop-Schutz")]
[DisplayName("Max. Schritte pro Run")]
[Description("Maximale Anzahl LLM-Aufrufe pro Run. Verhindert Endlosschleifen bei fehlerhaften Tool-Calls.")]
public int MaxSteps
{
get => _config.LoopGuard.MaxSteps;
set => _config.LoopGuard.MaxSteps = Math.Max(1, value);
}
[Category("2 - Loop-Schutz")]
[DisplayName("Kostenbudget pro Run (Tokens)")]
[Description("Summe ALLER abgerechneten Tokens eines Runs über alle Schritte. " +
"Da jeder Schritt den kompletten Kontext erneut sendet, wächst diese Summe " +
"überproportional — der Wert liegt deutlich über der Kontextgröße. " +
"Für die Kontextgröße ist 'Max. Kontext-Tokens' zuständig.")]
public int MaxCumulativeTokens
{
get => _config.LoopGuard.MaxCumulativeTokens;
set => _config.LoopGuard.MaxCumulativeTokens = Math.Max(10_000, value);
}
[Category("2 - Loop-Schutz")]
[DisplayName("Timeout (Sekunden)")]
[Description("Maximale Laufzeit pro Run in Sekunden. Danach wird der Run abgebrochen.")]
public int TimeoutSeconds
{
get => _config.LoopGuard.TimeoutSeconds;
set => _config.LoopGuard.TimeoutSeconds = Math.Max(10, value);
}
// ──────────────── Kontext-Management ────────────────
[Category("3 - Kontext-Management")]
[DisplayName("Max. Kontext-Tokens")]
[Description("Maximales Token-Budget für den gesamten Konversationskontext. Bei Überschreitung wird automatisch kompaktiert.")]
public int MaxContextTokens
{
get => _config.LoopGuard.MaxContextTokens;
set => _config.LoopGuard.MaxContextTokens = Math.Max(10_000, value);
}
[Category("3 - Kontext-Management")]
[DisplayName("Kompaktierungs-Schwelle (%)")]
[Description("Ab welchem Prozentsatz der Max. Kontext-Tokens wird kompaktiert. 80 = bei 80% Auslastung. Stufe 1: Tool-Results kürzen. Stufe 2: LLM-Zusammenfassung.")]
public int CompactionThresholdPercent
{
get => (int)(_config.LoopGuard.CompactionThreshold * 100);
set => _config.LoopGuard.CompactionThreshold = Math.Clamp(value, 50, 95) / 100.0;
}
[Category("3 - Kontext-Management")]
[DisplayName("Modell für Zusammenfassungen")]
[Description("Modell, mit dem beim Kompaktieren zusammengefasst wird. Leer = Modell des Agenten. " +
"Zusammenfassen ist anspruchslos — ein günstiges Modell spart hier deutlich, " +
"da bis zu 30.000 Zeichen verarbeitet werden.")]
public string SummaryModel
{
get => _config.LoopGuard.SummaryModel;
set => _config.LoopGuard.SummaryModel = value ?? "";
}
[Category("3 - Kontext-Management")]
[DisplayName("Max. Zeichen pro Tool-Ergebnis")]
[Description("Längere Tool-Ergebnisse werden gekürzt, bevor sie in den Kontext gelangen. " +
"Ohne Grenze kann ein einzelner Abruf den Kontext sprengen — 512 KB entsprechen " +
"etwa 130.000 Tokens in einer einzigen Antwort.")]
public int MaxToolResultChars
{
get => _config.MaxToolResultChars;
set => _config.MaxToolResultChars = Math.Max(1_000, value);
}
// ──────────────── Kosten ────────────────
[Category("5 - Kosten")]
[DisplayName("Prompt-Caching")]
[Description("auto = für Modelle aktivieren, die es unterstützen; on = erzwingen; off = aus. " +
"Spart erheblich, weil jeder Schritt eines Runs den kompletten Prompt erneut sendet — " +
"System-Prompt und Tool-Definitionen also dutzendfach.")]
[TypeConverter(typeof(PromptCachingConverter))]
public string PromptCaching
{
get => _config.PromptCaching;
set => _config.PromptCaching = string.IsNullOrWhiteSpace(value) ? "auto" : value;
}
[Category("5 - Kosten")]
[DisplayName("Tagesbudget (USD)")]
[Description("Hartes Tageslimit für diesen Agenten in US-Dollar. 0 = kein Limit. " +
"Bei Überschreitung wird kein Lauf mehr gestartet. " +
"Greift nur, wenn für das Modell Preise vorliegen — sonst zusätzlich " +
"das Token-Tageslimit setzen.")]
public decimal DailyBudgetUsd
{
get => _config.Budget.DailyCostUsd;
set => _config.Budget.DailyCostUsd = Math.Max(0, value);
}
[Category("5 - Kosten")]
[DisplayName("Token-Tageslimit")]
[Description("Hartes Tageslimit für diesen Agenten in Tokens. 0 = kein Limit. " +
"Wirkt auch dann, wenn für das Modell keine Preise vorliegen.")]
public long DailyTokenLimit
{
get => _config.Budget.DailyTokens;
set => _config.Budget.DailyTokens = Math.Max(0, value);
}
// ──────────────── Tools (Read-Only) ────────────────
[Category("4 - Tools")]
[DisplayName("Zugewiesene Tools")]
[Description("Liste der Tool-Namen, die diesem Agent zugewiesen sind. Zuweisung über die Tabelle unten.")]
[ReadOnly(true)]
public string AssignedTools =>
_config.Tools.Count == 0
? "(keine)"
: string.Join(", ", _config.Tools.Keys);
[Category("4 - Tools")]
[DisplayName("Anzahl")]
[ReadOnly(true)]
public int ToolCount => _config.Tools.Count;
// ──────────────── Intern ────────────────
[Browsable(false)]
public AgentConfig UnderlyingConfig => _config;
public override string ToString() =>
string.IsNullOrWhiteSpace(_config.DisplayName) ? _config.AgentId : _config.DisplayName;
}
-8
View File
@@ -1,8 +0,0 @@
namespace ClawdDotNet.Models;
public sealed class AgentToolDisplayEntry
{
public bool Assigned { get; set; }
public string ToolName { get; set; } = "";
public string Description { get; set; } = "";
}
-124
View File
@@ -1,124 +0,0 @@
using System.ComponentModel;
using ClawdDotNet.Core.Config;
namespace ClawdDotNet.Models;
/// <summary>
/// PropertyGrid-freundlicher Wrapper um InstanceConfig.
/// Änderungen werden direkt im zugrunde liegenden InstanceConfig-Objekt gespeichert.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class InstanceSettingsViewModel
{
private readonly InstanceConfig _config;
public InstanceSettingsViewModel(InstanceConfig config)
{
_config = config;
}
[Category("Instanz")]
[DisplayName("Instanz-ID")]
[Description("Eindeutige ID dieser laufenden Instanz.")]
public string InstanceId
{
get => _config.InstanceId;
set => _config.InstanceId = value;
}
[Category("Instanz")]
[DisplayName("Instanzname")]
[Description("Anzeigename dieser Instanz (z.B. 'Aktien-Team').")]
public string InstanceName
{
get => _config.InstanceName;
set => _config.InstanceName = value;
}
[Category("API")]
[DisplayName("OpenRouter API-Key")]
[Description("API-Schlüssel für OpenRouter. Wird für alle Agenten dieser Instanz verwendet.")]
[PasswordPropertyText(true)]
public string OpenRouterApiKey
{
get => _config.OpenRouterApiKey;
set => _config.OpenRouterApiKey = value;
}
[Category("Verzeichnisse")]
[DisplayName("Arbeitsverzeichnis")]
[Description("Basis-Arbeitsverzeichnis für diese Instanz.")]
public string WorkingDirectory
{
get => _config.WorkingDirectory;
set => _config.WorkingDirectory = value;
}
[Category("Verzeichnisse")]
[DisplayName("Log-Verzeichnis")]
[Description("Verzeichnis für Log-Dateien dieser Instanz.")]
public string LogDirectory
{
get => _config.LogDirectory;
set => _config.LogDirectory = value;
}
[Category("Netzwerk")]
[DisplayName("Webserver-Port")]
[Description("Port für den integrierten Webserver (0 = deaktiviert).")]
public int WebServerPort
{
get => _config.WebServerPort;
set => _config.WebServerPort = value;
}
[Category("Agenten")]
[DisplayName("Anzahl Agenten")]
[Description("Anzahl der konfigurierten Agenten in dieser Instanz.")]
[ReadOnly(true)]
public int AgentCount => _config.Agents.Count;
// ─── WatchDog (Instanz-Heartbeat) ───
// Server-URL und Master-Token stehen app-weit in den Anwendungseinstellungen. Die
// Instanz registriert sich damit selbst; hier stehen nur Source/Gruppe/Intervall.
// Ein/Aus läuft über den Service „Instanz-Watchdog" im Worker-Tab. Änderungen greifen
// beim nächsten Start der Instanz.
[Category("WatchDog")]
[DisplayName("Source")]
[Description("Dienst-Kennung im WatchDog. Alle Instanzen teilen sich dieselbe Source.")]
public string WatchdogSource
{
get => _config.Watchdog.Source;
set => _config.Watchdog.Source = value;
}
[Category("WatchDog")]
[DisplayName("Gruppe")]
[Description("Gruppierung im WatchDog-Dashboard.")]
public string WatchdogGroup
{
get => _config.Watchdog.Group;
set => _config.Watchdog.Group = value;
}
[Category("WatchDog")]
[DisplayName("Intervall (Sek)")]
[Description("Sende-Takt der Heartbeats in Sekunden (mindestens 5).")]
public int WatchdogIntervalSeconds
{
get => _config.Watchdog.IntervalSeconds;
set => _config.Watchdog.IntervalSeconds = value;
}
[Category("WatchDog")]
[DisplayName("Token registriert")]
[Description("Ob diese Instanz bereits einen eigenen Agent-Token vom WatchDog erhalten hat.")]
[ReadOnly(true)]
public bool WatchdogTokenRegistered => _config.Watchdog.HasToken;
[Browsable(false)]
public InstanceConfig UnderlyingConfig => _config;
public override string ToString() => _config.InstanceName;
}
-17
View File
@@ -1,17 +0,0 @@
namespace ClawdDotNet.Models;
public sealed class JobDisplayEntry
{
public string JobType { get; set; } = "Agent Wakeup";
public string AgentId { get; set; } = "";
public string AgentName { get; set; } = "";
public string ToolName { get; set; } = "";
public string CronExpression { get; set; } = "";
public string TaskMessage { get; set; } = "";
public string NextRun { get; set; } = "—";
public string LastRun { get; set; } = "—";
public string LastStatus { get; set; } = "—";
public bool RunOnStart { get; set; }
public string Status { get; set; } = "Aktiv";
public string JobId { get; set; } = "";
}
-94
View File
@@ -1,94 +0,0 @@
using System.ComponentModel;
using ClawdDotNet.Core.Api;
using ClawdDotNet.Core.Api.Models;
namespace ClawdDotNet.Models;
/// <summary>
/// TypeConverter der im PropertyGrid eine Dropdown-Liste
/// mit verfügbaren OpenRouter-Modellen anzeigt.
/// Die Modelle werden einmalig per API abgerufen und gecached.
/// Freitext-Eingabe bleibt weiterhin möglich (CanConvertFrom = true).
/// </summary>
public sealed class ModelTypeConverter : StringConverter
{
private static List<ModelInfo>? _cachedModels;
private static bool _fetchInProgress;
private static readonly Lock _lock = new();
/// <summary>
/// Wird von außen gesetzt (beim Start der Anwendung), damit
/// der Converter Zugriff auf den OpenRouterClient hat.
/// </summary>
public static OpenRouterClient? Client { get; set; }
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
/// <summary>false = Dropdown ist editierbar (Freitext erlaubt)</summary>
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => false;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
EnsureModelsLoaded();
if (_cachedModels is null || _cachedModels.Count == 0)
{
// Fallback: Einige gängige Modelle
return new StandardValuesCollection(new[]
{
"anthropic/claude-sonnet-4-5",
"anthropic/claude-haiku-4",
"openai/gpt-4.1",
"openai/gpt-4.1-mini",
"google/gemini-2.5-pro-preview",
"google/gemini-2.5-flash-preview",
"deepseek/deepseek-chat-v3-0324",
"meta-llama/llama-4-maverick"
});
}
var ids = _cachedModels.Select(m => m.Id).ToArray();
return new StandardValuesCollection(ids);
}
private static void EnsureModelsLoaded()
{
if (_cachedModels is not null || Client is null)
return;
lock (_lock)
{
if (_cachedModels is not null || _fetchInProgress)
return;
_fetchInProgress = true;
}
// Asynchronen Abruf im Hintergrund starten
_ = Task.Run(async () =>
{
try
{
var models = await Client.GetAvailableModelsAsync();
_cachedModels = models;
}
catch
{
_cachedModels = []; // Fehler → Fallback wird verwendet
}
finally
{
lock (_lock) { _fetchInProgress = false; }
}
});
}
/// <summary>
/// Kann von außen aufgerufen werden um den Cache zu leeren
/// (z.B. wenn sich der API-Key ändert).
/// </summary>
public static void InvalidateCache()
{
lock (_lock) { _cachedModels = null; }
}
}
-13
View File
@@ -1,13 +0,0 @@
namespace ClawdDotNet.Models;
public sealed class ServiceDisplayEntry
{
public string ServiceId { get; set; } = "";
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public int Port { get; set; }
public string Status { get; set; } = "Gestoppt";
public string StartedAt { get; set; } = "—";
public string Description { get; set; } = "";
public bool BuiltIn { get; set; }
}
-566
View File
@@ -1,566 +0,0 @@
using System.ComponentModel;
using System.Text.Json;
namespace ClawdDotNet.Models;
static class ConfigHelper
{
public static string GetString(Dictionary<string, object?> config, string key, string fallback = "")
{
var val = config.GetValueOrDefault(key);
return val switch
{
JsonElement je when je.ValueKind == JsonValueKind.String => je.GetString() ?? fallback,
JsonElement je => je.ToString(),
string s => s,
null => fallback,
_ => val.ToString() ?? fallback
};
}
public static int GetInt(Dictionary<string, object?> config, string key, int fallback = 0)
{
var val = config.GetValueOrDefault(key);
return val switch
{
JsonElement je when je.ValueKind == JsonValueKind.Number => je.GetInt32(),
JsonElement je => int.TryParse(je.ToString(), out var r) ? r : fallback,
int i => i,
_ => int.TryParse(val?.ToString(), out var r) ? r : fallback
};
}
public static bool GetBool(Dictionary<string, object?> config, string key, bool fallback = false)
{
var val = config.GetValueOrDefault(key);
return val switch
{
JsonElement je when je.ValueKind is JsonValueKind.True => true,
JsonElement je when je.ValueKind is JsonValueKind.False => false,
JsonElement je => bool.TryParse(je.ToString(), out var r) ? r : fallback,
bool b => b,
_ => bool.TryParse(val?.ToString(), out var r) ? r : fallback
};
}
public static string GetStringArray(Dictionary<string, object?> config, string key)
{
var val = config.GetValueOrDefault(key);
return val switch
{
JsonElement je when je.ValueKind == JsonValueKind.Array =>
string.Join(",", je.EnumerateArray().Select(e => e.GetString())),
object[] arr => string.Join(",", arr),
string s => s,
_ => ""
};
}
}
public enum FileRWAccessLevel
{
Denied,
Read,
ReadWrite,
Admin
}
public sealed class FileRWToolSettings
{
[Category("Persönlicher Workspace")]
[DisplayName("Erlaubte Endungen")]
[Description("Dateiendungen für den eigenen Agenten-Workspace (z.B. .txt,.json,.md)")]
public string PersonalAllowedExtensions { get; set; } = ".txt,.json,.md,.html,.js,.css";
[Category("Shared Workspace")]
[DisplayName("Zugriffslevel")]
[Description("Legt fest, welche Operationen im SharedWorkspace erlaubt sind")]
public FileRWAccessLevel SharedAccessLevel { get; set; } = FileRWAccessLevel.Denied;
[Category("Shared Workspace")]
[DisplayName("Erlaubte Endungen")]
[Description("Dateiendungen für den geteilten Workspace")]
public string SharedAllowedExtensions { get; set; } = ".txt,.json,.md";
[Category("Shared Workspace Schutz")]
[DisplayName("Geschützte Pfade")]
[Description("Komma-getrennte Pfade im SharedWorkspace die append-only sind (z.B. stocks/,archives/). Dateien dort können nur erstellt, nicht überschrieben oder gelöscht werden. Admin-Level umgeht den Schutz.")]
public string ProtectedPaths { get; set; } = "stocks/";
public Dictionary<string, object?> ToConfig() => new()
{
["personalAllowedExtensions"] = PersonalAllowedExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
["sharedAccessLevel"] = SharedAccessLevel.ToString(),
["sharedAllowedExtensions"] = SharedAllowedExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
["protectedPaths"] = ProtectedPaths.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
};
public static FileRWToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
PersonalAllowedExtensions = ConfigHelper.GetStringArray(config, "personalAllowedExtensions") is { Length: > 0 } s1
? s1 : (ConfigHelper.GetStringArray(config, "allowedExtensions") is { Length: > 0 } sOld ? sOld : ".txt,.json,.md,.html,.js,.css"),
SharedAccessLevel = Enum.TryParse<FileRWAccessLevel>(ConfigHelper.GetString(config, "sharedAccessLevel"), true, out var level)
? level : FileRWAccessLevel.Denied,
SharedAllowedExtensions = ConfigHelper.GetStringArray(config, "sharedAllowedExtensions") is { Length: > 0 } s2
? s2 : ".txt,.json,.md",
ProtectedPaths = ConfigHelper.GetStringArray(config, "protectedPaths") is { Length: > 0 } s3
? s3 : "stocks/"
};
}
public sealed class MailToolSettings
{
[Category("Mail - Konto")]
[DisplayName("Benutzername")]
public string Username { get; set; } = "";
[Category("Mail - Konto")]
[DisplayName("Passwort")]
[PasswordPropertyText(true)]
public string Password { get; set; } = "";
[Category("Mail - IMAP")]
[DisplayName("IMAP-Host")]
public string ImapHost { get; set; } = "";
[Category("Mail - IMAP")]
[DisplayName("IMAP-Port")]
public int ImapPort { get; set; } = 993;
[Category("Mail - SMTP")]
[DisplayName("SMTP-Host")]
public string SmtpHost { get; set; } = "";
[Category("Mail - SMTP")]
[DisplayName("SMTP-Port")]
public int SmtpPort { get; set; } = 587;
[Category("Mail - Sicherheit")]
[DisplayName("Erlaubte Empfänger")]
[Description("Komma-getrennte Liste erlaubter E-Mail-Adressen")]
public string AllowedRecipients { get; set; } = "";
public Dictionary<string, object?> ToConfig() => new()
{
["username"] = Username,
["password"] = Password,
["imapHost"] = ImapHost,
["imapPort"] = ImapPort,
["smtpHost"] = SmtpHost,
["smtpPort"] = SmtpPort,
["allowedRecipients"] = AllowedRecipients.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
};
public static MailToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
Username = ConfigHelper.GetString(config, "username"),
Password = ConfigHelper.GetString(config, "password"),
ImapHost = ConfigHelper.GetString(config, "imapHost"),
ImapPort = ConfigHelper.GetInt(config, "imapPort", 993),
SmtpHost = ConfigHelper.GetString(config, "smtpHost"),
SmtpPort = ConfigHelper.GetInt(config, "smtpPort", 587),
AllowedRecipients = ConfigHelper.GetStringArray(config, "allowedRecipients")
};
}
public enum DatabaseType
{
MySql,
Postgres,
MsSql,
MongoDb
}
public enum DatabaseAccessLevel
{
[Description("Nur Lesen (SELECT/find)")]
ReadOnly,
[Description("Lesen und Schreiben (INSERT/UPDATE/DELETE)")]
ReadWrite,
[Description("Vollzugriff (Admin/Schema-Änderungen)")]
Admin
}
public sealed class DatabaseToolSettings
{
[Category("Datenbank")]
[DisplayName("Typ")]
[Description("Der zu verwendende Datenbanktyp")]
public DatabaseType Type { get; set; } = DatabaseType.MySql;
[Category("Datenbank")]
[DisplayName("Connection-String")]
public string ConnectionString { get; set; } = "";
[Category("Datenbank")]
[DisplayName("Zugriffsebene")]
[Description("Legt fest, welche Operationen der Agent ausführen darf")]
public DatabaseAccessLevel AccessLevel { get; set; } = DatabaseAccessLevel.ReadOnly;
[Category("Datenbank - Sicherheit")]
[DisplayName("Erlaubte Tabellen")]
[Description("Komma-getrennte Liste erlaubter Tabellen/Collections")]
public string AllowedTables { get; set; } = "";
public Dictionary<string, object?> ToConfig() => new()
{
["type"] = Type.ToString().ToLowerInvariant(),
["connectionString"] = ConnectionString,
["accessLevel"] = AccessLevel.ToString(),
["allowedTables"] = AllowedTables.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
};
public static DatabaseToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
Type = Enum.TryParse<DatabaseType>(config.GetValueOrDefault("type")?.ToString(), true, out var result) ? result : DatabaseType.MySql,
ConnectionString = config.GetValueOrDefault("connectionString")?.ToString() ?? "",
AccessLevel = Enum.TryParse<DatabaseAccessLevel>(config.GetValueOrDefault("accessLevel")?.ToString() ?? config.GetValueOrDefault("allowWrite")?.ToString(), true, out var level)
? level
: (config.GetValueOrDefault("allowWrite") is true or "True" or "true" ? DatabaseAccessLevel.ReadWrite : DatabaseAccessLevel.ReadOnly),
AllowedTables = config.GetValueOrDefault("allowedTables") is object[] arr
? string.Join(",", arr)
: config.GetValueOrDefault("allowedTables")?.ToString() ?? ""
};
}
public sealed class FTPToolSettings
{
[Category("FTP Server")]
[DisplayName("Host")]
public string Host { get; set; } = "";
[Category("FTP Server")]
[DisplayName("Port")]
public int Port { get; set; } = 21;
[Category("FTP Server")]
[DisplayName("Benutzername")]
public string Username { get; set; } = "";
[Category("FTP Server")]
[DisplayName("Passwort")]
[PasswordPropertyText(true)]
public string Password { get; set; } = "";
[Category("FTP Lokal")]
[DisplayName("Root-Pfad")]
[Description("Basisverzeichnis für Dateiübertragungen")]
public string RootPath { get; set; } = "./data/";
public Dictionary<string, object?> ToConfig() => new()
{
["host"] = Host,
["port"] = Port,
["username"] = Username,
["password"] = Password,
["rootPath"] = RootPath
};
public static FTPToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
Host = ConfigHelper.GetString(config, "host"),
Port = ConfigHelper.GetInt(config, "port", 21),
Username = ConfigHelper.GetString(config, "username"),
Password = ConfigHelper.GetString(config, "password"),
RootPath = ConfigHelper.GetString(config, "rootPath", "./data/")
};
}
public sealed class TelegramToolSettings
{
[Category("Telegram")]
[DisplayName("Bot-Token")]
[PasswordPropertyText(true)]
public string BotToken { get; set; } = "";
[Category("Telegram")]
[DisplayName("Standard Chat-ID")]
[Description("Die Standard-ID, an die Nachrichten gesendet werden, wenn keine andere ID angegeben ist.")]
public string DefaultChatId { get; set; } = "";
[Category("Telegram - Sicherheit")]
[DisplayName("Erlaubte Chat-IDs")]
[Description("Komma-getrennte Liste erlaubter Chat-IDs")]
public string AllowedChatIds { get; set; } = "";
public Dictionary<string, object?> ToConfig() => new()
{
["botToken"] = BotToken,
["defaultChatId"] = DefaultChatId,
["allowedChatIds"] = AllowedChatIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
};
public static TelegramToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
BotToken = ConfigHelper.GetString(config, "botToken"),
DefaultChatId = ConfigHelper.GetString(config, "defaultChatId"),
AllowedChatIds = ConfigHelper.GetStringArray(config, "allowedChatIds")
};
}
public sealed class DirectAPIToolSettings
{
[Category("DirectAPI")]
[DisplayName("Standard-Provider")]
public string DefaultProvider { get; set; } = "twelvedata";
[Category("DirectAPI")]
[DisplayName("Cache TTL (Sekunden)")]
public int CacheTtlSeconds { get; set; } = 60;
[Category("DirectAPI - API Keys")]
[DisplayName("Twelve Data Key")]
[PasswordPropertyText(true)]
public string TwelveDataKey { get; set; } = "";
[Category("DirectAPI - API Keys")]
[DisplayName("Alpha Vantage Key")]
[PasswordPropertyText(true)]
public string AlphaVantageKey { get; set; } = "";
public Dictionary<string, object?> ToConfig() => new()
{
["defaultProvider"] = DefaultProvider,
["cacheTtlSeconds"] = CacheTtlSeconds,
["providers"] = new Dictionary<string, object?>
{
["twelvedata"] = new { apiKey = TwelveDataKey },
["alphavantage"] = new { apiKey = AlphaVantageKey }
}
};
public static DirectAPIToolSettings FromConfig(Dictionary<string, object?> config)
{
var settings = new DirectAPIToolSettings
{
DefaultProvider = ConfigHelper.GetString(config, "defaultProvider", "twelvedata"),
CacheTtlSeconds = ConfigHelper.GetInt(config, "cacheTtlSeconds", 60)
};
if (config.GetValueOrDefault("providers") is JsonElement providersJe)
{
var providers = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(providersJe.GetRawText());
if (providers != null)
{
if (providers.TryGetValue("twelvedata", out var td) && td.TryGetProperty("apiKey", out var tdk))
settings.TwelveDataKey = tdk.GetString() ?? "";
if (providers.TryGetValue("alphavantage", out var av) && av.TryGetProperty("apiKey", out var avk))
settings.AlphaVantageKey = avk.GetString() ?? "";
}
}
return settings;
}
}
public sealed class WebFetchToolSettings
{
[Category("WebFetch")]
[DisplayName("Erlaubte Domains")]
[Description("Komma-getrennte Liste (z.B. reuters.com,bloomberg.com)")]
public string AllowedDomains { get; set; } = "";
[Category("WebFetch")]
[DisplayName("Max Response KB")]
public int MaxResponseKb { get; set; } = 512;
[Category("WebFetch")]
[DisplayName("User Agent")]
public string UserAgent { get; set; } = "ClawdDotNet-Agent/1.0";
public Dictionary<string, object?> ToConfig() => new()
{
["allowedDomains"] = AllowedDomains.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
["maxResponseKb"] = MaxResponseKb,
["userAgent"] = UserAgent
};
public static WebFetchToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
AllowedDomains = ConfigHelper.GetStringArray(config, "allowedDomains"),
MaxResponseKb = ConfigHelper.GetInt(config, "maxResponseKb", 512),
UserAgent = ConfigHelper.GetString(config, "userAgent", "ClawdDotNet-Agent/1.0")
};
}
public sealed class WebMonitorToolSettings
{
[Category("WebMonitor")]
[DisplayName("Monitore (JSON)")]
[Description("JSON-Konfiguration der Monitore")]
public string MonitorsJson { get; set; } = "{}";
public Dictionary<string, object?> ToConfig()
{
try
{
return new Dictionary<string, object?>
{
["monitors"] = JsonSerializer.Deserialize<Dictionary<string, object?>>(MonitorsJson) ?? new()
};
}
catch { return new Dictionary<string, object?> { ["monitors"] = new Dictionary<string, object?>() }; }
}
public static WebMonitorToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
MonitorsJson = config.GetValueOrDefault("monitors") is JsonElement je ? je.GetRawText() : "{}"
};
}
public sealed class AgentCommToolSettings
{
[Category("AgentComm")]
[DisplayName("Info")]
[Description("Dieses Tool benötigt keine Konfiguration. Es ermöglicht Agenten, mit anderen Agenten in der gleichen Instanz zu kommunizieren.")]
[ReadOnly(true)]
public string Status { get; set; } = "Aktiv";
public Dictionary<string, object?> ToConfig() => new();
public static AgentCommToolSettings FromConfig(Dictionary<string, object?> config) => new();
}
public sealed class SocialMediaManagerToolSettings
{
// ─── X (Twitter) ───
[Category("1. X (Twitter) - API")]
[DisplayName("Bearer Token")]
[Description("X API v2 Bearer Token für die Authentifizierung")]
[PasswordPropertyText(true)]
public string XApiKey { get; set; } = "";
[Category("1. X (Twitter) - Monitoring")]
[DisplayName("Überwachte Accounts")]
[Description("Komma-getrennte Liste von X-Accounts die überwacht werden sollen (ohne @). Beispiel: elonmusk,unusual_whales,DeItaone")]
public string XWatchAccounts { get; set; } = "";
// ─── Reddit ───
[Category("2. Reddit - Monitoring")]
[DisplayName("Überwachte Subreddits")]
[Description("Komma-getrennte Liste von Subreddits die überwacht werden sollen (ohne r/). Beispiel: wallstreetbets,stocks,options")]
public string RedditWatchSubreddits { get; set; } = "";
[Category("2. Reddit - Monitoring")]
[DisplayName("Posts pro Subreddit")]
[Description("Maximale Anzahl Posts die pro Subreddit bei jedem Check abgerufen werden (Standard: 15)")]
public int RedditPostLimit { get; set; } = 15;
// ─── YouTube / STT ───
[Category("3. YouTube / STT")]
[DisplayName("OpenRouter API Key")]
[Description("API Key für Speech-to-Text Transkription über OpenRouter")]
[PasswordPropertyText(true)]
public string OpenRouterApiKey { get; set; } = "";
[Category("3. YouTube / STT")]
[DisplayName("STT Modell")]
[Description("OpenRouter Modell-ID für die Transkription")]
public string STTModel { get; set; } = "openai/whisper-1";
[Category("3. YouTube / STT")]
[DisplayName("YouTube Kanäle")]
[Description("Komma-getrennte Liste von YouTube Kanal-URLs für automatische Überwachung")]
public string YoutubeChannels { get; set; } = "";
public Dictionary<string, object?> ToConfig() => new()
{
["xApiKey"] = XApiKey,
["xWatchAccounts"] = SplitToArray(XWatchAccounts),
["redditWatchSubreddits"] = SplitToArray(RedditWatchSubreddits),
["redditPostLimit"] = RedditPostLimit,
["openRouterApiKey"] = OpenRouterApiKey,
["sttModel"] = STTModel,
["youtubeChannels"] = SplitToArray(YoutubeChannels)
};
public static SocialMediaManagerToolSettings FromConfig(Dictionary<string, object?> config) => new()
{
XApiKey = ConfigHelper.GetString(config, "xApiKey"),
XWatchAccounts = ConfigHelper.GetStringArray(config, "xWatchAccounts"),
RedditWatchSubreddits = ConfigHelper.GetStringArray(config, "redditWatchSubreddits"),
RedditPostLimit = ConfigHelper.GetInt(config, "redditPostLimit", 15),
OpenRouterApiKey = ConfigHelper.GetString(config, "openRouterApiKey"),
STTModel = ConfigHelper.GetString(config, "sttModel", "openai/whisper-1"),
YoutubeChannels = ConfigHelper.GetStringArray(config, "youtubeChannels")
};
private static string[] SplitToArray(string csv)
=> csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
public sealed class AgentEditorToolSettings
{
[Category("AgentEditor")]
[DisplayName("Info")]
[Description("Erlaubt dem Agenten, Identity und Soul anderer Agenten zu lesen, zu bearbeiten und neue Agenten zu erstellen. Keine weitere Konfiguration nötig.")]
[ReadOnly(true)]
public string Status { get; set; } = "Aktiv";
public Dictionary<string, object?> ToConfig() => new();
public static AgentEditorToolSettings FromConfig(Dictionary<string, object?> config) => new();
}
public sealed class AgentSpawnToolSettings
{
[Category("AgentSpawn")]
[DisplayName("Info")]
[Description("Dieses Tool benötigt keine Konfiguration. Es ermöglicht Agenten, andere Agenten zu starten und ihnen Aufgaben zuzuweisen.")]
[ReadOnly(true)]
public string Status { get; set; } = "Aktiv";
public Dictionary<string, object?> ToConfig() => new();
public static AgentSpawnToolSettings FromConfig(Dictionary<string, object?> config) => new();
}
public static class ToolSettingsFactory
{
public static object? CreateViewModel(string toolName, Dictionary<string, object?>? config)
{
config ??= new();
return toolName switch
{
"FileRW" => FileRWToolSettings.FromConfig(config),
"Mail" => MailToolSettings.FromConfig(config),
"Database" => DatabaseToolSettings.FromConfig(config),
"Telegram" => TelegramToolSettings.FromConfig(config),
"FTP" => FTPToolSettings.FromConfig(config),
"DirectAPI" => DirectAPIToolSettings.FromConfig(config),
"WebFetch" => WebFetchToolSettings.FromConfig(config),
"WebMonitor" => WebMonitorToolSettings.FromConfig(config),
"AgentComm" => AgentCommToolSettings.FromConfig(config),
"SocialMediaManager" => SocialMediaManagerToolSettings.FromConfig(config),
"AgentSpawn" => AgentSpawnToolSettings.FromConfig(config),
"AgentEditor" => AgentEditorToolSettings.FromConfig(config),
_ => null
};
}
public static Dictionary<string, object?>? ToConfig(string toolName, object? viewModel)
{
return viewModel switch
{
FileRWToolSettings f => f.ToConfig(),
MailToolSettings m => m.ToConfig(),
DatabaseToolSettings d => d.ToConfig(),
TelegramToolSettings t => t.ToConfig(),
FTPToolSettings ftp => ftp.ToConfig(),
DirectAPIToolSettings dapi => dapi.ToConfig(),
WebFetchToolSettings wf => wf.ToConfig(),
WebMonitorToolSettings wm => wm.ToConfig(),
AgentCommToolSettings ac => ac.ToConfig(),
SocialMediaManagerToolSettings smm => smm.ToConfig(),
AgentSpawnToolSettings asp => asp.ToConfig(),
AgentEditorToolSettings ae => ae.ToConfig(),
_ => null
};
}
}
-334
View File
@@ -1,334 +0,0 @@
using ClawdDotNet.Core.Api;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Logging;
using ClawdDotNet.Core.Security;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Core.State;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Memory;
using ClawdDotNet.Core.Accounting;
using ClawdDotNet.Services;
using ClawdDotNet.Tools.FileRW;
using ClawdDotNet.Tools.Telegram;
using ClawdDotNet.Tools.Mail;
using ClawdDotNet.Tools.Database;
using ClawdDotNet.Tools.FTP;
using ClawdDotNet.Tools.DirectAPI;
using ClawdDotNet.Tools.WebFetch;
using ClawdDotNet.Tools.WebMonitor;
using ClawdDotNet.Tools.AgentComm;
using ClawdDotNet.Tools.AgentSpawn;
using ClawdDotNet.Tools.SocialMediaManager;
using ClawdDotNet.Tools.AgentEditor;
using ClawdDotNet.Tools.TelegramClient;
using ClawdDotNet.UI;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet;
internal static class Program
{
[STAThread]
static void Main(string[] args)
{
ApplicationConfiguration.Initialize();
// ─── 0. Embedded UI extrahieren ───
EmbeddedUiManager.ExtractToTemp();
// ─── 1. App-Settings laden ───
var settingsManager = new SettingsManager();
settingsManager.Load();
var appSettings = settingsManager.AppSettings;
// ─── 2. InstanceDirectoryManager erstellen ───
var instancesDir = Path.GetFullPath(appSettings.InstancesDirectory);
var dirManager = new InstanceDirectoryManager(instancesDir);
// ─── 3. Instanz-Verzeichnis auswählen ───
string instancePath;
#if DEBUG
// Im Debug-Modus: Dev-Instanz automatisch erstellen/starten
const string devInstanceName = "Dev";
instancePath = dirManager.GetInstancePath(devInstanceName);
if (!Directory.Exists(instancePath))
{
dirManager.CreateInstance(devInstanceName);
}
#else
// Im Release-Modus: InstanceManager anzeigen
var instanceManager = new frm_InstanceManager(dirManager, settingsManager);
var dialogResult = instanceManager.ShowDialog();
if (dialogResult != DialogResult.OK || string.IsNullOrWhiteSpace(instanceManager.SelectedInstancePath))
{
return; // Benutzer hat abgebrochen
}
instancePath = instanceManager.SelectedInstancePath;
instanceManager.Dispose();
#endif
// ─── 4. Instanz-Config aus Verzeichnisstruktur laden ───
InstanceConfig instanceConfig;
try
{
instanceConfig = dirManager.LoadInstanceConfig(instancePath);
}
catch (Exception ex)
{
MessageBox.Show(
$"Fehler beim Laden der Instanz:\n{instancePath}\n\n{ex.Message}",
"ClawdDotNet Fehler",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// ─── 5. Logging-System initialisieren ───
var logDir = Path.GetFullPath(
!string.IsNullOrWhiteSpace(instanceConfig.LogDirectory)
? instanceConfig.LogDirectory
: appSettings.LogDirectory);
var minLevel = Enum.TryParse<ClawdDotNet.Core.Logging.LogLevel>(
appSettings.MinimumLogLevel, true, out var ml)
? ml
: ClawdDotNet.Core.Logging.LogLevel.Info;
var loggerFactory = LoggingExtensions.CreateClawdLoggerFactory(logDir, minLevel);
var coreLogger = loggerFactory.CreateLogger("ClawdDotNet.Core.Startup");
coreLogger.LogInformation("ClawdDotNet startet Instanz: {Instance} ({Id})",
instanceConfig.InstanceName, instanceConfig.InstanceId);
coreLogger.LogInformation("Instanz-Verzeichnis: {Path}", instancePath);
// ─── 5b. Lizenzprüfung ───
// Entfällt in dieser Fassung. Startprüfung, laufende Nachprüfung und die
// Deploymentcenter-Anbindung (Watchdog, Updates, Fehler-Stream, Bugtracker)
// liegen in ClawdDotNet.App (AppHost); Einstiegspunkt ist
// src/ClawdDotNet.Desktop. Diese Datei ist nur noch Vorlage — siehe
// ClawdDotNet.slnx.
// ─── 6. Core-Komponenten erzeugen ───
var toolRegistry = new ToolRegistry();
toolRegistry.Register(new FileRWTool());
toolRegistry.Register(new TelegramTool());
toolRegistry.Register(new MailTool());
toolRegistry.Register(new DatabaseTool());
toolRegistry.Register(new FTPTool());
toolRegistry.Register(new DirectApiTool());
toolRegistry.Register(new WebFetchTool());
toolRegistry.Register(new WebMonitorTool());
toolRegistry.Register(new AgentCommTool());
toolRegistry.Register(new SocialMediaManagerTool());
toolRegistry.Register(new AgentSpawnTool());
toolRegistry.Register(new AgentEditorTool());
toolRegistry.Register(new ClawdDotNet.Tools.Memory.MemoryTool());
toolRegistry.Register(new ClawdDotNet.Tools.Taskboard.TaskboardTool());
// ─── 6a. TelegramClient (MTProto User-API) ───
TelegramClientManager? tgClientManager = null;
if (instanceConfig.TelegramClient is not null)
{
tgClientManager = new TelegramClientManager(
instanceConfig,
instancePath,
loggerFactory.CreateLogger("ClawdDotNet.Tools.TelegramClient"));
toolRegistry.Register(new TelegramClientTool(tgClientManager));
coreLogger.LogInformation("TelegramClient-Tool registriert");
}
var permissionGate = new PermissionGate();
// TODO: Tools aus tools/ Ordner laden
// foreach (var toolDll in Directory.GetFiles("./tools", "*.dll"))
// LoadToolPlugin(toolDll, toolRegistry);
OpenRouterClient? openRouterClient = null;
AgentEngine? agentEngine = null;
ClawdDotNet.Core.Tasks.TaskScanner? taskScanner = null;
ClawdDotNet.Core.Staging.StagingService? stagingService = null;
SqliteUsageRepository? usageRepository = null;
if (!string.IsNullOrWhiteSpace(instanceConfig.OpenRouterApiKey))
{
openRouterClient = new OpenRouterClient(
instanceConfig.OpenRouterApiKey,
loggerFactory.CreateLogger("ClawdDotNet.Core.Api.OpenRouterClient"));
// ModelTypeConverter mit dem Client verbinden für PropertyGrid-Dropdown
ClawdDotNet.Models.ModelTypeConverter.Client = openRouterClient;
// ─── 6.1. Speicher initialisieren ───
// Eine Datenbank je Instanz; StateStore und Gedächtnis teilen sie sich.
var storage = new SqliteStorage(Path.Combine(instancePath, "state.db"));
var stateStore = new SqliteStateStore(storage);
var memoryRepository = new SqliteMemoryRepository(storage);
var taskRepository = new ClawdDotNet.Core.Tasks.SqliteTaskRepository(storage);
var auditRepository = new ClawdDotNet.Core.Audit.SqliteAuditRepository(storage);
var stagingRepository = new ClawdDotNet.Core.Staging.SqliteStagingRepository(storage);
var stagingGate = new ClawdDotNet.Core.Staging.StagingGate(
new ClawdDotNet.Core.Staging.StagingPolicy(), stagingRepository);
usageRepository = new SqliteUsageRepository(storage);
// Preise fürs Budget: Ohne sie greift nur die Token-Grenze.
var pricingCatalog = new ModelPricingCatalog();
_ = Task.Run(async () =>
{
try { pricingCatalog.Load(await openRouterClient.GetAvailableModelsAsync()); }
catch { /* Ohne Preise bleibt die Kostengrenze wirkungslos, die Token-Grenze nicht. */ }
});
agentEngine = new AgentEngine(
openRouterClient, toolRegistry, permissionGate, stateStore, loggerFactory,
memoryRepository, usageRepository, pricingCatalog, taskRepository, auditRepository, stagingGate)
{
InstanceBudget = instanceConfig.Budget
};
agentEngine.SetAgentConfigProvider(
() => instanceConfig.Agents,
instanceConfig.InstanceId,
agentId =>
{
var agent = instanceConfig.Agents.FirstOrDefault(a => a.AgentId == agentId);
if (agent is null) return null;
return string.IsNullOrWhiteSpace(agent.AgentDir) ? null : agent.AgentDir;
});
agentEngine.LoadPersistedChats();
// ─── Taskboard: Dateien in die DB spiegeln (Reconciliation beim Start) ───
// Verwaiste Claims eines abgestürzten Laufs lösen, dann alle Task-Dateien
// importieren. Nicht blockierend — der Start soll nicht auf das Dateisystem warten.
var sharedWorkspace = instanceConfig.Agents
.Select(a => a.SharedWorkspacePath)
.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p));
if (!string.IsNullOrWhiteSpace(sharedWorkspace))
{
var board = new ClawdDotNet.Core.Tasks.TaskboardService(
taskRepository, Path.Combine(sharedWorkspace, "tasks"));
// Staging (A2): Freigabe-/Ablehnungs-Dienst für die Review-Oberfläche.
// Weckt den Agenten nach der Entscheidung über einen Folge-Task (A1).
stagingService = new ClawdDotNet.Core.Staging.StagingService(
stagingRepository, agentEngine, board, loggerFactory, auditRepository);
// Scanner (A1): der Taktgeber. Startet erst, wenn die Reconciliation durch
// ist — sonst könnte er einen noch nicht bereinigten Claim antreffen.
// Der Scanner ist der EINZIGE periodische Treiber (A1) — er ersetzt die
// früheren AgentScheduler/ToolJobScheduler. Geplante Agent-Läufe wie auch
// Tool-Job-Polls sind jetzt Tasks.
var dispatcher = new ClawdDotNet.Core.Tasks.EngineTaskDispatcher(
agentEngine, () => instanceConfig.Agents, instanceConfig.InstanceId,
toolRegistry, stateStore, loggerFactory);
taskScanner = new ClawdDotNet.Core.Tasks.TaskScanner(
taskRepository, dispatcher, loggerFactory);
_ = Task.Run(async () =>
{
try
{
var reset = await taskRepository.ReleaseStaleClaimsAsync(
DateTime.UtcNow.AddMinutes(-15), DateTime.UtcNow, CancellationToken.None);
// Einmalige Übernahme der alten coordination/task_*-Dateien …
var coordMigration = new ClawdDotNet.Core.Tasks.CoordinationMigration(
board, Path.Combine(sharedWorkspace, "coordination"), loggerFactory);
var migratedCoord = await coordMigration.RunAsync(CancellationToken.None);
// … und der alten scheduler/toolJobs-Konfiguration in Tasks.
var schedMigration = new ClawdDotNet.Core.Tasks.SchedulerTaskMigration(
board, taskRepository, loggerFactory);
var migratedSched = await schedMigration.RunAsync(instanceConfig.Agents, CancellationToken.None);
var imported = await board.ImportAllAsync(CancellationToken.None);
coreLogger.LogInformation(
"Taskboard bereit: {Imported} Aufgabe(n), migriert {Coord} coordination + {Sched} scheduler, {Reset} verwaiste Claims zurückgesetzt",
imported, migratedCoord, migratedSched, reset);
taskScanner.Start();
}
catch (Exception ex)
{
coreLogger.LogWarning(ex, "Taskboard-Reconciliation beim Start fehlgeschlagen");
}
});
}
coreLogger.LogInformation("AgentEngine und Scanner erstellt, Chat-Verläufe geladen");
}
else
{
coreLogger.LogWarning("Kein OpenRouter API-Key konfiguriert Agenten sind deaktiviert");
}
// ─── 6b. TelegramClient verbinden (Session oder interaktiver Login) ───
if (tgClientManager is not null)
{
tgClientManager.OnLoginCodeRequired += async (prompt) =>
{
string? code = null;
await Task.Run(() =>
{
code = Microsoft.VisualBasic.Interaction.InputBox(
prompt, "Telegram Verifizierung", "");
});
return code ?? "";
};
tgClientManager.On2FAPasswordRequired += async () =>
{
string? pw = null;
await Task.Run(() =>
{
pw = Microsoft.VisualBasic.Interaction.InputBox(
"Bitte 2FA-Passwort eingeben:", "Telegram 2FA", "");
});
return pw ?? "";
};
try
{
tgClientManager.ConnectAsync(CancellationToken.None)
.GetAwaiter().GetResult();
}
catch (Exception ex)
{
coreLogger.LogError(ex, "Telegram: Login fehlgeschlagen");
}
}
// ─── 6c. Instanz-Watchdog ───
// Entfällt hier ebenfalls. Der Heartbeat ans Deploymentcenter (ein Monitor je
// Instanz, mit Gesundheitsprüfungen und angekündigtem Ende) steht in
// ClawdDotNet.App/Services/DeploymentcenterService.cs.
// ─── 7. MainForm starten ───
var form = new frm_main(
settingsManager,
instanceConfig,
instancePath,
logDir,
loggerFactory,
toolRegistry,
dirManager,
agentEngine,
taskScanner,
stagingService);
Application.Run(form);
// ─── 8. Aufräumen ───
coreLogger.LogInformation("ClawdDotNet wird beendet");
if (taskScanner is not null)
taskScanner.DisposeAsync().AsTask().GetAwaiter().GetResult();
if (tgClientManager is not null)
tgClientManager.DisposeAsync().AsTask().GetAwaiter().GetResult();
openRouterClient?.Dispose();
loggerFactory.Dispose();
}
}
-123
View File
@@ -1,123 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Dieser Code wurde von einem Tool generiert.
// Laufzeitversion:4.0.30319.42000
//
// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn
// der Code erneut generiert wird.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ClawdDotNet.Properties {
using System;
/// <summary>
/// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw.
/// </summary>
// Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert
// -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert.
// Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen
// mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ClawdDotNet.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle
/// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap add {
get {
object obj = ResourceManager.GetObject("add", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap cog {
get {
object obj = ResourceManager.GetObject("cog", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap messenger {
get {
object obj = ResourceManager.GetObject("messenger", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap plus {
get {
object obj = ResourceManager.GetObject("plus", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap server_add {
get {
object obj = ResourceManager.GetObject("server_add", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap server_go {
get {
object obj = ResourceManager.GetObject("server_go", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
}
}
-139
View File
@@ -1,139 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="add" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="cog" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\cog.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="messenger" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\messenger.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="plus" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\plus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="server_add" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\server_add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="server_go" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\server_go.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

-255
View File
@@ -1,255 +0,0 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
namespace ClawdDotNet.Services;
/// <summary>
/// Überwacht das Logs-Verzeichnis und liefert neue Log-Einträge gefiltert
/// an eine RichTextBox. Bereinigt die RichTextBox automatisch wenn sie
/// zu voll wird, damit das UI reaktionsfähig bleibt.
///
/// Struktur: Logs/{Datum}/{Modul}.log
/// Log-Format: [{Timestamp}] [{LEVEL}] {Message}
/// </summary>
public sealed class LiveLogViewerService : IDisposable
{
private readonly string _logDirectory;
private readonly RichTextBox _target;
private readonly System.Windows.Forms.Timer _refreshTimer;
private readonly int _maxLines;
// Tracking: pro Datei die letzte gelesene Position
private readonly ConcurrentDictionary<string, long> _filePositions = new();
// Filter
private string _moduleFilter = ""; // leer = alle
private string _levelFilter = ""; // leer = alle
private static readonly Regex LevelRegex = new(
@"\[(INF|WRN|ERR|DBG)\]",
RegexOptions.Compiled);
public LiveLogViewerService(string logDirectory, RichTextBox target, int refreshIntervalMs = 500, int maxLines = 2000)
{
_logDirectory = logDirectory;
_target = target;
_maxLines = maxLines;
_refreshTimer = new System.Windows.Forms.Timer { Interval = refreshIntervalMs };
_refreshTimer.Tick += OnTimerTick;
}
public void Start() => _refreshTimer.Start();
public void Stop() => _refreshTimer.Stop();
public void SetModuleFilter(string module)
{
_moduleFilter = module;
ClearAndResetPositions();
}
public void SetLevelFilter(string level)
{
_levelFilter = level;
ClearAndResetPositions();
}
/// <summary>
/// Gibt alle erkannten Modul-Namen zurück (basierend auf den vorhandenen .log-Dateien).
/// </summary>
public List<string> GetAvailableModules()
{
var modules = new HashSet<string> { "Alle" };
if (!Directory.Exists(_logDirectory))
return modules.ToList();
// Alle Unterordner (Datum-Ordner) durchsuchen
foreach (var dateDir in Directory.GetDirectories(_logDirectory))
{
foreach (var logFile in Directory.GetFiles(dateDir, "*.log"))
{
var name = Path.GetFileNameWithoutExtension(logFile);
modules.Add(name);
}
}
return modules.OrderBy(m => m == "Alle" ? "" : m).ToList();
}
private void OnTimerTick(object? sender, EventArgs e)
{
try
{
ReadNewEntries();
}
catch
{
// Logging-Viewer darf niemals das UI crashen
}
}
private void ReadNewEntries()
{
if (!Directory.Exists(_logDirectory))
return;
// Heutiges Datum-Verzeichnis (und ggf. gestriges für Logs um Mitternacht)
var today = DateTime.Now.ToString("yyyy-MM-dd");
var todayDir = Path.Combine(_logDirectory, today);
if (!Directory.Exists(todayDir))
return;
var logFiles = Directory.GetFiles(todayDir, "*.log");
var newLines = new List<(DateTime time, string line, string module)>();
foreach (var filePath in logFiles)
{
var moduleName = Path.GetFileNameWithoutExtension(filePath);
// Modul-Filter
if (!string.IsNullOrEmpty(_moduleFilter) && _moduleFilter != "Alle"
&& !string.Equals(moduleName, _moduleFilter, StringComparison.OrdinalIgnoreCase))
continue;
var lastPos = _filePositions.GetOrAdd(filePath, 0L);
try
{
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
if (fs.Length < lastPos)
{
// Datei wurde rotiert/gekürzt
lastPos = 0;
}
if (fs.Length == lastPos)
continue;
fs.Seek(lastPos, SeekOrigin.Begin);
using var reader = new StreamReader(fs);
while (reader.ReadLine() is { } line)
{
if (string.IsNullOrWhiteSpace(line))
continue;
// Level-Filter
if (!string.IsNullOrEmpty(_levelFilter) && _levelFilter != "Alle")
{
if (!PassesLevelFilter(line))
continue;
}
newLines.Add((DateTime.Now, $"[{moduleName}] {line}", moduleName));
}
_filePositions[filePath] = fs.Position;
}
catch (IOException)
{
// Datei wird gerade geschrieben - nächstes Mal versuchen
}
}
if (newLines.Count == 0)
return;
// In UI schreiben
AppendToRichTextBox(newLines);
}
private bool PassesLevelFilter(string line)
{
var match = LevelRegex.Match(line);
if (!match.Success)
return true; // Unbekanntes Format durchlassen
var level = match.Groups[1].Value;
return _levelFilter switch
{
"Info" => level is "INF" or "WRN" or "ERR",
"Warn" => level is "WRN" or "ERR",
"Error" => level is "ERR",
_ => true
};
}
private void AppendToRichTextBox(List<(DateTime time, string line, string module)> lines)
{
if (_target.IsDisposed || !_target.IsHandleCreated)
return;
_target.BeginInvoke(() =>
{
_target.SuspendLayout();
foreach (var (_, line, module) in lines)
{
var color = GetColorForLine(line);
_target.SelectionStart = _target.TextLength;
_target.SelectionLength = 0;
_target.SelectionColor = color;
_target.AppendText(line + Environment.NewLine);
}
// Bereinigung: wenn zu viele Zeilen, die ältesten entfernen
TrimIfNeeded();
// Auto-Scroll zum Ende
_target.SelectionStart = _target.TextLength;
_target.ScrollToCaret();
_target.ResumeLayout();
});
}
private static Color GetColorForLine(string line)
{
if (line.Contains("[ERR]"))
return Color.Red;
if (line.Contains("[WRN]"))
return Color.Orange;
if (line.Contains("[DBG]"))
return Color.Gray;
return Color.LightGreen; // INF
}
private void TrimIfNeeded()
{
if (_target.Lines.Length <= _maxLines)
return;
// Die älteste Hälfte entfernen
var removeCount = _maxLines / 2;
var removeEndIndex = _target.GetFirstCharIndexFromLine(removeCount);
if (removeEndIndex <= 0)
return;
_target.SelectionStart = 0;
_target.SelectionLength = removeEndIndex;
_target.SelectedText = $"--- {removeCount} ältere Zeilen entfernt ---{Environment.NewLine}";
}
private void ClearAndResetPositions()
{
_filePositions.Clear();
if (_target.IsHandleCreated && !_target.IsDisposed)
{
_target.BeginInvoke(() =>
{
_target.Clear();
});
}
}
public void Dispose()
{
_refreshTimer.Stop();
_refreshTimer.Dispose();
}
}
-428
View File
@@ -1,428 +0,0 @@
namespace ClawdDotNet.UI;
partial class BackupPanel
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && components is not null)
components.Dispose();
base.Dispose(disposing);
}
private void InitializeComponent()
{
layoutRoot = new TableLayoutPanel();
grp_create = new GroupBox();
layoutCreate = new TableLayoutPanel();
label_target = new Label();
txt_targetFolder = new TextBox();
btn_browseFolder = new Button();
label_secrets = new Label();
panel_secretMode = new FlowLayoutPanel();
rb_secretsExclude = new RadioButton();
rb_secretsPassphrase = new RadioButton();
label_passphrase = new Label();
txt_passphrase = new TextBox();
label_passphraseRepeat = new Label();
txt_passphraseRepeat = new TextBox();
label_scope = new Label();
panel_scope = new FlowLayoutPanel();
chk_includeChatHistory = new CheckBox();
chk_includeLogs = new CheckBox();
panel_createActions = new FlowLayoutPanel();
btn_createBackup = new Button();
label_createStatus = new Label();
grp_auto = new GroupBox();
layoutAuto = new TableLayoutPanel();
chk_autoEnabled = new CheckBox();
dtp_autoTime = new DateTimePicker();
label_keepCount = new Label();
num_keepCount = new NumericUpDown();
label_autoHint = new Label();
grp_existing = new GroupBox();
layoutExisting = new TableLayoutPanel();
lv_backups = new ListView();
col_file = new ColumnHeader();
col_created = new ColumnHeader();
col_instance = new ColumnHeader();
col_size = new ColumnHeader();
col_secrets = new ColumnHeader();
panel_existingActions = new FlowLayoutPanel();
btn_refresh = new Button();
btn_restore = new Button();
btn_showInFolder = new Button();
btn_delete = new Button();
layoutRoot.SuspendLayout();
grp_create.SuspendLayout();
layoutCreate.SuspendLayout();
panel_secretMode.SuspendLayout();
panel_scope.SuspendLayout();
panel_createActions.SuspendLayout();
grp_auto.SuspendLayout();
layoutAuto.SuspendLayout();
((System.ComponentModel.ISupportInitialize)num_keepCount).BeginInit();
grp_existing.SuspendLayout();
layoutExisting.SuspendLayout();
panel_existingActions.SuspendLayout();
SuspendLayout();
// ─── Wurzel ───
layoutRoot.ColumnCount = 1;
layoutRoot.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layoutRoot.Dock = DockStyle.Fill;
layoutRoot.Name = "layoutRoot";
layoutRoot.Padding = new Padding(12);
layoutRoot.RowCount = 3;
layoutRoot.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layoutRoot.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layoutRoot.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
layoutRoot.Controls.Add(grp_create, 0, 0);
layoutRoot.Controls.Add(grp_auto, 0, 1);
layoutRoot.Controls.Add(grp_existing, 0, 2);
// ═══ Sicherung erstellen ═══
grp_create.AutoSize = true;
grp_create.AutoSizeMode = AutoSizeMode.GrowAndShrink;
grp_create.Controls.Add(layoutCreate);
grp_create.Dock = DockStyle.Fill;
grp_create.Name = "grp_create";
grp_create.Padding = new Padding(10);
grp_create.TabIndex = 0;
grp_create.TabStop = false;
grp_create.Text = "Sicherung erstellen";
layoutCreate.AutoSize = true;
layoutCreate.AutoSizeMode = AutoSizeMode.GrowAndShrink;
layoutCreate.ColumnCount = 3;
layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
layoutCreate.Dock = DockStyle.Fill;
layoutCreate.Name = "layoutCreate";
layoutCreate.RowCount = 6;
for (var i = 0; i < 6; i++)
layoutCreate.RowStyles.Add(new RowStyle(SizeType.AutoSize));
label_target.Anchor = AnchorStyles.Left;
label_target.AutoSize = true;
label_target.Name = "label_target";
label_target.Text = "Zielordner:";
txt_targetFolder.Anchor = AnchorStyles.Left | AnchorStyles.Right;
txt_targetFolder.Name = "txt_targetFolder";
btn_browseFolder.AutoSize = true;
btn_browseFolder.Name = "btn_browseFolder";
btn_browseFolder.Text = "Durchsuchen…";
btn_browseFolder.UseVisualStyleBackColor = true;
label_secrets.Anchor = AnchorStyles.Left;
label_secrets.AutoSize = true;
label_secrets.Name = "label_secrets";
label_secrets.Text = "Zugangsdaten:";
panel_secretMode.AutoSize = true;
panel_secretMode.Controls.Add(rb_secretsExclude);
panel_secretMode.Controls.Add(rb_secretsPassphrase);
panel_secretMode.Margin = new Padding(0);
panel_secretMode.Name = "panel_secretMode";
panel_secretMode.WrapContents = false;
rb_secretsExclude.AutoSize = true;
rb_secretsExclude.Checked = true;
rb_secretsExclude.Name = "rb_secretsExclude";
rb_secretsExclude.TabStop = true;
rb_secretsExclude.Text = "nicht mitsichern";
rb_secretsExclude.UseVisualStyleBackColor = true;
rb_secretsPassphrase.AutoSize = true;
rb_secretsPassphrase.Name = "rb_secretsPassphrase";
rb_secretsPassphrase.Text = "mit Passphrase schützen";
rb_secretsPassphrase.UseVisualStyleBackColor = true;
label_passphrase.Anchor = AnchorStyles.Left;
label_passphrase.AutoSize = true;
label_passphrase.Name = "label_passphrase";
label_passphrase.Text = "Passphrase:";
txt_passphrase.Anchor = AnchorStyles.Left | AnchorStyles.Right;
txt_passphrase.Name = "txt_passphrase";
txt_passphrase.UseSystemPasswordChar = true;
label_passphraseRepeat.Anchor = AnchorStyles.Left;
label_passphraseRepeat.AutoSize = true;
label_passphraseRepeat.Name = "label_passphraseRepeat";
label_passphraseRepeat.Text = "Wiederholen:";
txt_passphraseRepeat.Anchor = AnchorStyles.Left | AnchorStyles.Right;
txt_passphraseRepeat.Name = "txt_passphraseRepeat";
txt_passphraseRepeat.UseSystemPasswordChar = true;
label_scope.Anchor = AnchorStyles.Left;
label_scope.AutoSize = true;
label_scope.Name = "label_scope";
label_scope.Text = "Umfang:";
panel_scope.AutoSize = true;
panel_scope.Controls.Add(chk_includeChatHistory);
panel_scope.Controls.Add(chk_includeLogs);
panel_scope.Margin = new Padding(0);
panel_scope.Name = "panel_scope";
panel_scope.WrapContents = false;
chk_includeChatHistory.AutoSize = true;
chk_includeChatHistory.Checked = true;
chk_includeChatHistory.CheckState = CheckState.Checked;
chk_includeChatHistory.Name = "chk_includeChatHistory";
chk_includeChatHistory.Text = "Chatverläufe";
chk_includeChatHistory.UseVisualStyleBackColor = true;
chk_includeLogs.AutoSize = true;
chk_includeLogs.Name = "chk_includeLogs";
chk_includeLogs.Text = "Protokolle";
chk_includeLogs.UseVisualStyleBackColor = true;
panel_createActions.AutoSize = true;
panel_createActions.Controls.Add(btn_createBackup);
panel_createActions.Controls.Add(label_createStatus);
panel_createActions.Margin = new Padding(0, 8, 0, 0);
panel_createActions.Name = "panel_createActions";
panel_createActions.WrapContents = false;
btn_createBackup.AutoSize = true;
btn_createBackup.Name = "btn_createBackup";
btn_createBackup.Padding = new Padding(8, 2, 8, 2);
btn_createBackup.Text = "Sicherung jetzt erstellen";
btn_createBackup.UseVisualStyleBackColor = true;
label_createStatus.Anchor = AnchorStyles.Left;
label_createStatus.AutoSize = true;
label_createStatus.Margin = new Padding(12, 8, 3, 0);
label_createStatus.Name = "label_createStatus";
layoutCreate.Controls.Add(label_target, 0, 0);
layoutCreate.Controls.Add(txt_targetFolder, 1, 0);
layoutCreate.Controls.Add(btn_browseFolder, 2, 0);
layoutCreate.Controls.Add(label_secrets, 0, 1);
layoutCreate.Controls.Add(panel_secretMode, 1, 1);
layoutCreate.Controls.Add(label_passphrase, 0, 2);
layoutCreate.Controls.Add(txt_passphrase, 1, 2);
layoutCreate.Controls.Add(label_passphraseRepeat, 0, 3);
layoutCreate.Controls.Add(txt_passphraseRepeat, 1, 3);
layoutCreate.Controls.Add(label_scope, 0, 4);
layoutCreate.Controls.Add(panel_scope, 1, 4);
layoutCreate.Controls.Add(panel_createActions, 1, 5);
// ═══ Automatik ═══
grp_auto.AutoSize = true;
grp_auto.AutoSizeMode = AutoSizeMode.GrowAndShrink;
grp_auto.Controls.Add(layoutAuto);
grp_auto.Dock = DockStyle.Fill;
grp_auto.Margin = new Padding(3, 10, 3, 3);
grp_auto.Name = "grp_auto";
grp_auto.Padding = new Padding(10);
grp_auto.TabIndex = 1;
grp_auto.TabStop = false;
grp_auto.Text = "Automatische Sicherung";
layoutAuto.AutoSize = true;
layoutAuto.AutoSizeMode = AutoSizeMode.GrowAndShrink;
layoutAuto.ColumnCount = 5;
for (var i = 0; i < 4; i++)
layoutAuto.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
layoutAuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layoutAuto.Dock = DockStyle.Fill;
layoutAuto.Name = "layoutAuto";
layoutAuto.RowCount = 2;
layoutAuto.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layoutAuto.RowStyles.Add(new RowStyle(SizeType.AutoSize));
chk_autoEnabled.Anchor = AnchorStyles.Left;
chk_autoEnabled.AutoSize = true;
chk_autoEnabled.Name = "chk_autoEnabled";
chk_autoEnabled.Text = "täglich sichern um";
chk_autoEnabled.UseVisualStyleBackColor = true;
dtp_autoTime.Anchor = AnchorStyles.Left;
dtp_autoTime.Format = DateTimePickerFormat.Time;
dtp_autoTime.Name = "dtp_autoTime";
dtp_autoTime.ShowUpDown = true;
dtp_autoTime.Width = 90;
label_keepCount.Anchor = AnchorStyles.Left;
label_keepCount.AutoSize = true;
label_keepCount.Margin = new Padding(24, 6, 3, 0);
label_keepCount.Name = "label_keepCount";
label_keepCount.Text = "Aufbewahren:";
num_keepCount.Anchor = AnchorStyles.Left;
num_keepCount.Maximum = 999;
num_keepCount.Name = "num_keepCount";
num_keepCount.Width = 70;
label_autoHint.Anchor = AnchorStyles.Left;
label_autoHint.AutoSize = true;
label_autoHint.ForeColor = SystemColors.GrayText;
label_autoHint.Name = "label_autoHint";
label_autoHint.Text = "Die Automatik sichert ohne Zugangsdaten — eine gespeicherte Passphrase "
+ "neben den Sicherungen wäre wirkungslos.";
layoutAuto.Controls.Add(chk_autoEnabled, 0, 0);
layoutAuto.Controls.Add(dtp_autoTime, 1, 0);
layoutAuto.Controls.Add(label_keepCount, 2, 0);
layoutAuto.Controls.Add(num_keepCount, 3, 0);
layoutAuto.Controls.Add(label_autoHint, 0, 1);
layoutAuto.SetColumnSpan(label_autoHint, 5);
// ═══ Vorhandene Sicherungen ═══
grp_existing.Controls.Add(layoutExisting);
grp_existing.Dock = DockStyle.Fill;
grp_existing.Margin = new Padding(3, 10, 3, 3);
grp_existing.Name = "grp_existing";
grp_existing.Padding = new Padding(10);
grp_existing.TabIndex = 2;
grp_existing.TabStop = false;
grp_existing.Text = "Vorhandene Sicherungen";
layoutExisting.ColumnCount = 1;
layoutExisting.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layoutExisting.Dock = DockStyle.Fill;
layoutExisting.Name = "layoutExisting";
layoutExisting.RowCount = 2;
layoutExisting.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
layoutExisting.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layoutExisting.Controls.Add(lv_backups, 0, 0);
layoutExisting.Controls.Add(panel_existingActions, 0, 1);
lv_backups.Columns.AddRange(new[] { col_file, col_created, col_instance, col_size, col_secrets });
lv_backups.Dock = DockStyle.Fill;
lv_backups.FullRowSelect = true;
lv_backups.MultiSelect = false;
lv_backups.Name = "lv_backups";
lv_backups.UseCompatibleStateImageBehavior = false;
lv_backups.View = View.Details;
col_file.Text = "Datei";
col_file.Width = 420;
col_created.Text = "Erstellt";
col_created.Width = 150;
col_instance.Text = "Instanz";
col_instance.Width = 180;
col_size.Text = "Größe";
col_size.Width = 100;
col_secrets.Text = "Zugangsdaten";
col_secrets.Width = 140;
panel_existingActions.AutoSize = true;
panel_existingActions.Controls.Add(btn_refresh);
panel_existingActions.Controls.Add(btn_restore);
panel_existingActions.Controls.Add(btn_showInFolder);
panel_existingActions.Controls.Add(btn_delete);
panel_existingActions.Margin = new Padding(0, 8, 0, 0);
panel_existingActions.Name = "panel_existingActions";
panel_existingActions.WrapContents = false;
btn_refresh.AutoSize = true;
btn_refresh.Name = "btn_refresh";
btn_refresh.Text = "Aktualisieren";
btn_refresh.UseVisualStyleBackColor = true;
btn_restore.AutoSize = true;
btn_restore.Name = "btn_restore";
btn_restore.Padding = new Padding(8, 2, 8, 2);
btn_restore.Text = "Wiederherstellen…";
btn_restore.UseVisualStyleBackColor = true;
btn_showInFolder.AutoSize = true;
btn_showInFolder.Name = "btn_showInFolder";
btn_showInFolder.Text = "Im Ordner zeigen";
btn_showInFolder.UseVisualStyleBackColor = true;
btn_delete.AutoSize = true;
btn_delete.Name = "btn_delete";
btn_delete.Text = "Löschen";
btn_delete.UseVisualStyleBackColor = true;
// ─── Steuerelement ───
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(layoutRoot);
Name = "BackupPanel";
Size = new Size(1200, 800);
layoutRoot.ResumeLayout(false);
layoutRoot.PerformLayout();
grp_create.ResumeLayout(false);
grp_create.PerformLayout();
layoutCreate.ResumeLayout(false);
layoutCreate.PerformLayout();
panel_secretMode.ResumeLayout(false);
panel_secretMode.PerformLayout();
panel_scope.ResumeLayout(false);
panel_scope.PerformLayout();
panel_createActions.ResumeLayout(false);
panel_createActions.PerformLayout();
grp_auto.ResumeLayout(false);
grp_auto.PerformLayout();
layoutAuto.ResumeLayout(false);
layoutAuto.PerformLayout();
((System.ComponentModel.ISupportInitialize)num_keepCount).EndInit();
grp_existing.ResumeLayout(false);
layoutExisting.ResumeLayout(false);
layoutExisting.PerformLayout();
panel_existingActions.ResumeLayout(false);
panel_existingActions.PerformLayout();
ResumeLayout(false);
}
private TableLayoutPanel layoutRoot;
private GroupBox grp_create;
private TableLayoutPanel layoutCreate;
private Label label_target;
private TextBox txt_targetFolder;
private Button btn_browseFolder;
private Label label_secrets;
private FlowLayoutPanel panel_secretMode;
private RadioButton rb_secretsExclude;
private RadioButton rb_secretsPassphrase;
private Label label_passphrase;
private TextBox txt_passphrase;
private Label label_passphraseRepeat;
private TextBox txt_passphraseRepeat;
private Label label_scope;
private FlowLayoutPanel panel_scope;
private CheckBox chk_includeChatHistory;
private CheckBox chk_includeLogs;
private FlowLayoutPanel panel_createActions;
private Button btn_createBackup;
private Label label_createStatus;
private GroupBox grp_auto;
private TableLayoutPanel layoutAuto;
private CheckBox chk_autoEnabled;
private DateTimePicker dtp_autoTime;
private Label label_keepCount;
private NumericUpDown num_keepCount;
private Label label_autoHint;
private GroupBox grp_existing;
private TableLayoutPanel layoutExisting;
private ListView lv_backups;
private ColumnHeader col_file;
private ColumnHeader col_created;
private ColumnHeader col_instance;
private ColumnHeader col_size;
private ColumnHeader col_secrets;
private FlowLayoutPanel panel_existingActions;
private Button btn_refresh;
private Button btn_restore;
private Button btn_showInFolder;
private Button btn_delete;
}
-511
View File
@@ -1,511 +0,0 @@
using ClawdDotNet.Core.Backup;
using ClawdDotNet.Models;
using ClawdDotNet.Services;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.UI;
/// <summary>
/// Bedienoberfläche für Sicherung und Wiederherstellung einer Instanz.
///
/// Zwei Entscheidungen prägen das Verhalten:
///
/// Die Passphrase wird nirgends gespeichert. Läge sie neben den Sicherungen, wäre die
/// Verschlüsselung wirkungslos. Deshalb muss sie bei jeder geschützten Sicherung neu
/// eingegeben werden, und die Automatik sichert ohne Zugangsdaten.
///
/// Wiederhergestellt wird standardmäßig in einen NEUEN Ordner, nicht über die laufende
/// Instanz. Diese hält Chatverläufe im Speicher und die Datenbank geöffnet — ein
/// Überschreiben im Betrieb würde teils sofort wieder überschrieben und teils
/// scheitern.
/// </summary>
public sealed partial class BackupPanel : UserControl
{
private readonly BackupService _service = new();
private SettingsManager? _settingsManager;
private ILogger? _logger;
private string _instanceDir = "";
private string _instanceName = "";
private bool _loading;
public BackupPanel()
{
InitializeComponent();
WireEvents();
}
/// <summary>
/// Aktualisiert die Liste von außen — etwa nachdem die Automatik gesichert hat.
/// </summary>
public void RefreshFromOutside()
{
if (!IsHandleCreated || IsDisposed)
return;
RefreshBackupList();
}
/// <summary>Wird vom Hauptfenster nach dem Laden der Instanz aufgerufen.</summary>
public void Initialize(string instanceDir, string instanceName,
SettingsManager settingsManager, ILogger logger)
{
_instanceDir = instanceDir;
_instanceName = instanceName;
_settingsManager = settingsManager;
_logger = logger;
LoadSettings();
UpdateEnabledState();
RefreshBackupList();
}
private void WireEvents()
{
btn_browseFolder.Click += OnBrowseFolder;
btn_createBackup.Click += async (_, _) => await CreateBackupAsync();
btn_refresh.Click += (_, _) => RefreshBackupList();
btn_restore.Click += async (_, _) => await RestoreSelectedAsync();
btn_showInFolder.Click += (_, _) => ShowSelectedInFolder();
btn_delete.Click += (_, _) => DeleteSelected();
rb_secretsExclude.CheckedChanged += (_, _) => UpdateEnabledState();
rb_secretsPassphrase.CheckedChanged += (_, _) => UpdateEnabledState();
chk_autoEnabled.CheckedChanged += (_, _) => { UpdateEnabledState(); SaveSettings(); };
dtp_autoTime.ValueChanged += (_, _) => SaveSettings();
num_keepCount.ValueChanged += (_, _) => SaveSettings();
txt_targetFolder.Leave += (_, _) => SaveSettings();
lv_backups.SelectedIndexChanged += (_, _) => UpdateEnabledState();
lv_backups.DoubleClick += async (_, _) => await RestoreSelectedAsync();
}
// ═══════════════════════════════════════════════════
// Einstellungen
// ═══════════════════════════════════════════════════
private void LoadSettings()
{
if (_settingsManager is null) return;
_loading = true;
try
{
var settings = _settingsManager.AppSettings;
txt_targetFolder.Text = Path.GetFullPath(settings.BackupDirectory);
chk_autoEnabled.Checked = settings.AutoBackupEnabled;
num_keepCount.Value = Math.Clamp(settings.BackupKeepCount, 0, 999);
dtp_autoTime.Value = TimeSpan.TryParse(settings.AutoBackupTime, out var time)
? DateTime.Today.Add(time)
: DateTime.Today.AddHours(3);
}
finally
{
_loading = false;
}
}
private void SaveSettings()
{
if (_loading || _settingsManager is null) return;
var settings = _settingsManager.AppSettings;
settings.BackupDirectory = txt_targetFolder.Text.Trim();
settings.AutoBackupEnabled = chk_autoEnabled.Checked;
settings.AutoBackupTime = dtp_autoTime.Value.ToString("HH:mm");
settings.BackupKeepCount = (int)num_keepCount.Value;
_settingsManager.Save();
}
private void UpdateEnabledState()
{
var withPassphrase = rb_secretsPassphrase.Checked;
label_passphrase.Enabled = withPassphrase;
txt_passphrase.Enabled = withPassphrase;
label_passphraseRepeat.Enabled = withPassphrase;
txt_passphraseRepeat.Enabled = withPassphrase;
dtp_autoTime.Enabled = chk_autoEnabled.Checked;
num_keepCount.Enabled = chk_autoEnabled.Checked;
var hasSelection = lv_backups.SelectedItems.Count > 0;
btn_restore.Enabled = hasSelection;
btn_showInFolder.Enabled = hasSelection;
btn_delete.Enabled = hasSelection;
}
// ═══════════════════════════════════════════════════
// Sichern
// ═══════════════════════════════════════════════════
private void OnBrowseFolder(object? sender, EventArgs e)
{
using var dialog = new FolderBrowserDialog
{
Description = "Ordner für Sicherungen wählen",
UseDescriptionForTitle = true,
SelectedPath = Directory.Exists(txt_targetFolder.Text)
? txt_targetFolder.Text
: Environment.CurrentDirectory
};
if (dialog.ShowDialog(this) == DialogResult.OK)
{
txt_targetFolder.Text = dialog.SelectedPath;
SaveSettings();
RefreshBackupList();
}
}
private async Task CreateBackupAsync()
{
if (string.IsNullOrWhiteSpace(_instanceDir))
return;
var options = BuildOptions();
if (options is null)
return; // Eingabefehler, bereits gemeldet
var folder = txt_targetFolder.Text.Trim();
var file = Path.Combine(folder, BuildFileName());
btn_createBackup.Enabled = false;
SetStatus("Sicherung läuft…", SystemColors.GrayText);
try
{
var result = await Task.Run(() => _service.CreateAsync(_instanceDir, file, options));
SetStatus($"Fertig: {Path.GetFileName(result.ZipPath)} ({FormatSize(result.SizeBytes)})",
Color.FromArgb(0, 128, 0));
_logger?.LogInformation("Sicherung erstellt: {Path} ({Size} Bytes)",
result.ZipPath, result.SizeBytes);
txt_passphrase.Clear();
txt_passphraseRepeat.Clear();
ApplyRotation(folder);
RefreshBackupList();
}
catch (Exception ex)
{
SetStatus("Fehlgeschlagen", Color.Firebrick);
_logger?.LogError(ex, "Sicherung fehlgeschlagen");
MessageBox.Show(this, $"Die Sicherung ist fehlgeschlagen:\n\n{ex.Message}",
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btn_createBackup.Enabled = true;
}
}
/// <summary>Liest die Eingaben. Gibt null zurück, wenn etwas fehlt.</summary>
private BackupOptions? BuildOptions()
{
if (string.IsNullOrWhiteSpace(txt_targetFolder.Text))
{
MessageBox.Show(this, "Bitte einen Zielordner angeben.",
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
return null;
}
if (!rb_secretsPassphrase.Checked)
{
return new BackupOptions
{
Secrets = SecretMode.Exclude,
IncludeLogs = chk_includeLogs.Checked,
IncludeChatHistory = chk_includeChatHistory.Checked
};
}
var passphrase = txt_passphrase.Text;
if (string.IsNullOrEmpty(passphrase))
{
MessageBox.Show(this, "Bitte eine Passphrase eingeben.",
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
txt_passphrase.Focus();
return null;
}
if (passphrase != txt_passphraseRepeat.Text)
{
MessageBox.Show(this, "Die beiden Passphrasen stimmen nicht überein.",
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
txt_passphraseRepeat.Focus();
txt_passphraseRepeat.SelectAll();
return null;
}
// Eine vergessene Passphrase bedeutet unwiederbringlich verlorene Zugangsdaten.
var answer = MessageBox.Show(this,
"Ohne diese Passphrase lassen sich die Zugangsdaten in der Sicherung " +
"nicht wiederherstellen. Es gibt keinen Ersatzweg.\n\n" +
"Passphrase sicher notiert?",
"Passphrase bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (answer != DialogResult.Yes)
return null;
return new BackupOptions
{
Secrets = SecretMode.Passphrase,
Passphrase = passphrase,
IncludeLogs = chk_includeLogs.Checked,
IncludeChatHistory = chk_includeChatHistory.Checked
};
}
private string BuildFileName()
{
var safeName = string.Concat(
(_instanceName.Length > 0 ? _instanceName : "Instanz")
.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
return $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip";
}
// ═══════════════════════════════════════════════════
// Liste
// ═══════════════════════════════════════════════════
private void RefreshBackupList()
{
lv_backups.BeginUpdate();
lv_backups.Items.Clear();
try
{
var folder = txt_targetFolder.Text.Trim();
if (!Directory.Exists(folder))
return;
var files = new DirectoryInfo(folder)
.GetFiles("*.zip")
.OrderByDescending(f => f.LastWriteTime);
foreach (var file in files)
{
var item = new ListViewItem(file.Name) { Tag = file.FullName };
item.SubItems.Add(file.LastWriteTime.ToString("g"));
// Das Manifest verrät Instanz und ob Zugangsdaten enthalten sind.
try
{
var manifest = _service.InspectAsync(file.FullName).GetAwaiter().GetResult();
item.SubItems.Add(manifest.InstanceName);
item.SubItems.Add(FormatSize(file.Length));
item.SubItems.Add(manifest.HasSecrets
? $"ja ({manifest.SecretCount}, Passphrase)"
: "nein");
}
catch (Exception)
{
// Fremde oder beschädigte Archive trotzdem anzeigen — der Benutzer
// soll sehen, was im Ordner liegt.
item.SubItems.Add("—");
item.SubItems.Add(FormatSize(file.Length));
item.SubItems.Add("unlesbar");
item.ForeColor = SystemColors.GrayText;
}
lv_backups.Items.Add(item);
}
}
finally
{
lv_backups.EndUpdate();
UpdateEnabledState();
}
}
private string? SelectedBackupPath
=> lv_backups.SelectedItems.Count > 0
? lv_backups.SelectedItems[0].Tag as string
: null;
private void ShowSelectedInFolder()
{
if (SelectedBackupPath is not { } path) return;
ClawdDotNet.Core.Storage.SystemShell.RevealFile(path);
}
private void DeleteSelected()
{
if (SelectedBackupPath is not { } path) return;
var answer = MessageBox.Show(this,
$"Sicherung endgültig löschen?\n\n{Path.GetFileName(path)}",
"Löschen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (answer != DialogResult.Yes)
return;
try
{
File.Delete(path);
RefreshBackupList();
}
catch (Exception ex)
{
MessageBox.Show(this, $"Konnte nicht gelöscht werden:\n\n{ex.Message}",
"Löschen", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ═══════════════════════════════════════════════════
// Wiederherstellen
// ═══════════════════════════════════════════════════
private async Task RestoreSelectedAsync()
{
if (SelectedBackupPath is not { } zipPath) return;
BackupManifest manifest;
try
{
manifest = await _service.InspectAsync(zipPath);
}
catch (Exception ex)
{
MessageBox.Show(this, $"Die Sicherung ist nicht lesbar:\n\n{ex.Message}",
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
using var dialog = new frm_RestoreBackup(manifest, SuggestRestoreTarget(manifest));
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
var options = new RestoreOptions
{
Passphrase = dialog.Passphrase,
Overwrite = dialog.Overwrite
};
try
{
// Erst ohne zu schreiben prüfen — Prüfsummen und Konflikte.
var preview = await Task.Run(() =>
_service.RestoreAsync(zipPath, dialog.TargetDirectory,
options with { DryRun = true, Overwrite = true }));
if (preview.WouldOverwrite.Count > 0 && !dialog.Overwrite)
{
var answer = MessageBox.Show(this,
$"Im Zielordner liegen bereits {preview.WouldOverwrite.Count} der enthaltenen " +
"Dateien.\n\nVorhandene Dateien überschreiben?",
"Wiederherstellen", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
if (answer == DialogResult.Cancel)
return;
options = options with { Overwrite = answer == DialogResult.Yes };
}
var result = await Task.Run(() =>
_service.RestoreAsync(zipPath, dialog.TargetDirectory, options));
_logger?.LogInformation("Wiederherstellung nach {Target}: {Written} Dateien, {Skipped} übersprungen",
dialog.TargetDirectory, result.Written.Count, result.Skipped.Count);
var summary = $"{result.Written.Count} Datei(en) wiederhergestellt nach:\n{dialog.TargetDirectory}";
if (result.Skipped.Count > 0)
summary += $"\n\n{result.Skipped.Count} vorhandene Datei(en) blieben unangetastet.";
if (!manifest.HasSecrets && manifest.SecretCount > 0)
summary += "\n\nHinweis: Diese Sicherung enthält keine Zugangsdaten. " +
"API-Schlüssel und Passwörter müssen neu eingetragen werden.";
summary += "\n\nDie wiederhergestellte Instanz wird erst nach einem Neustart " +
"der Anwendung sichtbar.";
MessageBox.Show(this, summary, "Wiederherstellen",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Wiederherstellung fehlgeschlagen");
MessageBox.Show(this, $"Die Wiederherstellung ist fehlgeschlagen:\n\n{ex.Message}",
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Schlägt einen neuen Ordner neben der Instanz vor, nicht die laufende Instanz
/// selbst — diese hält Dateien geöffnet und würde Teile sofort überschreiben.
/// </summary>
private string SuggestRestoreTarget(BackupManifest manifest)
{
var parent = Directory.GetParent(_instanceDir)?.FullName ?? _instanceDir;
var name = string.IsNullOrWhiteSpace(manifest.InstanceName)
? "Instanz"
: manifest.InstanceName;
var safeName = string.Concat(
name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
return Path.Combine(parent, $"Instance-{safeName}_wiederhergestellt_{DateTime.Now:yyyyMMdd_HHmm}");
}
// ═══════════════════════════════════════════════════
// Rotation
// ═══════════════════════════════════════════════════
/// <summary>Entfernt die ältesten Sicherungen dieser Instanz über der Aufbewahrungszahl.</summary>
private void ApplyRotation(string folder)
{
var keep = (int)num_keepCount.Value;
if (keep <= 0 || !Directory.Exists(folder))
return;
try
{
var prefix = $"backup_{_instanceName}_";
var obsolete = new DirectoryInfo(folder)
.GetFiles("*.zip")
.Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(f => f.LastWriteTime)
.Skip(keep)
.ToList();
foreach (var file in obsolete)
{
file.Delete();
_logger?.LogInformation("Alte Sicherung entfernt: {Name}", file.Name);
}
}
catch (Exception ex)
{
// Aufräumen darf eine erfolgreiche Sicherung nicht nachträglich entwerten.
_logger?.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
}
}
// ═══════════════════════════════════════════════════
// Kleinkram
// ═══════════════════════════════════════════════════
private void SetStatus(string text, Color color)
{
label_createStatus.Text = text;
label_createStatus.ForeColor = color;
}
private static string FormatSize(long bytes) => bytes switch
{
< 1024 => $"{bytes} B",
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
_ => $"{bytes / 1024.0 / 1024 / 1024:F2} GB"
};
}
-33
View File
@@ -1,33 +0,0 @@
using System.Text.Json.Serialization;
namespace ClawdDotNet.UI;
public sealed record BridgeMessage(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("agentId")] string? AgentId = null,
[property: JsonPropertyName("content")] string? Content = null,
[property: JsonPropertyName("status")] string? Status = null,
[property: JsonPropertyName("stepCount")] int? StepCount = null,
[property: JsonPropertyName("tokenCount")] int? TokenCount = null,
[property: JsonPropertyName("error")] string? Error = null,
[property: JsonPropertyName("extra")] object? Extra = null
);
public static class BridgeTypes
{
// C# → Browser
public const string AgentListUpdate = "agent_list_update";
public const string AgentStatusUpdate = "agent_status";
public const string SelectAgent = "select_agent";
public const string ChatMessage = "chat_message";
public const string ChatTyping = "chat_typing";
public const string ChatHistory = "chat_history";
public const string RunStarted = "run_started";
public const string RunFinished = "run_finished";
// Browser → C#
public const string UserMessage = "user_message";
public const string OpenAgentChat = "open_agent_chat";
public const string RunNow = "run_now";
public const string AbortRun = "abort_run";
}
-40
View File
@@ -1,40 +0,0 @@
using System.Reflection;
namespace ClawdDotNet.UI;
public static class EmbeddedUiManager
{
private static string? _extractedPath;
public static string ExtractToTemp()
{
if (_extractedPath is not null) return _extractedPath;
var tempDir = Path.Combine(Path.GetTempPath(), "ClawdDotNet_UI",
Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "dev");
Directory.CreateDirectory(tempDir);
var asm = Assembly.GetExecutingAssembly();
var prefix = "ClawdDotNet.EmbeddedUI.";
foreach (var name in asm.GetManifestResourceNames()
.Where(n => n.StartsWith(prefix)))
{
// "ClawdDotNet.EmbeddedUI.chat.html" → "chat.html"
var fileName = name[prefix.Length..];
var dest = Path.Combine(tempDir, fileName);
using var stream = asm.GetManifestResourceStream(name)!;
using var file = File.Create(dest);
stream.CopyTo(file);
}
_extractedPath = tempDir;
return tempDir;
}
public static string GetExtractedPath()
=> _extractedPath ?? throw new InvalidOperationException(
"EmbeddedUiManager.ExtractToTemp() must be called first.");
}
-75
View File
@@ -1,75 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Web.WebView2.Core;
namespace ClawdDotNet.UI;
public sealed class WebViewBridge : IDisposable
{
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly Microsoft.Web.WebView2.WinForms.WebView2 _wv;
private readonly ILogger _logger;
public event Action<BridgeMessage>? MessageReceived;
public WebViewBridge(
Microsoft.Web.WebView2.WinForms.WebView2 webView,
ILogger logger)
{
_wv = webView;
_logger = logger;
_wv.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
}
public async Task SendAsync(BridgeMessage message)
{
var json = JsonSerializer.Serialize(message, JsonOpts);
var script = $"window.__bridge?.receive({json})";
if (_wv.InvokeRequired)
{
await Task.Factory.FromAsync(
_wv.BeginInvoke(new Func<Task>(async () =>
await _wv.CoreWebView2.ExecuteScriptAsync(script))),
_ => { });
}
else
{
await _wv.CoreWebView2.ExecuteScriptAsync(script);
}
}
private void OnWebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e)
{
try
{
var raw = e.TryGetWebMessageAsString();
_logger.LogDebug("Bridge raw incoming: {Raw}", raw);
if (string.IsNullOrWhiteSpace(raw)) return;
var msg = JsonSerializer.Deserialize<BridgeMessage>(raw, JsonOpts);
_logger.LogInformation("Bridge received: type={Type}, agentId={AgentId}, content={Content}",
msg?.Type, msg?.AgentId, msg?.Content?.Length > 50 ? msg.Content[..50] + "..." : msg?.Content);
if (msg is not null)
MessageReceived?.Invoke(msg);
}
catch (Exception ex)
{
_logger.LogError(ex, "Bridge: failed to deserialize incoming message");
}
}
public void Dispose()
{
if (_wv.CoreWebView2 is not null)
_wv.CoreWebView2.WebMessageReceived -= OnWebMessageReceived;
}
}
-46
View File
@@ -1,46 +0,0 @@
{
"instanceId": "crypto-01",
"instanceName": "Krypto-Team",
"openRouterApiKey": "sk-or-DEIN-API-KEY-HIER",
"workingDirectory": "./data/crypto/",
"logDirectory": "./Logs",
"webServerPort": 8082,
"agents": [
{
"agentId": "crypto-analyst",
"displayName": "Krypto-Analyst",
"model": "anthropic/claude-sonnet-4-5",
"systemPrompt": "Du analysierst Kryptowährungsmärkte und erstellst Trading-Signale.",
"tools": {
"Database": {
"connectionString": "Server=localhost;Database=crypto;User=agent_crypto;Password=changeme;",
"type": "mysql",
"allowedTables": ["prices", "signals", "portfolio"]
},
"FileRW": {
"rootPath": "./data/crypto/reports/",
"allowWrite": true,
"allowedExtensions": [".json", ".txt", ".md"]
},
"Mail": {
"imapHost": "imap.example.com",
"imapPort": 993,
"smtpHost": "smtp.example.com",
"smtpPort": 587,
"username": "crypto-alerts@example.com",
"password": "changeme",
"allowedRecipients": ["owner@example.com"]
}
},
"scheduler": {
"cron": "*/30 * * * *",
"runOnStart": true
},
"loopGuard": {
"maxSteps": 30,
"maxTokens": 120000,
"timeoutSeconds": 900
}
}
]
}
-55
View File
@@ -1,55 +0,0 @@
{
"instanceId": "stock-01",
"instanceName": "Aktien-Team",
"openRouterApiKey": "sk-or-DEIN-API-KEY-HIER",
"workingDirectory": "./data/stock/",
"logDirectory": "./Logs",
"webServerPort": 8081,
"agents": [
{
"agentId": "market-analyst",
"displayName": "Marktanalyse",
"model": "anthropic/claude-sonnet-4-5",
"systemPrompt": "Du bist ein erfahrener Marktanalyst. Analysiere Marktdaten und erstelle Berichte.",
"tools": {
"Database": {
"connectionString": "Server=localhost;Database=stocks;User=agent_analyst;Password=changeme;",
"type": "mysql",
"allowedTables": ["quotes", "indicators", "news"]
},
"FileRW": {
"rootPath": "./data/stock/analyst/",
"allowWrite": true,
"allowedExtensions": [".json", ".txt", ".md"]
}
},
"scheduler": {
"cron": "0 7 * * 1-5",
"runOnStart": false
},
"loopGuard": {
"maxSteps": 25,
"maxTokens": 100000,
"timeoutSeconds": 600
}
},
{
"agentId": "webdev",
"displayName": "Web-Entwickler",
"model": "google/gemini-flash-1.5",
"systemPrompt": "Du erstellst HTML-Dashboards aus bereitgestellten Daten.",
"tools": {
"FileRW": {
"rootPath": "./data/stock/wwwroot/",
"allowWrite": true,
"allowedExtensions": [".html", ".css", ".js", ".json"]
}
},
"loopGuard": {
"maxSteps": 10,
"maxTokens": 40000,
"timeoutSeconds": 300
}
}
]
}
+8 -2
View File
@@ -1,5 +1,9 @@
# Agentenkommunikation — Erfassung, Ansicht, Auswertung
> **Bauplan — noch nicht gebaut.** Die fünf Phasen sind in der [Roadmap](Roadmap.md) 3.3
> einzeln eingeordnet; die Tiefenbegrenzung (B8) ist dort Punkt 4 der Reihenfolge.
> Die Ansicht aus Phase 4 steht bereits und wartet auf den Speicher aus Phase 2.
Ziel: Die **gesamte** Kommunikation zwischen Agenten wird erfasst, ist im WhatsApp-Stil
paarweise nachlesbar und lässt sich von einem Agenten automatisiert auswerten — um zu
finden, wo die Zusammenarbeit klemmt.
@@ -222,9 +226,11 @@ zu sehen, mit wem er überhaupt spricht. Zeitraum, Kanalfilter, Freitextsuche.
trägt eine kleine Kennzeichnung des Kanals (`Rückfrage` / `Auftrag` / `Kommentar` /
`Ergebnis`) und, wo vorhanden, die anklickbare Task-Id.
**Technisch**: über den vorhandenen **WebView2**-Unterbau, wie ihn `frm_chat` schon nutzt
**Technisch**: damals über den WebView2-Unterbau von `frm_chat` gedacht. Beides gibt es
seit der Avalonia-Portierung nicht mehr — der Abschnitt ist als Entwurfsstand von damals
zu lesen; die Ansicht wäre heute eine Avalonia-Seite wie `AgentChatsPageView`
(inkl. Virtual-Host-Mapping auf einen lokalen Ordner). Der Verlauf wird als HTML
gerendert. Handgezeichnete Sprechblasen in WinForms wären ein Vielfaches an Aufwand für
gerendert. Handgezeichnete Sprechblasen wären dort ein Vielfaches an Aufwand für
ein schlechteres Ergebnis.
**Paging**: die jüngsten ~200 Nachrichten, „ältere laden" nach oben. Ein Paar mit 50.000
+3
View File
@@ -1,5 +1,8 @@
# Audit-Log & Receipts — Nachvollziehbarkeit
> **Bauplan zu einem gebauten System.** Umgesetzt; der Stand steht in der
> [Roadmap](Roadmap.md) 3.1.
Setzt A3 aus der [Roadmap](Roadmap.md) um (F-A2). Zwei zusammengehörige Dinge:
- **Audit-Log** — ein Eintrag je Tool-Aufruf: wer, wann, welches Tool, mit welchem
+4 -1
View File
@@ -1,5 +1,8 @@
# Deploymentcenter-Integration
> **Beschreibung der Verdrahtung** (Heartbeat, Fehler-Stream, Bugtracker, Updates).
> Offene Punkte dazu stehen in der [Roadmap](Roadmap.md) 3.5, nicht hier.
Stand: 2026-08-08, Deploymentcenter **2.1**. Ersetzt den früheren
`Integrationsplan-WatchDog-LicenseLabrador.md`.
@@ -179,7 +182,7 @@ Die frühere Fassung führte einen Ed25519-Public-Key als „Vertrauensanker". I
Deploymentcenter gibt es dazu keine Gegenseite — der Client liest ausschließlich das Feld
`status`. Ein Schlüssel, der nichts prüft, ist schlimmer als keiner: Er lässt Schutz
vermuten, wo keiner ist. Details in
[Deploymentcenter-Anbindung-Review](Deploymentcenter-Anbindung-Review.md), Abschnitt 2.1.
[Deploymentcenter-Anbindung-Review](archiv/Deploymentcenter-Anbindung-Review.md), Abschnitt 2.1.
### Der Projekt-Slug ist `clawddotnet`
+4 -1
View File
@@ -1,6 +1,9 @@
# Memory — Langzeitgedächtnis für Agenten
Löst K1 aus der [Bestandsaufnahme](Bestandsaufnahme-2026-07.md): Geplante Agenten
> **Bauplan zu einem gebauten System.** Umgesetzt; der Stand steht in der
> [Roadmap](Roadmap.md) 3.2. Die drei Punkte unter „Offen" laufen dort weiter.
Löst K1 aus der [Bestandsaufnahme](archiv/Bestandsaufnahme-2026-07.md): Geplante Agenten
begannen bei jedem Cron-Lauf bei null. Ein Agent, der alle 30 Minuten lief, wusste
nichts von seinem letzten Durchgang — er rief dieselben Quellen ab, zog dieselben
Schlüsse und konnte keine Entwicklung über Zeit verfolgen.
@@ -1,27 +1,35 @@
# Avalonia-Portierung — Leitfaden
# Oberfläche — Leitfaden
Für alle, die weitere Ansichten von WinForms nach Avalonia übertragen.
Stand: 2026-08-07.
Für alle, die an `src/ClawdDotNet.Desktop` arbeiten.
Stand: 2026-08-23.
---
## 1. Auftrag
## 1. Stand
Drei Bereiche des Hauptfensters sind noch Platzhalter. In dieser Reihenfolge portieren —
sie steigen im Umfang, und jede baut auf dem Muster der vorigen auf:
**Die Portierung ist abgeschlossen.** Es gibt keine Platzhalter mehr und keine
WinForms-Vorlage, gegen die man vergleichen könnte: Die alte Oberfläche
(`ClawdDotNet.csproj`, `frm_*.cs`, `UI/`, `Models/`, `EmbeddedUI/`) ist am
2026-08-23 aus dem Arbeitsbaum entfernt worden. Wer sie doch einmal braucht,
findet sie im Tag `vor-fruehjahrsputz-2026-08`.
| # | Bereich | WinForms-Vorlage | Daten aus |
|---|---|---|---|
| 1 | **Info** | `frm_main.Designer.cs`, Suchwort `tabPage_Info` | `AppHost.AppVersion`, `AppHost.BuildSummary`, `host.Instance` |
| 2 | **Sicherung** | `UI/BackupPanel.cs` + `UI/BackupPanel.Designer.cs` | `host.InstancePath`, `host.Settings`, `Core.Backup.BackupService` |
| 3 | **Aufgaben** (Jobs/Services/Verlauf) | `frm_main.cs`, Abschnitt `WORKER TAB` ab Zeile 932 | `host.Instance.Agents`, `App.Services.JobHistoryService` |
Neun Bereiche, alle nativ in Avalonia:
**Nicht anfassen:** Chat und Einstellungen. Beide sind Entwurfsarbeit, nicht Übersetzung,
und werden gesondert gemacht.
| Gruppe | Bereich | Ansichtsmodell |
|---|---|---|
| Arbeit | Chat, Agenten, Aufgaben | `ChatPageViewModel`, `AgentsPageViewModel`, `TasksPageViewModel` |
| Analyse | Token-Verbrauch, Agenten-Chats | `TokenUsagePageViewModel`, `AgentChatsPageViewModel` |
| System | Sicherung, Protokoll, Einstellungen, Info | `BackupPageViewModel`, `LogPageViewModel`, `SettingsPageViewModel`, `InfoPageViewModel` |
Die WinForms-Dateien liegen noch im Repository, sind aber **nicht mehr Teil des Builds**
(siehe Kommentar in `ClawdDotNet.slnx`). Sie sind Vorlage zum Lesen — nicht zum Kompilieren,
nicht zum Reparieren.
Dazu die Fenster `InstancePickerWindow`, `LicenseWindow`, `TextEditorWindow` und
die Dialoge zum Anlegen von Agent, Auftrag und Dienst.
Das Erscheinungsbild folgt dem Entwurf in `Mockup/` — wer daran etwas ändert,
liest zuerst `Mockup/extracted/mockup/Implementierungsleitfaden.md`.
**Was in der Oberfläche noch fehlt**, siehe [Roadmap](Roadmap.md):
die Freigabe-Ansicht für gestagte Aufrufe (A2) und die Schaltfläche
„Fehler melden" (DC1).
---
+302 -265
View File
@@ -1,308 +1,345 @@
# Roadmap
# ClawdDotNet — Roadmap
Zentrale Liste aller offenen Vorhaben. Sie löst die beiden „Vorgeschlagene
Reihenfolge"-Abschnitte in der [Bestandsaufnahme](Bestandsaufnahme-2026-07.md) und im
[Konzepte-Dokument](Konzepte-Backup-Finanz-Analyse.md) ab — die bleiben als Befund bzw.
Konzept bestehen, gepflegt wird nur noch hier.
**Dies ist die einzige Vorhabenliste.** Vor dem 2026-08-23 lag der Stand über eine
Bestandsaufnahme, drei Konzeptpapiere, vier Umsetzungspläne und zwei
Deploymentcenter-Dokumente verteilt — jedes mit eigener Reihenfolge, teils
widersprüchlich. Alles davon ist hier zusammengeführt.
Kürzel (S4, K2, T4, F-A1, …) verweisen auf die Bestandsaufnahme.
Gepflegt wird nur noch dieses Dokument. Was aus den alten Papieren wurde, steht in
[Abschnitt 7](#7--woher-das-hier-kommt).
---
## A — Beschlossen (aus dem OpenAlice-Vergleich, Juli 2026)
## Wie das hier zu lesen ist
Hintergrund: Konzeptvergleich mit [OpenAlice](https://github.com/TraderAlice/OpenAlice)
(AGPL-3.0 — Konzepte übernehmen ja, Code nein). Übernommen werden Taskboard,
Staging-Freigabe, Audit-Log und das Skill-Modell. Die Inbox-Idee entfällt zugunsten
der geplanten Matrix-Migration (A5).
### A1 — Taskboard
Aufgaben als Markdown-Dateien mit YAML-Frontmatter im `SharedWorkspace`:
`title`, `status` (`backlog | todo | in_progress | done | canceled`), `priority`,
`assignee`, optional `when` (`at` | `every` | `cron` **mit Zeitzone**).
- **Scanner statt Delay-Schleifen**: Ein Takt (~60 s) prüft, was fällig ist.
Persistiert werden nur Last-Fired-Marker — ein fehlgeschlagener Lauf bleibt der
einzige Versuch für diesen Termin, kein automatischer Retry-Sturm.
- **Assignee bestimmt die Ausführung**: `@new` = frischer Lauf ohne Historie,
`@<agent>` = bestehender Agent mit seinem Kontext, `@human` = wartet auf uns.
Das ersetzt das implizite `UseChatContext`-Flag (T7) durch eine explizite Angabe
am Auftrag.
- **Agenten-Tool**: `task_create`, `task_list`, `task_update`, `task_comment`.
Agent-zu-Agent-Delegation läuft künftig über Tasks statt über rekursives
`send_message`.
- **Migration**: Die improvisierten `coordination/*.md`-Dateien der Agenten
(task_*, status_*, broadcast) gehen im Taskboard auf.
**Detailbauplan** (aus dem Fünf-Repo-Vergleich, Juli 2026 beschlossen):
- Status zusätzlich mit **`in_review`**; im Frontmatter **`require_approval`**
(Task gilt erst nach Review als done) und **`acceptance`** (Abnahmekriterien,
gegen die das Ergebnis geprüft wird).
- **Task-Typen `approval` und `human_input`** — ein Mensch ist einfach ein
Assignee; seine Antwort ist das Task-Ergebnis und Input für Folgetasks.
- **Atomares Claiming**: Die DB verhindert, dass zwei Läufe denselben Task
ziehen. Der Scanner arbeitet mit **Claim-before-run** (at-most-once — ein
doppelter Tick findet den Claim bereits vergeben) und
**Startup-Reconciliation**: Beim Start wird Soll (Frontmatter) gegen Ist
(Marker/Claims) abgeglichen, verpasste Läufe werden erkannt statt still
übersprungen.
- **`blocked_by`-Abhängigkeiten** mit Auto-Dispatch: Wird der letzte Blocker
fertig, wird der wartende Task automatisch angestoßen. Meldet ein Agent einen
Blocker, fällt der Task und der Zuständige (Lead/Benutzer) wird benachrichtigt
(**Blocker-Eskalation**).
- **Reopen-/Feedback-Semantik**: Ergebnis + Kritik gehen per `task_comment` an
denselben Agenten zur Nachbesserung zurück, statt einen neuen Task von vorn
zu beginnen.
Damit erledigt oder aufgegangen:
| Punkt | Warum |
| Zeichen | Bedeutung |
|---|---|
| F-A5 Task-Queue | das Taskboard **ist** die Queue |
| F-A4 Run-Historie | Läufe werden am Task verknüpft und persistiert |
| B8 Rekursion `send_message` | Delegation über Tasks ist strukturell zyklenfrei |
| B6 `Task.Delay`-Überlauf | Scanner-Modell kennt keine langen Delays |
| B7 Cron in Lokalzeit | Frontmatter-`when` ist zeitzonen-explizit |
| T7 `RunAsync` vs. `ChatAsync` | Assignee-Semantik beantwortet die Frage |
| ✅ | erledigt |
| 📋 | **beschlossen**, noch nicht gebaut — kann jederzeit angefasst werden |
| ❓ | **Entscheidung nötig**, bevor gebaut werden kann |
| ❄️ | zurückgestellt — wartet auf eine Entscheidung oder ein anderes Vorhaben |
| 💭 | **Idee**, nicht beschlossen — bewusst geparkt, damit sie nicht verlorengeht |
Verzahnung: Das Marktkalender-Flag (`onlyWhenMarketOpen`, siehe C1) gehört ins
Frontmatter, nicht in einen eigenen Mechanismus.
Die **Herkunft**-Spalte trägt das alte Kürzel (S4, K2, B9, T4, F-A1, C1, DC1 …). Damit
bleiben die archivierten Papiere auffindbar, ohne dass man sie lesen muss.
Konzept-Doc: [Taskboard-Konzept](Taskboard-Konzept.md) (Dateiformat,
Wahrheitsaufteilung Datei/DB, Scanner-Verhalten, Invarianten, Migration).
### A2 — Staging-Freigabe für irreversible Aktionen (F-A1 + S4)
Konzept-Doc: [Staging-Konzept](Staging-Konzept.md).
Irreversible Aktionen (Mail senden, X posten, DB-Schreibzugriff, Datei löschen,
perspektivisch Orders) werden **gestaged statt ausgeführt**: Vorschlag → Review im
Hauptfenster → Freigabe/Ablehnung. Pro Tool/Aktion konfigurierbar:
`auto | approve | deny`.
Das bisher wirkungslose `PermissionGate` (S4) wird dabei zum zentralen
Durchsetzungspunkt ausgebaut: Policy-Prüfung, Staging-Entscheidung und Audit-Hook
(A3) an einer Stelle statt ad-hoc in jedem Tool. S4 wird nicht separat bearbeitet,
sondern geht hier auf.
Ergänzungen (Juli 2026 beschlossen):
- **Plan-Freeze**: Freigegeben wird ein eingefrorener, konkreter Aufruf — Tool,
Aktion und exakte Argumente zum Zeitpunkt des Stagings. Ausgeführt wird genau
das Eingefrorene; jede nachträgliche Änderung ist eine neue Freigabe.
- **Approval-Records**: Jede Entscheidung (Freigabe wie Ablehnung) wird als
Datensatz im Audit-Log (A3) verankert — wer, wann, was, mit welchem Ergebnis.
Sicherheitswirkung: Eine Prompt-Injection (K2) kann dann nur noch einen Vorschlag
erzeugen, keine Ausführung.
### A3 — Audit-Log (F-A2)
Konzept-Doc: [Audit-Konzept](Audit-Konzept.md).
Jeder Tool-Aufruf wird protokolliert: Agent, Lauf/Session, Zeitstempel, Argumente,
Ergebnis-Status. Append-only (JSONL oder SQLite-Tabelle auf dem vorhandenen
`SqliteStorage`).
Designregeln (aus dem OpenAlice-Provenance-Konzept):
- Herkunft wird **von der Engine gestempelt**, nie vom Agenten behauptet.
- Einträge sind unveränderlich; Korrekturen sind neue Einträge.
- Unbekannte Herkunft wird als unbekannt markiert, nicht geraten.
- Worker-Typ (Modell/Engine) und verantwortliche Session sind getrennte Begriffe.
Ergänzung (Juli 2026 beschlossen) — **Receipts**: Jeder abgeschlossene Task/Lauf
erhält einen Abschluss-Beleg mit Ergebnis-Verweis, Schritten, Tokens und Kosten
(Verknüpfung `RunUsage` ↔ Task). Damit fällt C7 („Kosten pro Ergebnis")
weitgehend als Abfallprodukt ab.
Das Audit-Log ist zugleich das Fundament für das Ergebnisregister (C7) und die
Tool-Fehlerquote aus der Leistungsanalyse.
### A4 — Skill-/Toolset-Modell (ersetzt T6)
Dreischichtig statt „alles immer im System-Prompt":
1. **Dauerhafter Kern** — Identity, Soul, unveränderliche Regeln. Schlank, damit der
Prompt-Cache (T1) stabil bleibt.
2. **Nachladbare Skills/Toolsets** — fachliche Abläufe und selten genutzte Tools
werden erst auf Anforderung geladen (`list_toolsets``load_toolset`).
3. **Selbstkorrigierende Tool-Fehler** — Fehlermeldungen nennen die gültigen
Parameter/Aktionen, statt das Modell raten zu lassen.
Mechanik (Juli 2026 beschlossen, nach GoClaw-Vorbild): Skills liegen als
`SKILL.md` mit Frontmatter (`name`, `description`) im Instanz- bzw.
Agenten-Verzeichnis. Bei wenigen Skills werden die Kurzbeschreibungen inline in
den Prompt eingebettet, bei vielen gibt es stattdessen ein `skill_search`-Tool.
Änderungen an Skill-Dateien werden per Hot-Reload übernommen.
Dazu Hermes' Selbstverbesserungs-Idee: **Agenten dürfen Skills aus Erfahrung
selbst schreiben** (das AgentEditor-Tool ist die Vorstufe). Wichtig: Ein Skill
ist Prompt-Input — agentengeschriebene Skills werden erst nach Freigabe (A2)
aktiv, sonst wäre das ein Injection-Kanal in künftige Läufe.
### A5 — Matrix/Element-Migration
Beschlossene Richtung: Die Kommunikation (Benachrichtigungen, Berichte, Chat mit
Agenten) wird auf Element/Matrix umgestellt.
- Ersetzt die im OpenAlice-Vergleich erwogene Inbox — Berichte landen in
Matrix-Räumen.
- Der Tool-Kandidat **Notify** entfällt und geht hierin auf.
- Betroffen: Telegram-Tool (Rolle klären), WebView-Chat (bleibt er Haupt-UI?),
Streaming K4 (Matrix streamt nicht — nach dieser Entscheidung neu bewerten).
Scope ist noch unbestimmt — braucht ein eigenes Konzept-Doc, bevor es in die
Reihenfolge eingeordnet wird.
### A6 — MySQL-Replikations-Spiegel (optional)
Beschlossen Juli 2026. **SQLite bleibt die einzige Wahrheit** — gearbeitet wird
ausschließlich auf der Instanz-DB. MySQL ist ein reiner, nachgelagerter Spiegel:
Er empfängt nur INSERT/UPDATE/DELETE vom Replikator; die App liest im Betrieb
**nie** daraus. Einziger Lesezweck: Wiederherstellung, falls die SQLite korrupt
ist — daneben steht der Spiegel externen Auswertungen (Dashboards, Ad-hoc-SQL)
offen, ohne die Agenten-Maschine zu berühren.
Leitplanken:
- **Outbox-Muster** an der vorhandenen Schreib-Warteschlange von `SqliteStorage`:
Replikations-Einträge lokal puffern, idempotente Upserts nach MySQL.
Nie blockierend — ist MySQL nicht erreichbar, staut die Outbox und holt auf.
- Mehrere Instanzen replizieren in denselben Spiegel; Zeilen tragen `instance_id`.
- **Restore-Pfad (Spiegel → frische SQLite) muss existieren und getestet sein** —
gleiche Regel wie beim Backup: ein ungeprüftes Restore ist eine Vermutung.
- Kein Koordinationspunkt: Claiming, Locks, Taskboard-Zustand bleiben lokal.
Die Regel „nur der Replikator schreibt, niemand liest im Betrieb" gehört ins
Konzept-Doc.
- Sicherheit: TLS zur Datenbank, Zugangsdaten über `SecretProtector`;
Output-Scrubbing der Tool-Ergebnisse wird wichtiger, weil Kontext-Daten
künftig auch im Spiegel liegen.
- Ergänzt das ZIP-Backup, ersetzt es nicht — Identity/Soul, Settings, Workspace
und Telegram-Session bleiben Sache des Instanz-Backups.
**Voraussetzung — Historie-Umzug:** `ChatHistory.json` zieht in die Instanz-DB um
(aktive Tabelle + Archiv-Tabelle mit FTS5-Volltextindex und `history_search`-Tool,
siehe K6 in Abschnitt B). Das löst
nebenbei B10 (O(n²)-Schreiblast) und gibt K6 seine Form; erst danach schützt der
Spiegel auch die Historie. Zeitlich passt A6 zu dem Server, der ggf. mit A5
(Matrix) ohnehin dazukommt.
**Bauplan** heißt: Es gibt ein Dokument, das die Umsetzung im Detail beschreibt. Wer den
Punkt anfasst, liest es zuerst.
---
## BOffen aus der Bestandsaufnahme
## 0Stand
| Punkt | Was | Stand |
|---|---|---|
| T4 | Proaktiv statt reaktiv kompaktieren | offen, unverändert |
| K2-Rest | Untrusted Content als Daten rahmen (`<untrusted_content>`) | A2 nimmt die Schärfe; die Rahmung selbst bleibt nötig |
| K4 | Streaming | **zurückgestellt** bis A5 entschieden ist |
| K6 | Historie **archivieren + durchsuchbar machen** (FTS5-Index, `history_search`-Tool) statt nur rotieren | beschlossen; Teil des Historie-Umzugs (Voraussetzung von A6) |
| Memory-Flush vor Compaction | Bevor der ContextCompactor zusammenfasst, bekommt der Agent ein eng begrenztes Fenster, Dauerhaftes per `memory_store` zu sichern — sonst wirft die Compaction Wissen weg | beschlossen (GoClaw-Muster) |
| Memory-Auto-Injection | Relevante Memory-Abstracts werden automatisch eingeblendet (Relevanzschwelle, Deckel ~200 Tokens), **in die Nutzernachricht, nie in den System-Prompt** (Prompt-Cache T1) | beschlossen; löst den offenen Punkt „Automatische Einblendung" im [Memory-Konzept](Memory-Konzept.md) |
| Output-Scrubbing | Bekannte Secret-Werte (Register des `SecretProtector`) werden zentral aus **allen** Tool-Ergebnissen maskiert, bevor sie in Kontext, Historie oder Spiegel (A6) gelangen | beschlossen; schließt die Lücke, die S3 nur für URLs schloss |
| Hygiene-Paket | B9 (`index_Count`-Race), B11/T8 (`max_tokens` setzen), B13 (`instanceId`-Inkonsistenz), F-A6-Rest (UI zum Setzen/Rotieren der Secrets) | Kleinbugs, in einem Aufwasch. B10 geht im Historie-Umzug (A6) auf |
Der Kern trägt. Engine, Taskboard, Audit, Gedächtnis, Budget, Sicherung und die
Oberfläche sind gebaut und getestet (585 Tests grün, Build fehlerfrei, keine bekannten
Sicherheitslücken in den Paketen). Die Sicherheitsbefunde S1S7 und die Bugs B1B5,
B11, B12 aus der Bestandsaufnahme sind abgearbeitet.
Erledigt seit der letzten Fortschreibung: B4 vollständig (Preise kommen live vom
`/models`-Endpunkt, unbekannte Modelle werden sichtbar gemeldet).
Drei Dinge stehen im Weg:
### B-DC — Deploymentcenter-Anbindung
1. **Die Freigabe-Ansicht fehlt.** Staging ist gebaut und greift — aber niemand kann
freigeben. Damit ist der unbeaufsichtigte Betrieb, für den das ganze A2-Vorhaben da
war, nicht möglich.
2. **A5 ist unentschieden.** Rocket.Chat ist gebaut und bedient denselben Zweck, für den
Matrix vorgesehen war. Solange das offen ist, hängen Streaming (K4), der
Notfallkanal und ein Teil von A6 in der Luft.
3. **Agenten können sich gegenseitig verklemmen.** `AgentComm.send_message` hat keine
Tiefenbegrenzung; A→B→A blockiert bis zum Timeout. Bekannt seit Juli, unverändert.
---
## 1 — Reihenfolge
Was als Nächstes ansteht, mit Begründung. Alles darunter in Abschnitt 3.
| # | Vorhaben | Warum an dieser Stelle | Bereich |
|---|---|---|---|
| **1** | **Freigabe-Ansicht** (Rest von A2) | Der einzige Punkt, der echten Betrieb blockiert. Die Dienst-API steht vollständig; es fehlt eine Seite mit Liste und zwei Schaltflächen | [3.1](#31--kontrolle-und-sicherheit) |
| **2** | **A5 entscheiden**: Matrix oder Rocket.Chat | Kostet keine Umsetzung, löst aber drei blockierte Punkte auf einmal. Deshalb früh | [2](#2--offene-entscheidungen) |
| **3** | **FileRW-Papierkorb** | Kleinster Eingriff mit realer Wirkung: `delete` ist heute endgültig, ein Aufruf mit `path: "."` räumt einen Workspace ab. Danach kann `FileRW.delete` von `approve` auf `auto` — das entlastet die Freigabe-Ansicht sofort | [3.2](#32--agenten-fähigkeiten) |
| **4** | **Delegationstiefe begrenzen** (B8) | Behebt eine echte Verklemmung. Kleiner Eingriff, aber am Agent-Gate — mit Test A13 aus der [Teststrategie](Teststrategie.md) | [3.3](#33--kommunikation-zwischen-agenten) |
| **5** | **Output-Scrubbing** | Muss **vor** allem stehen, was Tool-Ergebnisse dauerhaft speichert (Agentenkommunikation Phase 2, MySQL-Spiegel). Sonst legen wir einen Speicher an, der nachträglich bereinigt werden müsste | [3.1](#31--kontrolle-und-sicherheit) |
| **6** | **C1 Marktkalender** | Spart ab dem ersten Tag Geld: Ein `*/30`-Cron läuft heute auch Sonntag um 3 Uhr. Der Haken im Scanner ist schon da (`IMarketCalendar`) | [3.6](#36--finanz-und-analyse) |
| **7** | **A4 Skills/Toolsets** | Größter Token-Hebel nach dem Caching, und Voraussetzung dafür, dass Agenten viele Tools haben können, ohne dass jeder Schritt teuer wird | [3.2](#32--agenten-fähigkeiten) |
| **8** | **Historie-Umzug in die Instanz-DB** | Löst B10 (O(n²)-Schreiblast) und K6 (unbegrenzte Historie) in einem Zug und ist Voraussetzung für A6 | [3.4](#34--daten-und-speicher) |
| — | **DC1 „Fehler melden"** | Zwischendurch; der Client steht, es fehlt die Schaltfläche | [3.5](#35--betrieb-und-auslieferung) |
| — | **Hygiene-Paket** | Zwischendurch, unabhängig von allem | [3.7](#37--hygiene) |
Leitlinie: **erst Bremsen, dann PS.** Ein Agent, der unbeaufsichtigt läuft, braucht
Nachvollziehbarkeit und Kontrolle, bevor er mehr können soll. Deshalb stehen Freigabe,
Papierkorb, Tiefenbegrenzung und Scrubbing vor den Fähigkeiten.
---
## 2 — Offene Entscheidungen
Diese Punkte kann niemand bauen, bevor sie entschieden sind. Sie kosten kein
Entwicklungsbudget, nur eine Festlegung.
| # | Frage | Was daran hängt | Empfehlung aus dem Konzept |
|---|---|---|---|
| **E1** ❓ | **Matrix oder Rocket.Chat?** Rocket.Chat ist gebaut und bedient A5. Bleibt Matrix Ziel, oder entfällt A5? | K4 (Streaming), ob der `ChannelRouter` Pflicht oder Kür ist, wo A6 wohnt | Keine — bewusst offengelassen ([RocketChat-Konzept §4](RocketChat-Nextcloud-Konzept.md)) |
| **E2** ❓ | **Nextcloud-Identität**: ein Dienstkonto mit Ordnern je Agent, oder je Agent ein eigener Benutzer? | Zuschnitt des Nextcloud-Tools | Dienstkonto |
| **E3** ❓ | **Öffentliche Freigabe-Links** grundsätzlich erlauben (mit Freigabe) oder hart sperren? | Voreinstellung `allowPublicShares` | offen |
| **E4** ❓ | **`ask_agent` behalten** (eng gefasst, Tiefe 1) oder ersatzlos streichen und alles über Tasks? | Zuschnitt von Phase 3 der Agentenkommunikation | behalten, eng gefasst |
| **E5** ❓ | **Notfallkanal-Reihenfolge** instanzweit oder je Agent einstellbar? | Kleinigkeit am `ChannelRouter` | instanzweit |
| **E6** ❓ | **`catchUp: true\|false`** je Task abschaltbar machen? | Verhalten des Scanners bei verpassten Terminen | offen |
| **E7** ❓ | **Priorität bei mehreren fälligen Tasks**: strikt oder gewichtet? | Reihenfolge im Scanner | offen |
| **E8** ❓ | **Aufbewahrungsdauer** der Agentenkommunikation | Vorgabewert im Speicher | unbegrenzt, bis A6 steht; dann 180 Tage lokal |
| **E9** ❓ | **Automatische Orderausführung** — überhaupt? | Der gesamte Handels-Zweig. Broker-API, Teilausführungen, rechtlicher Rahmen — eigene Kategorie, kein Nebenprodukt der Analyse-Agenten | bewusst **nicht** vorgesehen |
---
## 3 — Beschlossen und offen
### 3.1 — Kontrolle und Sicherheit
Bauplan: [Staging-Konzept](Staging-Konzept.md), [Audit-Konzept](Audit-Konzept.md).
| Punkt | Herkunft | Stand | Was |
|---|---|---|---|
| Audit-Log + Receipts | F-A2, A3 | ✅ | Append-only auf SQLite. Herkunft wird von der Engine gestempelt, nie vom Agenten behauptet. Einträge unveränderlich, Korrekturen sind neue Einträge. Receipts verknüpfen Lauf ↔ Task mit Schritten, Tokens, Kosten |
| Staging-Kern | F-A1, S4, A2 | ✅ | `PermissionGate` ist zum Durchsetzungspunkt ausgebaut. Policy `auto\|approve\|deny` je Tool/Aktion, Plan-Freeze (der eingefrorene Aufruf wird ausgeführt, nicht ein neu formulierter), atomarer Übergang `pending → approved`, Fortsetzung über Folge-Task statt pausiertem Lauf |
| **Freigabe-Ansicht** | A2 | 📋 **#1** | Seite in `src/ClawdDotNet.Desktop`: offene Vorschläge listen, freigeben, ablehnen (mit Grund). `StagingService.ListPendingAsync/ApproveAsync/RejectAsync` steht bereit, `AppHost.Staging` reicht ihn durch. **Ohne sie liegt jeder gestagte Aufruf unbeantwortet** |
| **Output-Scrubbing** | — | 📋 **#5** | Bekannte Secret-Werte aus dem Register des `SecretProtector` werden zentral aus **allen** Tool-Ergebnissen maskiert, bevor sie in Kontext, Historie, Audit-`Arguments` oder Spiegel gelangen. Ein Filter an `ExecuteToolCallAsync`. Schließt die Lücke, die S3 nur für URLs schloss |
| Untrusted Content rahmen | K2-Rest | 📋 | Tool-Ergebnisse als Daten rahmen (`<untrusted_content>`) und im Kern-Prompt verankern, dass daraus keine Anweisungen befolgt werden. Staging nimmt die Schärfe, die Rahmung bleibt nötig — Finanzinhalte auf X und in Newslettern sind genau der Ort, an dem gezielt manipuliert wird |
| Secret-UI | F-A6-Rest | 📋 | Oberfläche zum Setzen und Rotieren der Zugangsdaten. Die Verschlüsselung (S7, DPAPI) steht |
| Audit-Export | — | 💭 | JSONL-Export für externe Auswertung. Passt zum A6-Spiegel |
### 3.2 — Agenten-Fähigkeiten
| Punkt | Herkunft | Stand | Was |
|---|---|---|---|
| Taskboard | A1, F-A5, F-A4 | ✅ | Markdown + YAML-Frontmatter im `SharedWorkspace`, Zustand in der DB. Scanner im 60-s-Takt statt Delay-Schleifen, atomares Claiming (at-most-once), Startup-Reconciliation, `blocked_by` mit Auto-Dispatch, Blocker-Eskalation, Reopen über `task_comment`. Assignee entscheidet: `@new`, `@<agent>`, `@human`. Bauplan: [Taskboard-Konzept](Taskboard-Konzept.md) |
| Langzeitgedächtnis | K1 | ✅ | Typisierte Tabelle statt JSON im State-Store. Scope `agent`/`shared`, optionaler `Key` (erneutes Merken aktualisiert statt anzulegen), Wichtigkeit steuert die Reihenfolge bei Kappung. Bauplan: [Memory-Konzept](Memory-Konzept.md) |
| **FileRW-Papierkorb** | — | 📋 **#3** | `delete` verschiebt in einen Papierkorb je Workspace statt endgültig zu löschen; ein Cron-Task räumt nach X Tagen auf. Danach kann `FileRW.delete` im Staging von `approve` auf `auto`. Bauplan: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-FileRW-Papierkorb-Cleanup.md) |
| **A4 Skills/Toolsets** | T6 | 📋 **#7** | Dreischichtig statt „alles immer im System-Prompt": schlanker Dauerkern (hält den Prompt-Cache stabil), nachladbare Skills als `SKILL.md` mit Frontmatter (`list_toolsets``load_toolset`, bei vielen Skills stattdessen `skill_search`), Hot-Reload über `FileSystemWatcher` mit Debounce. Tool-Fehlermeldungen nennen die gültigen Parameter, statt das Modell raten zu lassen. **Agenten dürfen Skills aus Erfahrung selbst schreiben — aber erst nach Freigabe (A2) aktiv**, sonst ist es ein Injection-Kanal in künftige Läufe |
| Memory-Flush vor Compaction | — | 📋 | Bevor der `ContextCompactor` zusammenfasst, bekommt der Agent ein eng begrenztes Fenster, Dauerhaftes per `memory_store` zu sichern — sonst wirft die Compaction Wissen weg. Harte Grenzen: max. 35 Schritte, einziges Tool `memory_store`, Timeout, günstiges Modell, höchstens einmal je Zyklus |
| Memory-Auto-Injection | — | 📋 | Relevante Abstracts automatisch einblenden (Relevanzschwelle, Deckel ~200 Tokens). **In die Nutzernachricht, nie in den System-Prompt** — sonst verfällt der Prompt-Cache |
| Proaktive Compaction | T4 | 📋 | `CompactIfNeededAsync` läuft heute **nach** dem API-Aufruf und nutzt die `promptTokens` der gerade bezahlten Anfrage — der überfüllte Prompt wurde also schon bezahlt. Vor dem Senden prüfen (`EstimateTokens` gibt es), gemessene Werte zur Kalibrierung |
| AgentEditor härten | — | 📋 | Änderungen an `Identity.md`/`Soul.md` bleiben im laufenden Betrieb möglich, werden aber freigabepflichtig, nachvollziehbar und rücknehmbar. **Reihenfolge: nach AgentInspector.** Bauplan: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-AgentEditor-Haertung.md) |
| AgentInspector | — | 📋 | Ein Supervisor-Agent kann beurteilen, ob die anderen tun, was sie sollen — über Audit-Log, Taskboard und lesenden Zugriff auf fremde Workspaces. Heute geht nur die Selbstauskunft, und die ist als Kontrollinstrument wertlos. Bauplan: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-AgentInspector-Supervisor.md) |
| Memory-Verfall | — | 💭 | Alte, unwichtige Beobachtungen nach einer Frist entfallen lassen |
| Task-Archivierung | — | 💭 | `done`/`canceled` nach einer Frist archivieren, damit `tasks/` nicht zuwächst |
| Werkzeugwunsch | — | 💭 | Agenten bekommen in den Kern-Prompt den Hinweis, sich zu melden, wenn ihnen ein Werkzeug fehlt. Der Weg ist da — ein Task an `@human`. Gehört zum Prompt-Kern von A4 |
### 3.3 — Kommunikation zwischen Agenten
Bauplan: [Agentenkommunikation-Konzept](Agentenkommunikation-Konzept.md). **Nichts davon
ist gebaut** — mit einer Ausnahme: Die Ansicht (Phase 4) steht bereits als
`AgentChatsPageView` und wartet nur auf den Datenbestand aus Phase 2.
| Punkt | Herkunft | Stand | Was |
|---|---|---|---|
| **Delegationstiefe begrenzen** | B8 | 📋 **#4** | `AgentComm.send_message` ist ein synchroner Aufruf ohne Tiefenbegrenzung. A→B→A verklemmt echt: A hält sein Gate, während es auf B wartet — ruft B nun A, wartet B auf As Gate. Löst nur der Timeout auf. Test A13 der [Teststrategie](Teststrategie.md) deckt genau das ab und **fehlt bis heute** |
| Phase 1 — Korrelation | — | 📋 | `ParentRunId` + `RootRunId` in Audit-Log und Receipts, von der Engine gestempelt. Eine Delegationskette wird damit zu einer Abfrage. Voraussetzung für alles Weitere |
| Phase 2 — Nachrichtenspeicher | — | 📋 | Tabelle `AgentMessages` + FTS5, alle Wege gleich behandelt, beide Richtungen festgehalten. **Erst nach dem Output-Scrubbing** (#5) |
| Phase 3 — `ask_agent` | — | ❄️ | Tiefe 1 mit Zyklusprüfung, Rückbau von `AgentComm`/`AgentSpawn`. Wartet auf E4 |
| Phase 4 — Paar-Ansicht | — | ✅/❄️ | Auswahl, Filterung und Darstellung sind fertig; es fehlt genau eine Abfrage (`AgentChatsPageViewModel.LoadMessages`), sobald Phase 2 steht |
| Phase 5 — `AgentCommAnalysis` | — | 📋 | Analyse-Tool, das findet, wo die Zusammenarbeit klemmt. Wer es bekommt, ist offen |
| `@human`-Tasks mitschreiben | — | 💭 | Gäbe die Paar-Ansicht auch für Mensch↔Agent, fast ohne Zusatzaufwand. Vorschlag: ja, aber nach Phase 4 |
### 3.4 — Daten und Speicher
| Punkt | Herkunft | Stand | Was |
|---|---|---|---|
| Atomares Schreiben | — | ✅ | `File.Replace`-Muster. Es war bereits eine `TokenUsage.json` beschädigt worden |
| Speicher-Fundament | — | ✅ | WAL, `busy_timeout`, Pooling, Schreib-Warteschlange im Prozess. Vorher gab es `database is locked` — das sah nach einer SQLite-Grenze aus, war aber fehlende Konfiguration |
| Sicherung + Wiederherstellung | — | ✅ | ZIP mit Manifest und Prüfsummen, `VACUUM INTO` für die DB, Secrets wahlweise auf Passphrase umgeschlüsselt (PBKDF2 + AES-GCM) oder ausgelassen — DPAPI allein überlebt den Rechnerwechsel nicht, und genau dann braucht man das Backup. Rotation, Restore mit Vorschau, Zeitplan |
| **Historie-Umzug** | K6, B10 | 📋 **#8** | `ChatHistory.json` zieht in die Instanz-DB: aktive Tabelle + Archiv-Tabelle mit FTS5 und `history_search`-Tool. Löst zugleich B10 — heute wird bei **jedem** Chat-Eintrag die vollständige Historie und der vollständige Kontext als eingerücktes JSON neu geschrieben (O(n²); bei 500 Nachrichten mehrere MB pro Nachricht). Migration nur nach frischem Backup, alte JSON erst nach verifiziertem Import löschen |
| A6 MySQL-Spiegel | — | ❄️ | **SQLite bleibt die einzige Wahrheit.** MySQL ist reiner nachgelagerter Spiegel: empfängt nur vom Replikator, die App liest im Betrieb **nie** daraus. Outbox-Muster an der vorhandenen Schreib-Warteschlange, idempotente Upserts, nie blockierend. Zeilen tragen `instance_id`. **Der Restore-Pfad Spiegel → frische SQLite muss existieren und getestet sein** — gleiche Regel wie beim Backup. Wartet auf den Historie-Umzug und auf E1 (der Server kommt ggf. mit A5) |
| MySQL-Gedächtnis | — | 💭 | Zweite `IMemoryRepository`-Implementierung, wenn mehrere Rechner dazukommen |
### 3.5 — Betrieb und Auslieferung
WatchDog und LicenseLabrador sind durch das
[Deploymentcenter](Deploymentcenter-Integration.md) ersetzt: eine Adresse, ein Token,
und dazu Update-Prüfung, Fehler-Stream und Bugtracker. Watchdog läuft wieder pro
Instanz (ein Monitor je Instanz, mit Gesundheitsprüfungen und angekündigtem Ende).
dazu Update-Prüfung, Fehler-Stream und Bugtracker. Watchdog läuft pro Instanz.
Offen:
| Punkt | Herkunft | Stand | Was |
|---|---|---|---|
| Zugangsschutz | DC-2.4 | ✅ | Lizenzschlüssel als Basic-Auth-Zugang zur Release-Ablage. Benutzername wird als `lic_<sha256[0..16]>` abgeleitet — **die Ableitung muss auf beiden Seiten zeichengenau übereinstimmen, wer eine Seite ändert, sperrt die gesamte Installationsbasis aus** |
| Update anwenden | DC6 | ✅ | Agent liegt im Paket (Prüfsumme geprüft), Rückfrage in der Oberfläche, geordnetes Herunterfahren vor dem Agentenstart, `maintenance` an den Watchdog |
| Erstinstallation | DC7 | ✅ | `--action install` + `setup.json`, live durchgespielt (Login, Katalog, Token mit Rechteschranke) |
| Release-Strecke | DC3 | ✅/📋 | Erledigt für `win-x64`/`dev` ([`deploy/publish.py`](../deploy/publish.py)). **Offen:** `linux-x64` — die Portierung, auf die das wartete, ist durch — und `prod` |
| **DC1 „Fehler melden"** | DC1 | 📋 | Der Client steht, es fehlt die Schaltfläche in der Oberfläche |
| DC2 Bugtracker-Tool | DC2 | 📋 | Agenten-Tool, das den Claim/Lease-Workflow des Deploymentcenters nutzbar macht |
| DC4 SDK als Submodul | DC4 | 📋 | Heute ein Cross-Repo-Pfad (`..\..\..\Deploymentcenter\…` in `ClawdDotNet.App.csproj`) — auf einem anderen Rechner baut das nicht. Als Git-Submodul unter `external/`. Betrifft auch die CI |
| DC5 Evaluator-Cron | DC5 | 📋 | Betreiberaufgabe: Cron einrichten, Token ausstellen, `parent_source` pflegen. **Ohne den Cron ist die Überwachung wertlos** |
| DC8 Signaturpflicht | DC8 | ❄️ | Schlüssel steht, ab 0.1.2 wird signiert, Prüfung beidseitig getestet. **Blockiert:** `--require-signature` gibt es nur als Kommandozeilenschalter; `UpdateClient.LaunchUpdateAgent` — der empfohlene Weg — reicht ihn nicht durch. Selbst nachbauen hieße `--restart`, `--wait-for-pid` und `--wait-timeout` verlieren. Gemeldet |
| Release-Guard beim Upload | — | 📋 | Veröffentlichen löst `regenerateForProject` nicht aus; ein frisches Produktverzeichnis steht bis zum nächsten Sechs-Stunden-Turnus ohne `.htaccess` da. Ein Aufruf am Ende von `publish` würde das Fenster schließen. **Betreiberseite** |
| Linux-Betrieb | — | 📋 | Der Kern ist portabel: alle Bibliotheks- und Testprojekte auf `net10.0`, kein `DllImport`, keine Registry, kein `System.Drawing`. Windows steckt noch an drei Stellen — DPAPI, Groß-/Kleinschreibung bei Pfadvergleichen, Zeitzonen-IDs. Der teure Teil (8.900 Zeilen WinForms) ist mit der Avalonia-Portierung entfallen |
| systemd-Dienst | — | 💭 | Kopfloser Betrieb. `ClawdDotNet.App` läuft bereits ohne Fenster — der Schichtschnitt ist genau dafür da. `NonInteractiveLicensePrompt` und `ConsoleLicensePrompt` liegen ungenutzt bereit |
| Punkt | Was | Bemerkung |
### 3.6 — Finanz und Analyse
Dieser Block ist bewusst **nachgelagert**: ClawdDotNet soll zuerst als allgemeiner
Agenten-Client felsenfest sein, die handelsspezifischen Werkzeuge kommen danach.
| Punkt | Herkunft | Stand | Was |
|---|---|---|---|
| **C1 Marktkalender** | C1 | 📋 **#6** | Zwei Bausteine: `onlyWhenMarketOpen: "NYSE"` im Task-Frontmatter (der Lauf wird schlicht übersprungen, wirkt ohne Zutun des Modells) und ein `MarketCalendar`-Tool für Fragen des Agenten. Handelskalender ändern sich selten — eine gepflegte Datei reicht, keine externe Abhängigkeit. Der Haken im Scanner ist da |
| C2 Indicators | C2 | 📋 | Gleitende Durchschnitte, RSI, ATR, Volatilität, Korrelation, Drawdown, Positionsgröße nach Risiko — **im Code gerechnet**. Sprachmodelle rechnen unzuverlässig, und die Zahlenkolonnen müssen dafür durch den Kontext. Spart Tokens *und* verbessert die Qualität |
| C3 Datenaktualität | C3 | 📋 | `DirectAPI` liefert `dataAsOf` mit, aber nichts wertet es aus — ein Agent kann ungehindert auf drei Tage alten Kursen argumentieren. `maxAgeSeconds` je Tool: ablehnen oder unübersehbar kennzeichnen, nicht stillschweigend durchreichen |
| C4 Termine und Fundamentaldaten | C4 | 📋 | Earnings, Dividenden, Splits, SEC EDGAR (8-K, 10-Q, 13F — frei und gut strukturiert), Wirtschaftstermine. Für „Finanznachrichten" ist der Kalender oft wichtiger als der Kurs |
| C5 Bestandsregister | C5 | 📋 | `stock_add` ist eine Wissenssammlung, kein Bestand. Eigene Tabelle mit Positionen, auch rein zur Beobachtung. Grundlage für C7/C8 |
| C6 Nachrichten-Entdopplung | C6 | 📋 | Dieselbe Meldung läuft über zehn Quellen — man zahlt zehnmal, und der Agent hält es für zehn unabhängige Signale. Das verzerrt die Einschätzung systematisch. `SeenItems` mit Prüfsumme über den normalisierten Titel plus Ähnlichkeitsabgleich |
| C7 Ergebnisregister | C7 | 📋 | Tabelle `AgentOutput` am Lauf: Art, Betreff, Verweis. Macht aus „Kosten pro Lauf" die nützlichere Größe **Kosten pro Ergebnis**. Fällt weitgehend aus den A3-Receipts ab |
| C8 Falsifizierbare Aussagen | C8 | 📋 | Das eigentliche Leistungsmaß. Der Agent legt sich fest (Subjekt, Aussage, Horizont, Konfidenz); ein Auflösungs-Task prüft gegen die tatsächlichen Kurse — kein Mensch muss bewerten. Daraus Trefferquote, **Brier-Score** (misst, ob die Konfidenz ehrlich war — ein Agent, der bei 0,9 nur in 60 % recht hat, ist überheblich, das bleibt bei reiner Trefferquote unsichtbar) und Vergleich gegen eine Nulllinie. **Unter 30 aufgelösten Aussagen keine Rangliste, nur Rohzahlen** — sonst optimiert man Rauschen. Braucht C5 und C7 |
| Leerlaufquote | — | 📋 | Die wirksamste einfache Kennzahl: Läufe ohne greifbares Ergebnis. 40 % Leerlauf heißt meist Zeitplan-Problem — genau das, was C1 löst. Aus vorhandenen Daten |
| `stock_add` herauslösen | T6 | 📋 | Fachfremdes Feature, das das `FileRW`-Schema für alle aufbläht, auch für Agenten, die es nie nutzen |
| Orderausführung | — | 💭 | Siehe E9. Eigene Kategorie, ausdrücklich kein Nebenprodukt |
### 3.7 — Hygiene
Kleinbugs, in einem Aufwasch zu erledigen.
| Punkt | Herkunft | Stand | Was |
|---|---|---|---|
| `index_Count`-Race | B9 | 📋 | `FileRWTool` ruft `index_Count` auf, nachdem das Lock freigegeben ist — Race mit parallelen `stock_add`, plus ein überflüssiges vollständiges Parsen. Der Zähler ist innerhalb des Locks ohnehin bekannt |
| `instanceId`-Inkonsistenz | B13 | 📋 | `RunAsync`/`ChatAsync` bekommen `instanceId` als Parameter, `SendMessageAsync`/`SpawnAgentAsync` nutzen das Feld `_instanceId`, das nur bei `SetAgentConfigProvider` gesetzt wird — sonst leer. In Produktion unkritisch |
| DST-Randfall | — | 📋 | Eine bei der Zeitumstellung nicht existierende Ortszeit („02:30" im Frühjahr) wird übersprungen statt verschoben. Tests R4/R5 der [Teststrategie](Teststrategie.md) |
| Nullable-Warnungen | — | 📋 | 45 Build-Warnungen, Schwerpunkt `Mail` (basisnah) und `WebFetch`/`DirectAPI` (handelsnah) |
| Paket-Anhebungen | — | ✅ | `SQLitePCLRaw.bundle_e_sqlite3` 2.1.13 und `SharpCompress` 0.48.0 stehen als **direkte Verweise, obwohl kein Code sie aufruft** — sie heben verwundbare transitive Fassungen an. Sehen aus wie toter Ballast und sind es nicht |
### 3.8 — Tests
Grundlage: [Teststrategie](Teststrategie.md). Stand: 585 grün, 6 übersprungen.
| Punkt | Stand | Was |
|---|---|---|
| DC1 | Oberfläche „Fehler melden" | Client vorhanden, Schaltfläche fehlt |
| DC2 | Agenten-Tool für den Bugtracker | macht den Claim/Lease-Workflow des Deploymentcenters nutzbar |
| DC3 | Release-Strecke | **erledigt für win-x64/dev** ([`deploy/publish.py`](../deploy/publish.py), 0.1.1 veröffentlicht). Offen: `linux-x64` nach der Avalonia-Portierung, `prod` |
| DC4 | SDK als Git-Submodul unter `external/` statt Cross-Repo-Pfad | betrifft auch die CI |
| DC5 | Betreiber: Evaluator-Cron einrichten, Token ausstellen, `parent_source` pflegen | **ohne den Cron ist die Überwachung wertlos** |
| DC6 | Update anwenden statt nur melden | **erledigt.** Agent liegt im Paket (Prüfsumme geprüft), Rückfrage in der Oberfläche, geordnetes Herunterfahren vor dem Agentenstart, `maintenance` an den Watchdog |
| DC7 | Erstinstallation über `--action install` + `setup.json` | **erledigt.** Setup-Kette live durchgespielt (Login, Katalog, Token mit Rechteschranke), Round-Trip SDK-Writer → SettingsManager trägt. `fileWindows`/`fileLinux` gesetzt |
| DC8 | Signaturpflicht (`--require-signature`) | Schlüssel steht, ab 0.1.2 wird signiert, Prüfung beidseitig getestet. **Blockiert:** `LaunchUpdateAgent` reicht den Schalter nicht durch — aus der Anwendung heraus nicht erzwingbar (gemeldet) |
Details: [Deploymentcenter 2.22.4 Integrationsplan](Deploymentcenter-2.4-Integrationsplan.md).
Der Zugangsschutz aus 2.4 (Lizenzschlüssel als Basic-Auth-Zugang zur Release-Ablage) ist
bereits umgesetzt — er muss im **ersten** veröffentlichten Release enthalten sein.
| **A13** | 📋 | `send_message` A→B→A wird nach `maxDelegationDepth` abgebrochen statt zu verklemmen. Gehört zwingend zu Reihenfolge-Punkt #4 |
| R4/R5 | 📋 | Zeitumstellung Oktober (feuert genau einmal) und März (wird nicht übersprungen) |
| Oberfläche | — | Hat **keine** Testabdeckung. Die leisen Fehler (Fäden, unbegrenzt wachsende Listen, nicht behandeltes Fenster-Schließen) fallen ausschließlich beim Laufen auf — Abschnitt 4 des [Oberflächen-Leitfadens](Oberflaechen-Leitfaden.md) ist deshalb Pflichtlektüre vor jeder Abgabe |
---
## COffen aus dem Finanz-/Analyse-Konzept
## 4Ideenspeicher
Punkte 12 von dort (atomares Schreiben, Backup/Restore inkl. UI) sind umgesetzt.
Nicht beschlossen. Steht hier, damit es nicht verlorengeht — nicht, weil es demnächst
gebaut wird.
| # | Was | Bemerkung |
### Tool-Kandidaten
| Tool | 💭 Nutzen | Bemerkung |
|---|---|---|
| C1 | Marktkalender (`onlyWhenMarketOpen` + `MarketCalendar`-Tool) | Scheduler-Teil gehört ins Taskboard-Frontmatter (A1) |
| C2 | `Indicators`-Tool — deterministische Berechnung | Qualität hoch, Tokens runter |
| C3 | Datenaktualität erzwingen (`maxAgeSeconds`) | |
| C4 | Termine & Fundamentaldaten (Earnings, EDGAR, Wirtschaftskalender) | |
| C5 | Bestandsregister (Positionen) | Grundlage für C7/C8 |
| C6 | Nachrichten-Entdopplung (`SeenItems`) | |
| C7 | Ergebnisregister (Stufe 2, „Kosten pro Ergebnis") | fällt weitgehend aus den A3-Receipts ab |
| C8 | Falsifizierbare Aussagen + Auflösung, Brier-Score (Stufe 3) | braucht C5, C7 und einen Auflösungs-Task (A1) |
| **WebSearch** | Agenten können heute nur bekannte Domains abrufen, aber nichts *finden*. Brave/Tavily/SearXNG | Bauplan liegt vor: [Umsetzungsplan](umsetzungsplaene/UMSETZUNGSPLAN-WebSearch-Tool.md). **Ausdrücklich zuletzt** — größter Sicherheitshebel, deshalb erst nach Papierkorb, Inspector und AgentEditor-Härtung |
| **Nextcloud** | Ort, an dem Agenten Ergebnisse ablegen. Datei lokal erzeugen → hochladen → Link zurück in den Chat | Nextcloud hat keine API, die Inhalte *erzeugt*. Für PDF/XLSX kann Collabora als Konverter dienen. Bauplan: [RocketChat-Nextcloud-Konzept §5](RocketChat-Nextcloud-Konzept.md). Lohnt sich unabhängig vom Ausgang von E1 |
| **Http** | Generisches REST-Tool mit Allowlist je 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 |
| **Git** | Für den „Senior Developer"-Agenten | |
| **Vision** | Charts und Screenshots analysieren | |
| ~~Notify~~ | — | **Gestrichen**, geht in A5 auf |
### Weitere Ideen
| Idee | 💭 |
|---|---|
| Rocket.Chat Realtime/DDP statt Polling | Antwortzeit ~1 s statt ~30 s. Optional, Phase 6 |
| Collabora-Konvertierung | Berichte in Büroformaten. Hängt an E-Frage 5 (ist der `convert-to`-Endpunkt freigebbar?) |
| `ChannelRouter` + Eskalation | Erkennt den Ausfall des Chat-Kanals und weicht auf Telegram aus. **Kein Nice-to-have**, sobald der Chat der Hauptweg ist — sonst ist er ein Einzelpunkt, dessen Ausfall niemand meldet. Hängt an E1 |
| K4 Streaming | ❄️ Zurückgestellt bis E1 entschieden ist — Matrix streamt nicht |
---
## DToolkandidaten (unbeschlossen)
## 5Erledigt
WebSearch, Http (generisch mit Allowlist), Shell (sandboxed), Git, Vision.
Notify ist gestrichen — geht in A5 auf.
Damit die Chronik nicht mit den alten Papieren verschwindet.
**Sicherheit:** S1 `SqlGuard` statt Teilzeichenketten-Prüfung · S2 `YouTubeUrl` +
`ArgumentList` gegen Options-Injection · S3 `UrlSanitizer` gegen API-Key-Leak ins Modell ·
S4 `PermissionGate` zum Durchsetzungspunkt ausgebaut (in A2 aufgegangen) · S5 `UrlGuard`,
Redirects einzeln geprüft · S6 `WorkspacePath` auf Verzeichnisgrenzen · S7
`SecretProtector` (DPAPI).
**Bugs:** B1/B14 Compaction — sicherer Schnittpunkt, kein doppelter System-Prompt ·
B2 Chat-Läufe je Agent serialisiert · B3/T5 `maxCumulativeTokens` von `maxContextTokens`
getrennt · B4 Prompt/Completion getrennt erfasst, Preise live vom `/models`-Endpunkt ·
B5/T2 Tool-Ergebnisse zentral gekappt · B6/B7 gegenstandslos, seit der Scanner die
Delay-Schleifen ersetzt · B11/T8 `max_tokens` aus `LoopGuard.MaxResponseTokens` ·
B12 Retry mit Backoff für 429/5xx.
**Konzepte:** K1 Gedächtnis · K3 Testfundament · K5 Budget-Guard (F-A3) · A1 Taskboard
(mit F-A4, F-A5) · A3 Audit-Log (F-A2) · A2-Kern.
**Token:** T1/T9 Prompt-Caching mit Breakpoints, `cached_tokens` gemessen · T3 günstiges
Modell für die Zusammenfassung · T7 durch die Assignee-Semantik des Taskboards
beantwortet.
**Betrieb:** Alt-Scheduler abgelöst — `AgentScheduler`, `ToolJobScheduler` und
`SchedulerDelay` gelöscht, alles ist ein Task · Atomares Schreiben · Backup und Restore
inkl. Oberfläche · Deploymentcenter statt WatchDog und LicenseLabrador · DC3 (win-x64),
DC6, DC7, Zugangsschutz 2.4 · Avalonia-Portierung, WinForms entfernt.
**Frühjahrsputz 2026-08-23:** automatische Sicherung war nie gestartet · doppelte
Sicherungs-Einstellungen auf der Einstellungsseite überschrieben die der Sicherungsseite ·
NU1903 und NU1902 geschlossen.
---
## Vorgeschlagene Reihenfolge
## 6 — Modell-Einstufung
| # | Vorhaben | Begründung |
Die Entwicklung läuft mit Opus 4.6. Die meisten Vorhaben sind damit gut machbar, sofern
die hier notierten Vorgaben mitgegeben werden. Die zwei ursprünglich für Opus 5 / Fable
markierten Stellen sind inzwischen gebaut — die Begründung bleibt stehen, weil sie
erklärt, *warum* es so gebaut ist.
| Vorhaben | Einstufung | Vorgabe |
|---|---|---|
| 1 | A1 Taskboard | Fundament; löst sechs bestehende Punkte auf einmal |
| 2 | A3 Audit-Log | klein, sofort nützlich; muss vor A2 da sein, damit Freigaben protokolliert werden |
| 3 | A2 Staging-Freigabe | größter Sicherheitsgewinn; Voraussetzung für unbeaufsichtigten Betrieb |
| 4 | C1 Marktkalender | spart sofort Kosten; nutzt A1-Frontmatter |
| 5 | A4 Skills/Toolsets | Token-Hebel, Cache-stabil |
| 6 | C2 Indicators | Qualität + Kosten |
| 7 | C7 + C8 Ergebnisregister, Aussagen | das eigentliche Leistungsmaß; braucht A3 |
| — | Hygiene-Paket (B) | zwischendurch, unabhängig |
| — | Historie-Umzug in die Instanz-DB | löst B10 + K6; Voraussetzung für A6 |
| — | A6 MySQL-Spiegel | nach dem Historie-Umzug; natürliches Zuhause auf dem A5-Server |
| — | A5 Matrix | eigenes Konzept-Doc zuerst; Scope klären, dann einordnen |
| A1 Scanner-Kern | war ⚠️ Opus 5 / Fable | At-most-once, Claim-CAS, Zusammenspiel mit den serialisierten Chat-Läufen — genau die Fehlerklasse, die hier schon einmal schiefging. Gebaut mit Property-Tests für die Invarianten |
| ✅ A2 Fortsetzung nach Freigabe | 4.6 mit Vorgabe | **Kein pausierter, im Speicher gehaltener Lauf.** Der Lauf endet beim Staging regulär, die Freigabe erzeugt einen Folge-Task mit dem eingefrorenen Aufruf. Echtes Suspend/Resume wäre Fable-Terrain — und ist damit unnötig |
| Freigabe-Ansicht | 4.6 | Reine Oberflächenarbeit gegen eine fertige API |
| Delegationstiefe (B8) | ⚠️ **mit Vorsicht** | Fasst das Agent-Gate an, an dem schon ein Deadlock lauerte. Erst Invarianten festlegen, dann bauen, Test A13 zwingend |
| Output-Scrubbing | 4.6 | Ein zentraler Filter an `ExecuteToolCallAsync`, Werte aus dem Secret-Register |
| A4 Skills | 4.6 | `FileSystemWatcher` mit Debounce (~500 ms); agentengeschriebene Skills erst nach Freigabe aktiv |
| Memory-Flush vor Compaction | 4.6 mit Anleitung | Der `ContextCompactor` hatte B1/B14 — die bestehenden Paarungs-Tests müssen unverändert grün bleiben |
| Historie-Umzug + FTS5 | 4.6 | Migration nur nach frischem Backup; alte JSON erst nach verifiziertem Import löschen |
| A6 MySQL-Spiegel | 4.6 mit Anleitung | Outbox mit Wasserzeichen, idempotente Upserts, nie blockieren. **Der getestete Restore-Pfad ist Teil der Definition of Done** |
| C1, C2 | 4.6 | Reine Fachlogik, deterministisch testbar |
Leitlinie der Reihung: erst Nachvollziehbarkeit und Kontrolle (Audit, Staging),
dann Fähigkeiten — ein Agent, der unbeaufsichtigt läuft, braucht zuerst Bremsen,
dann PS.
Generell: Neue Subsysteme kommen mit Tests nach der [Teststrategie](Teststrategie.md).
Bei den markierten Punkten sind die Invarianten-Tests kein Nice-to-have, sondern die
Absicherung dafür, dass ein schwächeres Modell sie umsetzen darf.
---
## Umsetzung mit Opus 4.6 — Einstufung
## 7 — Woher das hier kommt
Die Entwicklung erfolgt mit Opus 4.6. Die meisten Vorhaben sind damit gut
machbar, sofern die hier notierten Vorgaben mitgegeben werden. Zwei Stellen
berühren Nebenläufigkeits-Invarianten bzw. Engine-Querschnitte — sie sind für
Opus 5 / Fable markiert oder durch eine Architektur-Vorgabe entschärft.
### Weiter gültig — Baupläne
| Vorhaben | Einstufung | Vorgabe / Begründung |
|---|---|---|
| A1: Dateiformat, Frontmatter-Parsing, `task_*`-Tool, Migration | 4.6 | klar spezifizierbar, gut testbar |
| A1: **Scanner-Kern** (atomares Claiming, Auto-Dispatch, Reconciliation) | ⚠️ **Opus 5 / Fable** | At-most-once-Semantik, Claim-CAS und das Zusammenspiel mit den seit B2 serialisierten Chat-Läufen sind genau die Fehlerklasse, die hier schon einmal schiefging. Falls doch 4.6: erst Konzept-Doc, Umsetzung strikt dagegen, Property-Tests für die Invarianten („nie zwei Claims auf einen Task", „kein Dispatch bei offenem Blocker", „doppelter Tick = ein Lauf") |
| A2: Gate, Policy, Staging-Queue, Review-UI, Approval-Records | 4.6 | mit der folgenden Architektur-Vorgabe |
| A2: **Fortsetzung nach Freigabe** | 4.6 nur mit Vorgabe | **Kein pausierter, im Speicher gehaltener Lauf.** Vorgabe: Der Lauf endet beim Staging regulär — das Tool liefert „zur Freigabe vorgelegt" als Ergebnis, der Agent schließt ab. Die Freigabe erzeugt einen Folge-Task (A1), der den Agenten mit dem **eingefrorenen** Aufruf weckt. Echtes Suspend/Resume eines laufenden `ChatAsync` wäre Fable-Terrain — und ist mit dieser Vereinfachung unnötig |
| A3: Audit-Log + Receipts | 4.6 | append-only, klares Schema, keine Nebenläufigkeitsfallen |
| A4: Skills | 4.6 | `FileSystemWatcher` mit Debounce (~500 ms); agentengeschriebene Skills erst nach Freigabe aktiv (siehe A4) |
| Memory-Flush vor Compaction | 4.6 mit Anleitung | Harte Grenzen: max. 35 Schritte, einziges Tool `memory_store`, Timeout, günstiges Modell (wie T3), höchstens einmal je Compaction-Zyklus. Vorsicht: Der ContextCompactor hatte B1/B14 — die bestehenden Paarungs-Tests müssen unverändert grün bleiben |
| Memory-Auto-Injection | 4.6 | in die Nutzernachricht, nie in den System-Prompt (sonst verfällt der Prompt-Cache T1); Deckel ~200 Tokens |
| Output-Scrubbing | 4.6 | ein zentraler Filter an der Stelle, wo Tool-Ergebnisse in den Kontext gelangen (`ExecuteToolCallAsync`); Werte aus dem Secret-Register |
| Historie-Umzug + FTS5 + `history_search` | 4.6 | Migration nur nach frischem Backup; alte JSON-Dateien erst nach verifiziertem Import löschen |
| A6: MySQL-Spiegel | 4.6 mit Anleitung | Outbox mit Wasserzeichen, idempotente Upserts, nie blockieren; der getestete Restore-Pfad ist Teil der Definition of Done |
| C1 Marktkalender, C2 Indicators | 4.6 | reine Fachlogik, deterministisch testbar |
Aus der Roadmap verlinkt. Wer einen Punkt anfasst, liest das zugehörige Dokument.
Generell: Neue Subsysteme (Scanner, Staging, Audit, Replikator) kommen mit Tests
nach der [Teststrategie](Teststrategie.md) — die Invarianten-Tests sind bei den
markierten Punkten kein Nice-to-have, sondern die Absicherung dafür, dass ein
schwächeres Modell sie umsetzen darf.
| Dokument | Wofür |
|---|---|
| [Taskboard-Konzept](Taskboard-Konzept.md) | Dateiformat, Wahrheitsaufteilung Datei/DB, Scanner-Verhalten, Invarianten |
| [Audit-Konzept](Audit-Konzept.md) | Schema, Provenance-Regeln, Receipts |
| [Staging-Konzept](Staging-Konzept.md) | Policy-Auflösung, Plan-Freeze, Fortsetzung nach Freigabe — **Bauplan für Punkt #1** |
| [Memory-Konzept](Memory-Konzept.md) | Modell, Schlüssel-Semantik, Abrufreihenfolge |
| [Agentenkommunikation-Konzept](Agentenkommunikation-Konzept.md) | Fünf Phasen, Korrelation, Nachrichtenspeicher |
| [RocketChat-Nextcloud-Konzept](RocketChat-Nextcloud-Konzept.md) | Rocket.Chat gebaut; Nextcloud, Rückweg und `ChannelRouter` offen. Enthält die Messung gegen die echte Instanz |
| [Deploymentcenter-Integration](Deploymentcenter-Integration.md) | Wie Heartbeat, Fehler-Stream, Bugtracker und Updates verdrahtet sind |
| [umsetzungsplaene/](umsetzungsplaene/) | Vier Detailpläne: FileRW-Papierkorb, AgentInspector, AgentEditor-Härtung, WebSearch |
### Weiter gültig — Handbücher
| Dokument | Wofür |
|---|---|
| [Oberflaechen-Leitfaden](Oberflaechen-Leitfaden.md) | Arbeiten an `ClawdDotNet.Desktop`: Schichtschnitt, Fäden, Avalonia-12-Fallen, Prüfliste |
| [ToolDevelopmentGuide](ToolDevelopmentGuide.md) | Ein neues Agenten-Tool bauen |
| [InstanceSetupGuide](InstanceSetupGuide.md) | Eine Instanz einrichten |
| [Teststrategie](Teststrategie.md) | Was wie getestet wird, Fallkatalog |
### Archiviert
Vollständig in dieses Dokument überführt. Sie bleiben als Begründung und Herkunft
lesbar, werden aber **nicht mehr fortgeschrieben** — siehe [docs/archiv/](archiv/).
| Dokument | Was daraus wurde |
|---|---|
| `Bestandsaufnahme-2026-07.md` | S1S7, B1B14, K1K6, T1T9, F-A1…F-A7 — erledigte in Abschnitt 5, offene in 3.1/3.2/3.7 |
| `Konzepte-Backup-Finanz-Analyse.md` | Backup ✅; Finanzteil als C1C6; Leistungsanalyse als C7/C8 und Leerlaufquote |
| `Linux-Portierung-Analyse.md` | Die drei Kernstellen und das `linux-x64`-Release in 3.5. Der teure Teil (WinForms) ist entfallen |
| `Lizenz-HardwareId-v2-Implementierungsvorschlag.md` | **Gegenstandslos** — LicenseLabrador ist durch das Deploymentcenter ersetzt |
| `Deploymentcenter-Anbindung-Review.md` | Befunde behoben, in 3.5 aufgegangen |
| `Deploymentcenter-2.4-Integrationsplan.md` | DC-Punkte in 3.5, inklusive der blockierten Signaturpflicht und der Betreiber-Restpunkte |
| `ClawdDotNet_StartPrompt.md` und die drei Prompt-Anhänge | Entwicklungs-Prompts der Anfangszeit. Eine Regel gilt weiter: die Pflichtfelder `fetchedAt`/`dataAsOf`/`source` der Internet-Tools |
+13 -6
View File
@@ -6,7 +6,14 @@ Der typische Ablauf ist die Kombination aus beidem — „schreib mir die Auswer
sie in die Cloud" im Chat, Datei in Nextcloud, Link zurück in den Chat.
Dieses Dokument prüft die Machbarkeit, legt den Schnitt fest und benennt die Punkte, die
vor der Umsetzung entschieden werden müssen. **Es ist noch keine Umsetzungsfreigabe.**
vor der Umsetzung entschieden werden müssen.
> **Stand 2026-08-23:** Der Rocket.Chat-Teil ist **umgesetzt** —
> `src/ClawdDotNet.Tools.RocketChat`, im `AppHost` registriert, `send_file`
> freigabepflichtig. Der Befund unten bleibt als Begründung stehen; Abschnitt 2.0
> hält fest, was die Messung gegen die echte Instanz an den Annahmen korrigiert hat.
> **Offen:** der Nextcloud-Teil und die Kollision mit Roadmap A5 (Matrix) — die ist
> eine Entscheidung, keine Umsetzung.
Verwandt: [Taskboard-Konzept](Taskboard-Konzept.md) (Scanner/Wake), [Staging-Konzept](Staging-Konzept.md)
(Freigaben), [Audit-Konzept](Audit-Konzept.md), [Roadmap](Roadmap.md) (A5 — siehe Konflikt unten).
@@ -177,7 +184,7 @@ der richtige Ansatz und wird von Rocket.Chat direkt unterstützt.
- Authentifiziert wird jeder Aufruf über zwei Header: `X-Auth-Token` und `X-User-Id`.
**Entscheidung, die ich empfehle:** Das Anlegen der Benutzer ist **kein Agenten-Tool**.
Es ist eine einmalige Einrichtungsfunktion in der WinForms-Oberfläche
Es ist eine einmalige Einrichtungsfunktion in der Oberfläche
(Instanz-Einstellungen → Rocket.Chat → „Agenten-Benutzer anlegen"). Sonst müsste ein
Agent ein Admin-Token halten — und ein Admin-Token in Reichweite einer Prompt-Injection
ist genau das, was A2 verhindern soll. Der Admin-Token liegt in der **Instanz**-Konfiguration,
@@ -444,7 +451,7 @@ Das ist die Anforderung, die die Architektur bestimmt — nicht der Chat selbst.
| Kanal | Unabhängig von Rocket.Chat? | Richtung |
|---|---|---|
| WinForms-Chat (`frm_chat`) | vollständig — läuft in der App selbst | beide |
| Chat-Seite in der App (`ChatPageView`) | vollständig — läuft in der App selbst | beide |
| Telegram-Bot-Tool | ja — fremde Infrastruktur | beide |
| Mail-Tool | ja, sofern der Mailserver anderswo läuft | beide |
| Web-Chat / `ClawdDotNetApi` | ja, aber nur im lokalen Netz | beide |
@@ -483,7 +490,7 @@ Ein Tool-Job `rocketchat_health` (Takt ~5 Minuten, `GET /api/info`):
**Eingehend während des Ausfalls:** Der Telegram-Poll-Job bleibt dauerhaft aktiv, nur mit
langsamem Takt (z. B. alle 5 Minuten). Er kostet nichts, wenn nichts kommt — und ist im
Ernstfall der Weg, auf dem *du* die Agenten erreichst. Der WinForms-Chat ist ohnehin immer
Ernstfall der Weg, auf dem *du* die Agenten erreichst. Die eingebaute Chat-Seite ist ohnehin immer
da, solange die App läuft.
**Entschieden (August 2026): Telegram ist der Notfallkanal.** Die Kanalliste lautet damit
@@ -706,9 +713,9 @@ Damit der Zuschnitt klar ist:
- Keine Rocket.Chat-**App** (Apps-Engine, TypeScript im Server) — wir bleiben Client.
- Keine Verwaltung von Rocket.Chat durch Agenten (Benutzer anlegen, Räume erstellen,
Rechte vergeben). Das ist Admin-Arbeit in der WinForms-Oberfläche.
Rechte vergeben). Das ist Admin-Arbeit in der Oberfläche.
- Keine Sprach-/Videofunktionen, keine Nextcloud Talk-Anbindung.
- Kein Ersatz für den WinForms-Chat — der bleibt und ist die unterste Rückfallebene.
- Kein Ersatz für die eingebaute Chat-Seite — die bleibt und ist die unterste Rückfallebene.
- Keine Ende-zu-Ende-Verschlüsselung. Rocket.Chat kann das, aber verschlüsselte Räume sind
über die REST-API nicht lesbar. Agenten arbeiten in unverschlüsselten Räumen — das ist
eine bewusste Einschränkung, die du kennen solltest.
+2 -1
View File
@@ -102,7 +102,8 @@ Erst nach gewonnenem Übergang wird der eingefrorene Aufruf ausgeführt.
## Offen
- **Review-Oberfläche** im Hauptfenster (Liste der offenen Vorschläge, Freigeben/Ablehnen)
— die Dienst-API steht bereit; die WinForms-Ansicht ist die verbleibende Integration.
— die Dienst-API steht bereit; die Freigabe-Ansicht in `src/ClawdDotNet.Desktop` ist
die verbleibende Integration und der einzige offene Punkt von A2.
- **Output-Scrubbing** greift auch hier auf den gespeicherten Argument-JSON, sobald es
steht (eigener Roadmap-Punkt).
- **Orders** (Handelsaufträge) reihen sich später als weitere `approve`-Aktionen ein.
+7 -2
View File
@@ -1,5 +1,10 @@
# Taskboard — Aufgaben statt Delay-Schleifen
> **Bauplan zu einem gebauten System.** Dateiformat, Wahrheitsaufteilung Datei/DB,
> Scanner-Verhalten und Invarianten. Umgesetzt; der Stand steht in der
> [Roadmap](Roadmap.md) 3.2. Die Abschnitte im Präsens beschreiben teils den Zustand
> *vor* der Umsetzung — sie begründen den Entwurf.
Setzt A1 aus der [Roadmap](Roadmap.md) um. Das Taskboard ist das Fundament, auf dem
Audit (A3), Staging (A2), Marktkalender (C1) und das Ergebnisregister (C7/C8)
aufsetzen. Es löst zugleich sechs Altpunkte auf einmal (F-A5 Queue, F-A4 Run-Historie,
@@ -15,8 +20,8 @@ Invarianten, Migration.
Heute gibt es zwei getrennte, je für sich unzureichende Wege, einen Agenten Arbeit
tun zu lassen:
- **Der Cron-Scheduler** ([`AgentScheduler`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs),
[`ToolJobScheduler`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs)) hängt starr
- **Der Cron-Scheduler** (`AgentScheduler`, `ToolJobScheduler` — beide inzwischen
gelöscht, siehe unten) hing starr
am Agenten: eine Cron-Zeile je Agent, ausgeführt über ein `Task.Delay` bis zum
nächsten Termin. Ein jährlicher Termin bedeutet ein `Task.Delay` über Monate (B6).
Cron läuft in Lokalzeit ohne explizite Zone (B7). Ob mit oder ohne Kontext gelaufen
+6 -5
View File
@@ -1,6 +1,6 @@
# ClawdDotNet — Teststrategie
Ergänzung zur [Bestandsaufnahme](Bestandsaufnahme-2026-07.md).
Ergänzung zur [Bestandsaufnahme](archiv/Bestandsaufnahme-2026-07.md).
Ziel: Fehlerklassen wie B1 (Compaction zerstört tool_call-Paarung) und B2 (Race Condition)
strukturell unmöglich machen, statt sie im Betrieb zu entdecken.
@@ -61,9 +61,10 @@ internal sealed class FakeChatClient : IChatCompletionClient
### R2 — `TimeProvider` statt `DateTime.Now`
.NET 10 bringt `TimeProvider` mit; `FakeTimeProvider` steckt in
`Microsoft.Extensions.TimeProvider.Testing`. Betrifft
[`CronExpression.cs:53`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L53),
[`AgentScheduler.cs:75`](../src/ClawdDotNet.Core/Scheduling/AgentScheduler.cs#L75),
[`ToolJobScheduler.cs:111`](../src/ClawdDotNet.Core/Scheduling/ToolJobScheduler.cs#L111).
[`CronExpression.cs:53`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L53) und den
[`TaskScanner`](../src/ClawdDotNet.Core/Tasks/TaskScanner.cs). (Die ursprünglich hier
genannten `AgentScheduler` und `ToolJobScheduler` sind gelöscht — der Scanner hat sie
abgelöst.)
Damit werden Zeitumstellungs- und Langzeit-Tests deterministisch und laufen in Millisekunden
statt in Echtzeit.
@@ -116,7 +117,7 @@ tests/
Die drei Projekte in `ClawdDotNet.slnx` unter einem Ordner `/tests/` eintragen.
> Nebenbefund: In `ClawdDotNet.slnx` fehlen vier Tool-Projekte (AgentComm, AgentSpawn,
> AgentEditor, SocialMediaManager), obwohl sie in `ClawdDotNet.csproj` referenziert sind.
> AgentEditor, SocialMediaManager), obwohl sie in `src/ClawdDotNet.App/ClawdDotNet.App.csproj` referenziert sind.
> Sie sollten mit aufgenommen werden, sonst laufen sie in der IDE-Solution nicht mit.
---
+3 -2
View File
@@ -21,7 +21,8 @@ ClawdDotNet.sln
│ ├── ClawdDotNet.Tools.MeinTool/ ← Dein Tool-Plugin
│ │ ├── ClawdDotNet.Tools.MeinTool.csproj
│ │ └── MeinToolTool.cs
── ClawdDotNet.Host/WinForms-App (verweist auf Core + alle Tools)
── ClawdDotNet.App/ Fachschicht ohne Oberflaeche (verweist auf Core + alle Tools)
│ └── ClawdDotNet.Desktop/ ← Oberflaeche in Avalonia (verweist auf App)
```
### Neues Tool-Projekt anlegen
@@ -450,7 +451,7 @@ Das Logging-System trennt automatisch nach Modul. Wenn dein Tool den Logger aus
Logs/
├── 2026-05-12/
│ ├── Core.log ← Engine, Scheduler, Config
│ ├── Host.log ← WinForms UI
│ ├── Host.log ← Oberflaeche
│ ├── Tool_Database.log ← Database-Tool
│ ├── Tool_FileRW.log ← FileRW-Tool
│ ├── Tool_MeinTool.log ← Dein Tool!
@@ -3,6 +3,11 @@
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
@@ -14,7 +19,7 @@ deshalb umgehbar sind, (2) fehlende Nebenläufigkeits-Absicherung im geteilten C
## 1. Kritische Sicherheitslücken (P0)
### S1 — DatabaseTool: Tabellen-Whitelist ist vollständig umgehbar
[`DatabaseTool.cs:218`](../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L218)
[`DatabaseTool.cs:218`](../../src/ClawdDotNet.Tools.Database/DatabaseTool.cs#L218)
```csharp
return allowed.Any(t => t != null && inputLower.Contains(t));
@@ -31,7 +36,7 @@ DELETE FROM users -- 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))
- `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).
@@ -43,7 +48,7 @@ vordefinierte, parametrisierte Named Queries ersetzen. Freies SQL vom LLM ist gr
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
[`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}")
@@ -63,7 +68,7 @@ eine Regex validieren (`^https://(www\.)?(youtube\.com|youtu\.be)/…`) und `--`
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
[`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}";
@@ -81,7 +86,7 @@ landet der Key:
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)
[`PermissionGate.cs:7`](../../src/ClawdDotNet.Core/Security/PermissionGate.cs#L7)
```csharp
public bool IsAllowed(string agentId, string toolName, AgentConfig agentConfig)
@@ -98,13 +103,13 @@ Rate-Limits pro Agent/Tool/Zeitfenster, Audit-Log jedes Aufrufs mit Argumenten,
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)
[`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))
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,
@@ -112,7 +117,7 @@ dabei jede Zwischen-URL erneut gegen die Whitelist prüfen. Zusätzlich private
(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)
[`FileRWTool.cs:211`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L211)
```csharp
if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
@@ -128,7 +133,7 @@ ausnutzbar, aber eine Zeitbombe.
### 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)).
([`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
@@ -139,7 +144,7 @@ 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)
[`ContextCompactor.cs:140`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
```csharp
var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
@@ -161,7 +166,7 @@ einer Nachricht mit Rolle `user` oder `assistant` ohne `tool_calls`. Analog muss
`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
[`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.
@@ -185,8 +190,8 @@ Beim Umsetzen kamen zwei Folgeprobleme dazu, die denselben Ursprung haben:
(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)
[`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:
@@ -209,7 +214,7 @@ häufigste Frustquelle im laufenden Betrieb.
und beide getrennt im UI ausweisen.
### B4 — Kostenanzeige ist doppelt falsch
[`frm_main.cs:292`](../frm_main.cs#L292)
[`frm_main.cs:292`](../../frm_main.cs#L292)
```csharp
_statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2);
@@ -219,7 +224,7 @@ _statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2)
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.
([`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`).
@@ -230,22 +235,22 @@ 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
([`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,
([`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
([`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)
[`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
@@ -257,14 +262,14 @@ Job läuft ab dann einfach nie wieder.
Scheduler-Tasks mit einem `ContinueWith`-Fehler-Logger versehen.
### B7 — Cron rechnet in Lokalzeit
[`CronExpression.cs:51`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs#L51) mit
[`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)
[`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
@@ -275,14 +280,14 @@ in der Zwischenzeit brennt jeder Hop einen vollständigen LLM-Run.
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
[`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)
[`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
@@ -292,19 +297,19 @@ mit 500 Nachrichten sind das mehrere MB pro Nachricht.
`WriteIndented = false`.
### B11 — Kein `max_tokens` im Request
[`AgentEngine.cs:105`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs#L105) und Z.279 setzen
[`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
[`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)
[`ContextCompactor.cs:140`](../../src/ClawdDotNet.Core/Engine/ContextCompactor.cs#L140)
*Gefunden durch den Property-Test, nicht beim Lesen des Codes.*
@@ -341,7 +346,7 @@ verwenden dagegen das Feld `_instanceId`, das nur gesetzt wird, wenn
### 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
([`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.
@@ -370,7 +375,7 @@ Erste Kandidaten: `CronExpression`, `ContextCompactor` (Paarungs-Invarianten!),
`FileRWTool`-Pfadprüfungen.
### K4 — Kein Streaming
`request.Stream = false` fest verdrahtet ([`OpenRouterClient.cs:34`](../src/ClawdDotNet.Core/Api/OpenRouterClient.cs#L34)).
`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
@@ -405,7 +410,7 @@ Input-Tokens kosten nur ~10 % des Normalpreises (Schreiben in den Cache kostet e
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)).
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.
@@ -421,7 +426,7 @@ Sinnvolle Defaults: 4.000 Tokens pro Tool-Ergebnis, mit Kürzungshinweis und der
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
[`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.
@@ -430,7 +435,7 @@ Bei Opus kostet eine einzige Compaction so mehr als der halbe Run.
### 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
([`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
@@ -443,7 +448,7 @@ 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
([`FileRWTool.cs:26`](../../src/ClawdDotNet.Tools.FileRW/FileRWTool.cs#L26)). Bei 8 zugewiesenen
Tools sind das schnell 46k Tokens — bei **jedem** Schritt (ohne Caching).
Maßnahmen:
@@ -453,7 +458,7 @@ Maßnahmen:
- 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)
[`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.
@@ -504,7 +509,7 @@ 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)).
([`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.
@@ -523,7 +528,7 @@ Siehe K3.
## 6. Vorgeschlagene Reihenfolge
> **Abgelöst durch die [Roadmap](Roadmap.md)** (Juli 2026). Die offenen Punkte
> **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 B6B8 sind in den Roadmap-Vorhaben
> A1A4 aufgegangen.
@@ -1,6 +1,6 @@
# Deploymentcenter 2.2 2.4: Was noch zu tun ist
Stand: 2026-08-13. Ergänzt [Deploymentcenter-Integration](Deploymentcenter-Integration.md)
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 |
@@ -40,7 +40,7 @@ pack-and-deploy --config deploy/packager.config.json \
```
- Zugangsdaten in `deploy/packager.config.json` (per `.gitignore` ausgeschlossen),
Vorlage ohne Werte in [`packager.config.example.json`](../deploy/packager.config.example.json).
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`
@@ -50,7 +50,7 @@ pack-and-deploy --config deploy/packager.config.json \
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.
[Directory.Build.props](../../Directory.Build.props) stimmt mit `clawddotnet.dll` überein.
Offen: `linux-x64` (erst nach der Avalonia-Portierung) und `prod`.
@@ -63,7 +63,7 @@ Unsere Konfiguration liegt seit der Linux-Portierung in `AppPaths.ConfigDirector
(`%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/`
[ClawdDotNet.csproj](../../ClawdDotNet.csproj) legen `tools/`, `Logs/` und `Instances/`
unter `OutputPath` an, und die sind mit AppPaths ohnehin überholt.
---
@@ -72,7 +72,7 @@ unter `OutputPath` an, und die sind mit AppPaths ohnehin überholt.
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)
[`DeploymentcenterService.StartUpdate`](../../src/ClawdDotNet.App/Services/DeploymentcenterService.cs)
und `App.StartUpdateAsync`.
### Der Agent wird mitgeliefert — er muss es
@@ -82,7 +82,7 @@ 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
[`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.
@@ -126,7 +126,7 @@ 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
[`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
@@ -148,7 +148,7 @@ leeres Verzeichnis neben dem, aus dem die Anwendung liest.
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
[Integrationsbeschreibung](../Deploymentcenter-Integration.md) — jedenfalls für frisch
installierte Systeme.
Der Installer schreibt Lizenzschlüssel und Token **im Klartext**; er kennt unsere
@@ -2,7 +2,7 @@
> **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).
> [Deploymentcenter-Integration](../Deploymentcenter-Integration.md).
>
> Mit **SDK 2.1 erledigt** (waren Befunde aus Abschnitt 3 bzw. aus der Durchsicht der
> 2.0-Anbindung):
@@ -48,7 +48,7 @@ Punkte aus derselben Durchsicht — drei davon würden beim Ausrollen wehtun.
| Punkt aus dem Vorschlag | Umsetzung |
|---|---|
| Format `2:<plattform>:<hex>` | [HardwareId.cs:125](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs) |
| 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 | `:42107`, inklusive `dmi-uuid` |
| `IsPlausibleMachineId` (Länge, `uninitialized`, nur Nullen) | `:161` |
@@ -57,11 +57,11 @@ Punkte aus derselben Durchsicht — drei davon würden beim Ausrollen wehtun.
| 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` |
| 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 |
| `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` |
| 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` |
@@ -80,7 +80,7 @@ das ist jetzt reine Anschlussarbeit in ClawdDotNet, keine Konzeptarbeit mehr.
### 2.1 Es wird nichts signiert — die Lizenzprüfung ist eine Vertrauensfrage an DNS
[LicenseClient.cs:62](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs):
[LicenseClient.cs:62](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs):
```csharp
string status = root.TryGetProperty("status", out var sProp) ? sProp.GetString() ?? "unknown" : "unknown";
@@ -135,7 +135,7 @@ 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)):
Alt ([LicenseLabrador/HardwareId.cs:20](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)):
```csharp
rawBuilder.Append(machineId); // MachineGuid, sonst MAC
@@ -143,7 +143,7 @@ rawBuilder.Append(Environment.MachineName); // direkt angehängt, kein Tren
→ sha256(machineGuid + machineName)
```
Neu ([HardwareId.cs:149](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs)):
Neu ([HardwareId.cs:149](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs)):
```csharp
string raw = $"{Environment.MachineName}:{firstMac}";
@@ -174,7 +174,7 @@ 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) —
[StateStore.cs:79](../../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) —
„Legacy Migration Check":
```csharp
@@ -217,7 +217,7 @@ und ist als Cache wertlos.
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))
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.
@@ -228,7 +228,7 @@ damit niemand später Schutz vermutet, wo keiner ist.
| 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. |
| [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. |
@@ -277,7 +277,7 @@ abzugleichen.
| [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). |
| `docs/Integrationsplan-WatchDog-LicenseLabrador.md` | Abgelöst durch [Deploymentcenter-Integration](../Deploymentcenter-Integration.md). |
---
@@ -57,8 +57,8 @@ 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
([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
@@ -278,7 +278,7 @@ Rohzahlen.
# Vorgeschlagene Reihenfolge
> **Abgelöst durch die [Roadmap](Roadmap.md)** (Juli 2026). Die offenen Punkte
> **Abgelöst durch die [Roadmap](../Roadmap.md)** (Juli 2026). Die offenen Punkte
> laufen dort als C1C8 weiter; S4 + K2 sind in den Vorhaben A2/A3
> (Staging-Freigabe, Audit-Log) aufgegangen.
@@ -2,6 +2,14 @@
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?
---
@@ -526,7 +534,7 @@ LiveCharts2 ist damit **keine Portierung, sondern neue Funktionalität** — sie
gehört zum Trading-Teil, nicht zum Linux-Umzug, und ist in den 5080 PT oben
nicht enthalten. Wenn die Kursansichten kommen, sind dafür grob 510 PT
zusätzlich zu rechnen. Das passt zu dem, was in
[docs/Roadmap.md](docs/Roadmap.md) und der Notiz „Basis vor Trading härten"
[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
@@ -14,7 +14,7 @@ denselben Code betreffen — die sollten in einem Zug mit erledigt werden.
### 1.1 Der Rechnername steckt im Hash — das ist das eigentliche Problem
[HardwareId.cs:43](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs):
[HardwareId.cs:43](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs):
```csharp
rawBuilder.Append(Environment.MachineName); // "for system isolation"
@@ -31,12 +31,12 @@ 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)).
([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)
[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),
@@ -50,7 +50,7 @@ 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)
[StateStore.cs:41](../../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/StateStore.cs)
beim Lesen und `:90` beim Schreiben:
```csharp
@@ -63,17 +63,17 @@ 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)),
[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)).
([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)
[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,
@@ -255,7 +255,7 @@ hwid_source = "machine-id",
```
**Server** — in `LicenseService::validate`, an der Stelle der heutigen Suche
([LicenseService.php:100](../../LicenseLabrador/server/src/LicenseService.php)):
([LicenseService.php:100](../../../LicenseLabrador/server/src/LicenseService.php)):
```
1. Aktivierung mit hardware_id = <v2> suchen
@@ -281,7 +281,7 @@ Veröffentlichungen) aus dem Client fallen.
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))
([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.
@@ -321,7 +321,7 @@ 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)),
([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.
+41
View File
@@ -0,0 +1,41 @@
# Archiv
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.
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.
> **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.
---
## 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 S1S7, B1B14, K1K6, T1T9, 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 C1C8 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
MaiJuli 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](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 |
@@ -1,5 +1,9 @@
# Umsetzungsplan: AgentEditor härten (Personalverwaltung)
> **Stand 2026-08-23: beschlossen, noch nicht gebaut.** Einordnung siehe
> [Roadmap](../Roadmap.md) 3.2 (nach AgentInspector) — die Reihenfolge gilt dort,
> dieses Dokument ist der Bauplan.
> Stand: 2026-08-05
> Ziel: Änderungen an Identity und Soul im laufenden Betrieb bleiben möglich,
> werden aber freigabepflichtig, nachvollziehbar und rücknehmbar.
@@ -1,5 +1,8 @@
# Umsetzungsplan: AgentInspector (Supervisor-Einsicht)
> **Stand 2026-08-23: beschlossen, noch nicht gebaut.** Einordnung siehe
> [Roadmap](../Roadmap.md) 3.2 — die Reihenfolge gilt dort, dieses Dokument ist der Bauplan.
> Stand: 2026-08-05
> Ziel: Ein Supervisor-Agent kann beurteilen, ob die anderen Agenten der Instanz
> das tun, was sie tun sollen — über Audit-Log, Taskboard und lesenden Zugriff
@@ -1,5 +1,8 @@
# Umsetzungsplan: FileRW-Papierkorb + Cleanup-Job
> **Stand 2026-08-23: beschlossen, noch nicht gebaut.** Eingeordnet als Punkt 3 der
> [Roadmap](../Roadmap.md) — die Reihenfolge gilt dort, dieses Dokument ist der Bauplan.
> Stand: 2026-08-05
> Ziel: `FileRW.delete` löscht nicht mehr endgültig, sondern verschiebt in einen
> Papierkorb je Workspace. Ein Cron-Job räumt den Papierkorb nach X Tagen auf.
@@ -1,5 +1,9 @@
# Umsetzungsplan: WebSearch-Tool (Internetzugang erweitern)
> **Stand 2026-08-23: nicht beschlossen.** Steht im Ideenspeicher der
> [Roadmap](../Roadmap.md) (Abschnitt 4) und bleibt bewusst zuletzt — größter
> Sicherheitshebel. Dieses Dokument ist der Bauplan, falls entschieden wird.
> Stand: 2026-08-05
> Ziel: Agenten können das Web durchsuchen, statt nur bekannte URLs abzurufen.
> Reihenfolge: **Zuletzt.** Größter Sicherheitshebel, deshalb erst nach
@@ -20,7 +24,7 @@ Internetzugang ist bereits vorhanden, aber nur in eine Richtung:
| `WebMonitor` | strukturierte Seiten überwachen | feste Zielseiten |
Alle drei liefern die Pflichtfelder `fetchedAt` / `dataAsOf` / `source` aus
`ClawdDotNet_Prompt_InternetTools.md` und sind gegen SSRF abgesichert
`docs/archiv/ClawdDotNet_Prompt_InternetTools.md` und sind gegen SSRF abgesichert
(`UrlGuard` prüft Schema, private Netze und Whitelist, auch über Redirects
hinweg — `UrlSanitizer` analog für DirectAPI).
@@ -153,7 +157,7 @@ Ergänzend im Tool:
### Slice 3 — Betrieb
- [ ] Rechercheagent-Vorlage in `docs/InstanceSetupGuide.md` gemäß Abschnitt 4
- [ ] `ClawdDotNet_Prompt_InternetTools.md` um das Tool ergänzen
- [ ] `docs/archiv/ClawdDotNet_Prompt_InternetTools.md` um das Tool ergänzen
- [ ] `docs/ToolDevelopmentGuide.md`: Steckbrief
---
-246
View File
@@ -1,246 +0,0 @@
namespace ClawdDotNet
{
partial class frm_AddJob
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
lblJobType = new Label();
cbJobType = new ComboBox();
lblAgent = new Label();
cbAgent = new ComboBox();
lblTool = new Label();
cbTool = new ComboBox();
lblJobDef = new Label();
cbJobDef = new ComboBox();
lblCron = new Label();
txtCron = new TextBox();
lblPreview = new Label();
lblTask = new Label();
txtTask = new TextBox();
chkRunOnStart = new CheckBox();
lblHelp = new Label();
btnOk = new Button();
btnCancel = new Button();
SuspendLayout();
//
// lblJobType
//
lblJobType.AutoSize = true;
lblJobType.Location = new Point(12, 9);
lblJobType.Name = "lblJobType";
lblJobType.Size = new Size(64, 25);
lblJobType.TabIndex = 0;
lblJobType.Text = "Job-Typ:";
//
// cbJobType
//
cbJobType.DropDownStyle = ComboBoxStyle.DropDownList;
cbJobType.Items.AddRange(new object[] { "Agent Wakeup", "Tool Job" });
cbJobType.Location = new Point(130, 6);
cbJobType.Name = "cbJobType";
cbJobType.Size = new Size(326, 33);
cbJobType.TabIndex = 1;
cbJobType.SelectedIndexChanged += OnJobTypeChanged;
//
// lblAgent
//
lblAgent.AutoSize = true;
lblAgent.Location = new Point(12, 48);
lblAgent.Name = "lblAgent";
lblAgent.Size = new Size(64, 25);
lblAgent.TabIndex = 2;
lblAgent.Text = "Agent:";
//
// cbAgent
//
cbAgent.DropDownStyle = ComboBoxStyle.DropDownList;
cbAgent.Location = new Point(130, 45);
cbAgent.Name = "cbAgent";
cbAgent.Size = new Size(326, 33);
cbAgent.TabIndex = 3;
//
// lblTool
//
lblTool.AutoSize = true;
lblTool.Location = new Point(12, 87);
lblTool.Name = "lblTool";
lblTool.Size = new Size(45, 25);
lblTool.TabIndex = 4;
lblTool.Text = "Tool:";
//
// cbTool
//
cbTool.DropDownStyle = ComboBoxStyle.DropDownList;
cbTool.Location = new Point(130, 84);
cbTool.Name = "cbTool";
cbTool.Size = new Size(326, 33);
cbTool.TabIndex = 5;
cbTool.SelectedIndexChanged += OnToolChanged;
//
// lblJobDef
//
lblJobDef.AutoSize = true;
lblJobDef.Location = new Point(12, 126);
lblJobDef.Name = "lblJobDef";
lblJobDef.Size = new Size(64, 25);
lblJobDef.TabIndex = 6;
lblJobDef.Text = "Job:";
//
// cbJobDef
//
cbJobDef.DropDownStyle = ComboBoxStyle.DropDownList;
cbJobDef.Location = new Point(130, 123);
cbJobDef.Name = "cbJobDef";
cbJobDef.Size = new Size(326, 33);
cbJobDef.TabIndex = 7;
//
// lblCron
//
lblCron.AutoSize = true;
lblCron.Location = new Point(12, 168);
lblCron.Name = "lblCron";
lblCron.Size = new Size(79, 25);
lblCron.TabIndex = 8;
lblCron.Text = "Zeitplan:";
//
// txtCron
//
txtCron.Location = new Point(130, 165);
txtCron.Name = "txtCron";
txtCron.PlaceholderText = "z.B. */2 * * * * (alle 2 Min)";
txtCron.Size = new Size(326, 31);
txtCron.TabIndex = 9;
txtCron.TextChanged += OnCronChanged;
//
// lblPreview
//
lblPreview.Font = new Font("Segoe UI", 8F);
lblPreview.ForeColor = Color.Gray;
lblPreview.Location = new Point(130, 199);
lblPreview.Name = "lblPreview";
lblPreview.Size = new Size(326, 20);
lblPreview.TabIndex = 10;
//
// lblTask
//
lblTask.AutoSize = true;
lblTask.Location = new Point(12, 226);
lblTask.Name = "lblTask";
lblTask.Size = new Size(84, 25);
lblTask.TabIndex = 11;
lblTask.Text = "Aufgabe:";
//
// txtTask
//
txtTask.Location = new Point(130, 223);
txtTask.Multiline = true;
txtTask.Name = "txtTask";
txtTask.ScrollBars = ScrollBars.Vertical;
txtTask.Size = new Size(326, 80);
txtTask.TabIndex = 12;
txtTask.Text = "Führe deine zugewiesenen Aufgaben aus.";
//
// chkRunOnStart
//
chkRunOnStart.AutoSize = true;
chkRunOnStart.Location = new Point(12, 312);
chkRunOnStart.Name = "chkRunOnStart";
chkRunOnStart.Size = new Size(395, 29);
chkRunOnStart.TabIndex = 13;
chkRunOnStart.Text = "Beim Programmstart sofort einmal ausführen";
//
// lblHelp
//
lblHelp.Font = new Font("Segoe UI", 8F);
lblHelp.ForeColor = Color.DimGray;
lblHelp.Location = new Point(12, 348);
lblHelp.Name = "lblHelp";
lblHelp.Size = new Size(444, 60);
lblHelp.TabIndex = 14;
lblHelp.Text = "Cron-Format: Min Std Tag Mon Wochentag\r\nBeispiele: */5 * * * * = alle 5 Min\r\n 0 8 * * 1-5 = Mo-Fr um 08:00\r\n 0 */2 * * * = alle 2 Stunden";
//
// btnOk
//
btnOk.DialogResult = DialogResult.OK;
btnOk.Location = new Point(240, 418);
btnOk.Name = "btnOk";
btnOk.Size = new Size(106, 38);
btnOk.TabIndex = 15;
btnOk.Text = "Hinzufügen";
btnOk.Click += OnOkClick;
//
// btnCancel
//
btnCancel.DialogResult = DialogResult.Cancel;
btnCancel.Location = new Point(356, 418);
btnCancel.Name = "btnCancel";
btnCancel.Size = new Size(100, 38);
btnCancel.TabIndex = 16;
btnCancel.Text = "Abbrechen";
//
// frm_AddJob
//
AcceptButton = btnOk;
CancelButton = btnCancel;
ClientSize = new Size(470, 470);
Controls.Add(lblJobType);
Controls.Add(cbJobType);
Controls.Add(lblAgent);
Controls.Add(cbAgent);
Controls.Add(lblTool);
Controls.Add(cbTool);
Controls.Add(lblJobDef);
Controls.Add(cbJobDef);
Controls.Add(lblCron);
Controls.Add(txtCron);
Controls.Add(lblPreview);
Controls.Add(lblTask);
Controls.Add(txtTask);
Controls.Add(chkRunOnStart);
Controls.Add(lblHelp);
Controls.Add(btnOk);
Controls.Add(btnCancel);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "frm_AddJob";
StartPosition = FormStartPosition.CenterParent;
Text = "Job hinzufügen";
ResumeLayout(false);
PerformLayout();
}
#endregion
private Label lblJobType;
private ComboBox cbJobType;
private Label lblAgent;
private ComboBox cbAgent;
private Label lblTool;
private ComboBox cbTool;
private Label lblJobDef;
private ComboBox cbJobDef;
private Label lblCron;
private TextBox txtCron;
private Label lblPreview;
private Label lblTask;
private TextBox txtTask;
private CheckBox chkRunOnStart;
private Label lblHelp;
private Button btnOk;
private Button btnCancel;
}
}
-258
View File
@@ -1,258 +0,0 @@
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Scheduling;
using ClawdDotNet.Core.Tools;
namespace ClawdDotNet;
public sealed partial class frm_AddJob : Form
{
private readonly IReadOnlyList<AgentConfig> _agents;
private readonly IReadOnlyList<(IAgentTool Tool, IToolJobProvider Provider)> _jobProviders;
// ─── Output Properties ───
public bool IsToolJob => cbJobType.SelectedIndex == 1;
public string SelectedAgentId => (cbAgent.SelectedItem as AgentComboItem)?.AgentId ?? "";
public string CronExpression => txtCron.Text.Trim();
public string TaskMessage => txtTask.Text.Trim();
public bool RunOnStart => chkRunOnStart.Checked;
public string SelectedToolName => (cbTool.SelectedItem as ToolComboItem)?.ToolName ?? "";
public string SelectedJobTypeId => (cbJobDef.SelectedItem as JobDefComboItem)?.JobTypeId ?? "";
/// <summary>Konstruktor für "Hinzufügen"</summary>
public frm_AddJob(IReadOnlyList<AgentConfig> agents, ToolRegistry toolRegistry)
{
InitializeComponent();
_agents = agents;
_jobProviders = toolRegistry.GetJobProviders();
foreach (var agent in agents)
cbAgent.Items.Add(new AgentComboItem(agent.AgentId, agent.DisplayName));
if (cbAgent.Items.Count > 0) cbAgent.SelectedIndex = 0;
foreach (var (tool, _) in _jobProviders)
cbTool.Items.Add(new ToolComboItem(tool.Name, tool.Description));
if (cbTool.Items.Count > 0) cbTool.SelectedIndex = 0;
cbJobType.SelectedIndex = 0;
}
/// <summary>Konstruktor für "Bearbeiten" — Agent, Tool und Job-Typ sind gesperrt</summary>
public frm_AddJob(IReadOnlyList<AgentConfig> agents, ToolRegistry toolRegistry,
string agentId, string cron, string taskMessage, bool runOnStart,
bool isToolJob, string? toolName = null, string? jobTypeId = null)
: this(agents, toolRegistry)
{
Text = "Job bearbeiten";
btnOk.Text = "Speichern";
// Job-Typ setzen und sperren
cbJobType.SelectedIndex = isToolJob ? 1 : 0;
cbJobType.Enabled = false;
// Agent vorauswählen und sperren
for (int i = 0; i < cbAgent.Items.Count; i++)
{
if (((AgentComboItem)cbAgent.Items[i]!).AgentId == agentId)
{
cbAgent.SelectedIndex = i;
break;
}
}
cbAgent.Enabled = false;
// Bearbeitbare Felder befüllen
txtCron.Text = cron;
chkRunOnStart.Checked = runOnStart;
if (isToolJob && toolName is not null)
{
// Tool vorauswählen und sperren
for (int i = 0; i < cbTool.Items.Count; i++)
{
if (((ToolComboItem)cbTool.Items[i]!).ToolName == toolName)
{
cbTool.SelectedIndex = i;
break;
}
}
cbTool.Enabled = false;
// Job-Definition vorauswählen und sperren
if (jobTypeId is not null)
{
for (int i = 0; i < cbJobDef.Items.Count; i++)
{
if (((JobDefComboItem)cbJobDef.Items[i]!).JobTypeId == jobTypeId)
{
cbJobDef.SelectedIndex = i;
break;
}
}
}
cbJobDef.Enabled = false;
}
else
{
txtTask.Text = taskMessage;
}
}
private void OnJobTypeChanged(object? sender, EventArgs e)
{
var isToolJob = cbJobType.SelectedIndex == 1;
lblTool.Visible = isToolJob;
cbTool.Visible = isToolJob;
lblJobDef.Visible = isToolJob;
cbJobDef.Visible = isToolJob;
lblTask.Visible = !isToolJob;
txtTask.Visible = !isToolJob;
RefreshAgentList();
}
private void OnToolChanged(object? sender, EventArgs e)
{
cbJobDef.Items.Clear();
if (cbTool.SelectedItem is not ToolComboItem toolItem)
return;
var provider = _jobProviders.FirstOrDefault(p => ((IAgentTool)p.Provider).Name == toolItem.ToolName);
if (provider.Provider is null)
return;
foreach (var def in provider.Provider.GetJobDefinitions())
cbJobDef.Items.Add(new JobDefComboItem(def.JobTypeId, def.DisplayName, def.Description));
if (cbJobDef.Items.Count > 0) cbJobDef.SelectedIndex = 0;
RefreshAgentList();
}
private void RefreshAgentList()
{
var previousAgentId = (cbAgent.SelectedItem as AgentComboItem)?.AgentId;
cbAgent.Items.Clear();
var toolName = IsToolJob && cbTool.SelectedItem is ToolComboItem toolItem
? toolItem.ToolName : null;
foreach (var agent in _agents)
{
if (toolName is not null && !agent.Tools.ContainsKey(toolName))
continue;
cbAgent.Items.Add(new AgentComboItem(agent.AgentId, agent.DisplayName));
}
if (previousAgentId is not null)
{
for (int i = 0; i < cbAgent.Items.Count; i++)
{
if (((AgentComboItem)cbAgent.Items[i]!).AgentId == previousAgentId)
{
cbAgent.SelectedIndex = i;
break;
}
}
}
if (cbAgent.SelectedIndex < 0 && cbAgent.Items.Count > 0)
cbAgent.SelectedIndex = 0;
}
private void OnCronChanged(object? sender, EventArgs e)
{
var text = txtCron.Text.Trim();
if (string.IsNullOrWhiteSpace(text))
{
lblPreview.Text = "";
return;
}
try
{
var cron = Core.Scheduling.CronExpression.Parse(text);
var next = cron.GetNextOccurrence(DateTime.Now);
lblPreview.Text = next is not null
? $"Nächste Ausführung: {next:dd.MM.yyyy HH:mm}"
: "Kein nächster Zeitpunkt";
lblPreview.ForeColor = Color.Green;
}
catch
{
lblPreview.Text = "Ungültiger Cron-Ausdruck";
lblPreview.ForeColor = Color.Red;
}
}
private void OnOkClick(object? sender, EventArgs e)
{
if (cbAgent.SelectedItem is null)
{
MessageBox.Show("Bitte einen Agenten auswählen.", "Validierung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
if (string.IsNullOrWhiteSpace(txtCron.Text))
{
MessageBox.Show("Bitte einen Cron-Ausdruck eingeben.", "Validierung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
try
{
Core.Scheduling.CronExpression.Parse(txtCron.Text.Trim());
}
catch
{
MessageBox.Show("Ungültiger Cron-Ausdruck.", "Validierung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
if (IsToolJob)
{
if (cbTool.SelectedItem is null)
{
MessageBox.Show("Bitte ein Tool auswählen.", "Validierung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
if (cbJobDef.SelectedItem is null)
{
MessageBox.Show("Bitte einen Job-Typ auswählen.", "Validierung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
}
}
}
// ─── ComboBox Items ───
private sealed record AgentComboItem(string AgentId, string DisplayName)
{
public override string ToString() => DisplayName;
}
private sealed record ToolComboItem(string ToolName, string Description)
{
public override string ToString() => ToolName;
}
private sealed record JobDefComboItem(string JobTypeId, string DisplayName, string Description)
{
public override string ToString() => $"{DisplayName} ({JobTypeId})";
}
}
-120
View File
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
-157
View File
@@ -1,157 +0,0 @@
namespace ClawdDotNet
{
partial class frm_AddService
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
lblType = new Label();
cbType = new ComboBox();
lblName = new Label();
txtName = new TextBox();
lblPort = new Label();
txtPort = new TextBox();
lblDesc = new Label();
txtDescription = new TextBox();
btnOk = new Button();
btnCancel = new Button();
SuspendLayout();
//
// lblType
//
lblType.AutoSize = true;
lblType.Location = new Point(16, 18);
lblType.Name = "lblType";
lblType.Size = new Size(85, 20);
lblType.TabIndex = 0;
lblType.Text = "Service-Typ:";
//
// cbType
//
cbType.DropDownStyle = ComboBoxStyle.DropDownList;
cbType.Items.AddRange(new object[] { "StaticFileServer", "ReverseProxy", "Custom" });
cbType.Location = new Point(140, 15);
cbType.Name = "cbType";
cbType.Size = new Size(260, 28);
cbType.TabIndex = 1;
cbType.SelectedIndexChanged += OnTypeChanged;
//
// lblName
//
lblName.AutoSize = true;
lblName.Location = new Point(16, 58);
lblName.Name = "lblName";
lblName.Size = new Size(49, 20);
lblName.TabIndex = 2;
lblName.Text = "Name:";
//
// txtName
//
txtName.Location = new Point(140, 55);
txtName.Name = "txtName";
txtName.Size = new Size(260, 27);
txtName.TabIndex = 3;
//
// lblPort
//
lblPort.AutoSize = true;
lblPort.Location = new Point(16, 98);
lblPort.Name = "lblPort";
lblPort.Size = new Size(37, 20);
lblPort.TabIndex = 4;
lblPort.Text = "Port:";
//
// txtPort
//
txtPort.Location = new Point(140, 95);
txtPort.Name = "txtPort";
txtPort.Size = new Size(100, 27);
txtPort.TabIndex = 5;
txtPort.Text = "8090";
//
// lblDesc
//
lblDesc.AutoSize = true;
lblDesc.Location = new Point(16, 138);
lblDesc.Name = "lblDesc";
lblDesc.Size = new Size(101, 20);
lblDesc.TabIndex = 6;
lblDesc.Text = "Beschreibung:";
//
// txtDescription
//
txtDescription.Location = new Point(140, 135);
txtDescription.Name = "txtDescription";
txtDescription.Size = new Size(260, 27);
txtDescription.TabIndex = 7;
//
// btnOk
//
btnOk.Location = new Point(200, 185);
btnOk.Name = "btnOk";
btnOk.Size = new Size(100, 30);
btnOk.TabIndex = 8;
btnOk.Text = "Hinzufügen";
btnOk.DialogResult = DialogResult.OK;
btnOk.Click += OnOkClick;
//
// btnCancel
//
btnCancel.Location = new Point(310, 185);
btnCancel.Name = "btnCancel";
btnCancel.Size = new Size(90, 30);
btnCancel.TabIndex = 9;
btnCancel.Text = "Abbrechen";
btnCancel.DialogResult = DialogResult.Cancel;
//
// frm_AddService
//
AcceptButton = btnOk;
CancelButton = btnCancel;
ClientSize = new Size(420, 230);
Controls.Add(lblType);
Controls.Add(cbType);
Controls.Add(lblName);
Controls.Add(txtName);
Controls.Add(lblPort);
Controls.Add(txtPort);
Controls.Add(lblDesc);
Controls.Add(txtDescription);
Controls.Add(btnOk);
Controls.Add(btnCancel);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "frm_AddService";
StartPosition = FormStartPosition.CenterParent;
Text = "Service hinzufügen";
ResumeLayout(false);
PerformLayout();
}
#endregion
private Label lblType;
private ComboBox cbType;
private Label lblName;
private TextBox txtName;
private Label lblPort;
private TextBox txtPort;
private Label lblDesc;
private TextBox txtDescription;
private Button btnOk;
private Button btnCancel;
}
}
-50
View File
@@ -1,50 +0,0 @@
namespace ClawdDotNet;
public sealed partial class frm_AddService : Form
{
public string ServiceName => txtName.Text.Trim();
public string ServiceType => cbType.SelectedItem?.ToString() ?? "Custom";
public int ServicePort => int.TryParse(txtPort.Text, out var p) ? p : 0;
public string ServiceDescription => txtDescription.Text.Trim();
public frm_AddService()
{
InitializeComponent();
cbType.SelectedIndex = 0;
OnTypeChanged(this, EventArgs.Empty);
}
private void OnTypeChanged(object? sender, EventArgs e)
{
var type = cbType.SelectedItem?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(txtName.Text) || txtName.Text.StartsWith("Neuer "))
txtName.Text = $"Neuer {type}";
txtDescription.Text = type switch
{
"StaticFileServer" => "Statischer Datei-Server",
"ReverseProxy" => "Reverse-Proxy zu einem externen Service",
"Custom" => "Benutzerdefinierter Service",
_ => ""
};
}
private void OnOkClick(object? sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(txtName.Text))
{
MessageBox.Show("Bitte einen Namen eingeben.", "Validierung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
if (!int.TryParse(txtPort.Text, out var port) || port < 1 || port > 65535)
{
MessageBox.Show("Bitte einen gültigen Port (1-65535) eingeben.", "Validierung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
}
}
}
-120
View File
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
-111
View File
@@ -1,111 +0,0 @@
namespace ClawdDotNet
{
partial class frm_CreateInstance
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
lblInfo = new Label();
txtName = new TextBox();
lblPreview = new Label();
btnOk = new Button();
btnCancel = new Button();
SuspendLayout();
//
// lblInfo
//
lblInfo.Font = new Font("Segoe UI", 10F);
lblInfo.ForeColor = Color.White;
lblInfo.Location = new Point(20, 20);
lblInfo.Name = "lblInfo";
lblInfo.Size = new Size(380, 25);
lblInfo.TabIndex = 0;
lblInfo.Text = "Name der neuen Instanz:";
//
// txtName
//
txtName.Font = new Font("Segoe UI", 11F);
txtName.Location = new Point(20, 50);
txtName.Name = "txtName";
txtName.Size = new Size(390, 32);
txtName.TabIndex = 1;
txtName.TextChanged += OnTxtNameTextChanged;
//
// lblPreview
//
lblPreview.Font = new Font("Segoe UI", 9F);
lblPreview.ForeColor = Color.Gray;
lblPreview.Location = new Point(20, 90);
lblPreview.Name = "lblPreview";
lblPreview.Size = new Size(390, 25);
lblPreview.TabIndex = 2;
lblPreview.Text = "Ordner: Instance-...";
//
// btnOk
//
btnOk.BackColor = Color.FromArgb(60, 130, 60);
btnOk.FlatStyle = FlatStyle.Flat;
btnOk.ForeColor = Color.White;
btnOk.Location = new Point(220, 130);
btnOk.Name = "btnOk";
btnOk.Size = new Size(90, 35);
btnOk.TabIndex = 3;
btnOk.Text = "Erstellen";
btnOk.UseVisualStyleBackColor = false;
btnOk.DialogResult = DialogResult.OK;
//
// btnCancel
//
btnCancel.BackColor = Color.FromArgb(80, 80, 80);
btnCancel.FlatStyle = FlatStyle.Flat;
btnCancel.ForeColor = Color.White;
btnCancel.Location = new Point(320, 130);
btnCancel.Name = "btnCancel";
btnCancel.Size = new Size(90, 35);
btnCancel.TabIndex = 4;
btnCancel.Text = "Abbrechen";
btnCancel.UseVisualStyleBackColor = false;
btnCancel.DialogResult = DialogResult.Cancel;
//
// frm_CreateInstance
//
AcceptButton = btnOk;
BackColor = Color.FromArgb(45, 45, 45);
CancelButton = btnCancel;
ClientSize = new Size(430, 180);
Controls.Add(lblInfo);
Controls.Add(txtName);
Controls.Add(lblPreview);
Controls.Add(btnOk);
Controls.Add(btnCancel);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "frm_CreateInstance";
StartPosition = FormStartPosition.CenterParent;
Text = "Neue Instanz erstellen";
ResumeLayout(false);
PerformLayout();
}
#endregion
private Label lblInfo;
private TextBox txtName;
private Label lblPreview;
private Button btnOk;
private Button btnCancel;
}
}
-21
View File
@@ -1,21 +0,0 @@
using ClawdDotNet.Services;
namespace ClawdDotNet;
public sealed partial class frm_CreateInstance : Form
{
public string InstanceName => txtName.Text.Trim();
public frm_CreateInstance()
{
InitializeComponent();
}
private void OnTxtNameTextChanged(object? sender, EventArgs e)
{
var name = txtName.Text.Trim();
lblPreview.Text = string.IsNullOrWhiteSpace(name)
? "Ordner: Instance-..."
: $"Ordner: {InstanceDirectoryManager.BuildInstanceFolderName(name)}";
}
}
-120
View File
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
-117
View File
@@ -1,117 +0,0 @@
namespace ClawdDotNet
{
partial class frm_InstanceManager
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frm_InstanceManager));
toolStrip1 = new ToolStrip();
btn_startInstance = new ToolStripButton();
toolStripSeparator1 = new ToolStripSeparator();
btn_createInstance = new ToolStripButton();
dgv_instances = new DataGridView();
notifyIcon1 = new NotifyIcon(components);
toolStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgv_instances).BeginInit();
SuspendLayout();
//
// toolStrip1
//
toolStrip1.ImageScalingSize = new Size(24, 24);
toolStrip1.Items.AddRange(new ToolStripItem[] { btn_startInstance, toolStripSeparator1, btn_createInstance });
toolStrip1.Location = new Point(0, 0);
toolStrip1.Name = "toolStrip1";
toolStrip1.Size = new Size(783, 34);
toolStrip1.TabIndex = 0;
toolStrip1.Text = "toolStrip1";
//
// btn_startInstance
//
btn_startInstance.Image = Properties.Resources.server_go;
btn_startInstance.ImageTransparentColor = Color.Magenta;
btn_startInstance.Name = "btn_startInstance";
btn_startInstance.Size = new Size(146, 29);
btn_startInstance.Text = "Start Instance";
//
// toolStripSeparator1
//
toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new Size(6, 34);
//
// btn_createInstance
//
btn_createInstance.Alignment = ToolStripItemAlignment.Right;
btn_createInstance.Image = Properties.Resources.server_add;
btn_createInstance.ImageTransparentColor = Color.Magenta;
btn_createInstance.Name = "btn_createInstance";
btn_createInstance.Size = new Size(200, 29);
btn_createInstance.Text = "Create New Instance";
//
// dgv_instances
//
dgv_instances.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgv_instances.Dock = DockStyle.Bottom;
dgv_instances.Location = new Point(0, 45);
dgv_instances.Name = "dgv_instances";
dgv_instances.RowHeadersWidth = 62;
dgv_instances.Size = new Size(783, 408);
dgv_instances.TabIndex = 1;
//
// notifyIcon1
//
notifyIcon1.Text = "notifyIcon1";
notifyIcon1.Visible = true;
//
// frm_InstanceManager
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(783, 453);
Controls.Add(dgv_instances);
Controls.Add(toolStrip1);
HelpButton = true;
Icon = (Icon)resources.GetObject("$this.Icon");
Name = "frm_InstanceManager";
Text = "ClawdDotNet - Instance Manager";
toolStrip1.ResumeLayout(false);
toolStrip1.PerformLayout();
((System.ComponentModel.ISupportInitialize)dgv_instances).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
private ToolStrip toolStrip1;
private DataGridView dgv_instances;
private ToolStripButton btn_createInstance;
private ToolStripSeparator toolStripSeparator1;
private ToolStripButton btn_startInstance;
private NotifyIcon notifyIcon1;
}
}
-165
View File
@@ -1,165 +0,0 @@
using ClawdDotNet.Models;
using ClawdDotNet.Services;
namespace ClawdDotNet;
public partial class frm_InstanceManager : Form
{
private readonly InstanceDirectoryManager _dirManager;
private readonly SettingsManager _settingsManager;
private readonly BindingSource _bindingSource = new();
/// <summary>
/// Wird gesetzt, wenn der Benutzer eine Instanz zum Starten ausgewählt hat.
/// Program.cs liest diesen Wert nach DialogResult.OK aus.
/// </summary>
public string? SelectedInstancePath { get; private set; }
public frm_InstanceManager(InstanceDirectoryManager dirManager, SettingsManager settingsManager)
{
_dirManager = dirManager;
_settingsManager = settingsManager;
InitializeComponent();
SetupForm();
}
// Parameterloser Konstruktor für Designer
public frm_InstanceManager()
{
_dirManager = new InstanceDirectoryManager("./Instances");
_settingsManager = new SettingsManager();
InitializeComponent();
}
private void SetupForm()
{
Text = "ClawdDotNet - Instance Manager";
// DataGridView konfigurieren
dgv_instances.AutoGenerateColumns = false;
dgv_instances.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgv_instances.MultiSelect = false;
dgv_instances.AllowUserToAddRows = false;
dgv_instances.AllowUserToDeleteRows = false;
dgv_instances.ReadOnly = true;
dgv_instances.BackgroundColor = Color.FromArgb(45, 45, 45);
dgv_instances.DefaultCellStyle.BackColor = Color.FromArgb(55, 55, 55);
dgv_instances.DefaultCellStyle.ForeColor = Color.White;
dgv_instances.DefaultCellStyle.SelectionBackColor = Color.FromArgb(80, 120, 200);
dgv_instances.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(35, 35, 35);
dgv_instances.ColumnHeadersDefaultCellStyle.ForeColor = Color.White;
dgv_instances.EnableHeadersVisualStyles = false;
dgv_instances.Dock = DockStyle.Fill;
// Spalten
dgv_instances.Columns.AddRange(
new DataGridViewTextBoxColumn
{
DataPropertyName = "InstanceName",
HeaderText = "Instanzname",
Width = 200
},
new DataGridViewTextBoxColumn
{
DataPropertyName = "FolderName",
HeaderText = "Ordner",
Width = 200
},
new DataGridViewTextBoxColumn
{
DataPropertyName = "AgentCount",
HeaderText = "Agenten",
Width = 80
},
new DataGridViewTextBoxColumn
{
DataPropertyName = "ApiKeyStatus",
HeaderText = "API-Key",
Width = 120
}
);
dgv_instances.DataSource = _bindingSource;
dgv_instances.DoubleClick += OnInstanceDoubleClick;
// Buttons verdrahten
btn_startInstance.Click += OnStartInstanceClick;
btn_createInstance.Click += OnCreateInstanceClick;
// Daten laden
RefreshInstanceList();
}
private void RefreshInstanceList()
{
var instances = _dirManager.ListInstances();
_bindingSource.DataSource = instances;
dgv_instances.Refresh();
}
private void OnStartInstanceClick(object? sender, EventArgs e)
{
StartSelectedInstance();
}
private void OnInstanceDoubleClick(object? sender, EventArgs e)
{
StartSelectedInstance();
}
private void StartSelectedInstance()
{
var info = GetSelectedInstance();
if (info is null)
{
MessageBox.Show("Bitte wähle eine Instanz aus.", "Hinweis",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SelectedInstancePath = info.FolderPath;
DialogResult = DialogResult.OK;
Close();
}
private void OnCreateInstanceClick(object? sender, EventArgs e)
{
using var dialog = new frm_CreateInstance();
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
var instanceName = dialog.InstanceName;
if (string.IsNullOrWhiteSpace(instanceName))
{
MessageBox.Show("Bitte gib einen Instanznamen ein.", "Fehler",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
_dirManager.CreateInstance(instanceName);
RefreshInstanceList();
MessageBox.Show(
$"Instanz '{instanceName}' wurde erfolgreich erstellt.",
"Instanz erstellt", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Fehler beim Erstellen der Instanz:\n{ex.Message}",
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private InstanceInfo? GetSelectedInstance()
{
if (dgv_instances.SelectedRows.Count == 0)
return null;
return dgv_instances.SelectedRows[0].DataBoundItem as InstanceInfo;
}
}
File diff suppressed because it is too large Load Diff
-179
View File
@@ -1,179 +0,0 @@
using ClawdDotNet.Core.Backup;
namespace ClawdDotNet;
/// <summary>
/// Fragt ab, wohin wiederhergestellt wird und — falls nötig — die Passphrase.
///
/// Bewusst ein eigener Schritt statt stiller Vorbelegung: Eine Wiederherstellung
/// überschreibt Arbeitsstände, das soll niemand versehentlich auslösen.
/// </summary>
public sealed class frm_RestoreBackup : Form
{
private readonly TextBox _txtTarget = new();
private readonly TextBox _txtPassphrase = new() { UseSystemPasswordChar = true };
private readonly CheckBox _chkOverwrite = new();
public string TargetDirectory => _txtTarget.Text.Trim();
public string? Passphrase => _txtPassphrase.Text.Length > 0 ? _txtPassphrase.Text : null;
public bool Overwrite => _chkOverwrite.Checked;
public frm_RestoreBackup(BackupManifest manifest, string suggestedTarget)
{
Text = "Sicherung wiederherstellen";
FormBorderStyle = FormBorderStyle.FixedDialog;
StartPosition = FormStartPosition.CenterParent;
MinimizeBox = false;
MaximizeBox = false;
ClientSize = new Size(680, 380);
var layout = new TableLayoutPanel
{
ColumnCount = 3,
Dock = DockStyle.Fill,
Padding = new Padding(14),
RowCount = 7
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
for (var i = 0; i < 7; i++)
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
// ─── Beschreibung der Sicherung ───
var info = new Label
{
AutoSize = true,
Margin = new Padding(0, 0, 0, 12),
Text = $"Instanz: {manifest.InstanceName}\n"
+ $"Erstellt: {manifest.CreatedAt:g}\n"
+ $"Dateien: {manifest.Files.Count}\n"
+ $"Zugangsdaten: {DescribeSecrets(manifest)}"
};
layout.Controls.Add(info, 0, 0);
layout.SetColumnSpan(info, 3);
// ─── Zielordner ───
layout.Controls.Add(new Label { Anchor = AnchorStyles.Left, AutoSize = true, Text = "Zielordner:" }, 0, 1);
_txtTarget.Anchor = AnchorStyles.Left | AnchorStyles.Right;
_txtTarget.Text = suggestedTarget;
layout.Controls.Add(_txtTarget, 1, 1);
var browse = new Button { AutoSize = true, Text = "…" };
browse.Click += OnBrowse;
layout.Controls.Add(browse, 2, 1);
var targetHint = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 2, 0, 10),
MaximumSize = new Size(620, 0),
Text = "Vorgeschlagen ist ein neuer Ordner. Die laufende Instanz zu überschreiben "
+ "wird nicht empfohlen: Sie hält Dateien geöffnet und schreibt Teile davon "
+ "sofort wieder zurück."
};
layout.Controls.Add(targetHint, 1, 2);
layout.SetColumnSpan(targetHint, 2);
// ─── Passphrase ───
var needsPassphrase = manifest.HasSecrets;
var lblPass = new Label
{
Anchor = AnchorStyles.Left,
AutoSize = true,
Enabled = needsPassphrase,
Text = "Passphrase:"
};
layout.Controls.Add(lblPass, 0, 3);
_txtPassphrase.Anchor = AnchorStyles.Left | AnchorStyles.Right;
_txtPassphrase.Enabled = needsPassphrase;
layout.Controls.Add(_txtPassphrase, 1, 3);
var passHint = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 2, 0, 10),
MaximumSize = new Size(620, 0),
Text = needsPassphrase
? "Diese Sicherung enthält geschützte Zugangsdaten."
: "Diese Sicherung enthält keine Zugangsdaten — API-Schlüssel und Passwörter "
+ "müssen nach dem Wiederherstellen neu eingetragen werden."
};
layout.Controls.Add(passHint, 1, 4);
layout.SetColumnSpan(passHint, 2);
// ─── Überschreiben ───
_chkOverwrite.AutoSize = true;
_chkOverwrite.Margin = new Padding(0, 0, 0, 12);
_chkOverwrite.Text = "Vorhandene Dateien im Zielordner überschreiben";
layout.Controls.Add(_chkOverwrite, 1, 5);
layout.SetColumnSpan(_chkOverwrite, 2);
// ─── Schaltflächen ───
var buttons = new FlowLayoutPanel
{
AutoSize = true,
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft
};
var cancel = new Button { AutoSize = true, DialogResult = DialogResult.Cancel, Text = "Abbrechen" };
var ok = new Button { AutoSize = true, Text = "Wiederherstellen" };
ok.Click += OnConfirm;
buttons.Controls.Add(cancel);
buttons.Controls.Add(ok);
layout.Controls.Add(buttons, 1, 6);
layout.SetColumnSpan(buttons, 2);
Controls.Add(layout);
AcceptButton = ok;
CancelButton = cancel;
}
private static string DescribeSecrets(BackupManifest manifest) => manifest.HasSecrets
? $"{manifest.SecretCount}, mit Passphrase geschützt"
: manifest.SecretCount > 0
? $"{manifest.SecretCount} entfernt"
: "keine";
private void OnBrowse(object? sender, EventArgs e)
{
using var dialog = new FolderBrowserDialog
{
Description = "Zielordner für die Wiederherstellung",
UseDescriptionForTitle = true
};
if (dialog.ShowDialog(this) == DialogResult.OK)
_txtTarget.Text = dialog.SelectedPath;
}
private void OnConfirm(object? sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(TargetDirectory))
{
MessageBox.Show(this, "Bitte einen Zielordner angeben.",
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (_txtPassphrase.Enabled && string.IsNullOrEmpty(_txtPassphrase.Text))
{
MessageBox.Show(this,
"Diese Sicherung enthält geschützte Zugangsdaten. Ohne Passphrase lässt sie " +
"sich nicht wiederherstellen.",
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Information);
_txtPassphrase.Focus();
return;
}
DialogResult = DialogResult.OK;
Close();
}
}
-65
View File
@@ -1,65 +0,0 @@
namespace ClawdDotNet
{
partial class frm_chat
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frm_chat));
webView_chat2 = new Microsoft.Web.WebView2.WinForms.WebView2();
((System.ComponentModel.ISupportInitialize)webView_chat2).BeginInit();
SuspendLayout();
//
// webView_chat2
//
webView_chat2.AllowExternalDrop = true;
webView_chat2.CreationProperties = null;
webView_chat2.DefaultBackgroundColor = Color.White;
webView_chat2.Dock = DockStyle.Fill;
webView_chat2.Location = new Point(0, 0);
webView_chat2.Name = "webView_chat2";
webView_chat2.Size = new Size(1003, 706);
webView_chat2.TabIndex = 0;
webView_chat2.ZoomFactor = 1D;
//
// frm_chat
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1003, 706);
Controls.Add(webView_chat2);
Icon = (Icon)resources.GetObject("$this.Icon");
Name = "frm_chat";
Text = "ClawdDotNet - Chat: AgentName";
((System.ComponentModel.ISupportInitialize)webView_chat2).EndInit();
ResumeLayout(false);
}
#endregion
private Microsoft.Web.WebView2.WinForms.WebView2 webView_chat2;
}
}
-165
View File
@@ -1,165 +0,0 @@
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.UI;
using Microsoft.Extensions.Logging;
using Microsoft.Web.WebView2.Core;
namespace ClawdDotNet;
public partial class frm_chat : Form
{
private readonly AgentConfig _agentConfig;
private readonly AgentEngine _engine;
private readonly string _instanceId;
private readonly ILogger _logger;
private WebViewBridge? _bridge;
public frm_chat(AgentConfig agentConfig, AgentEngine engine,
string instanceId, ILoggerFactory loggerFactory)
{
_agentConfig = agentConfig;
_engine = engine;
_instanceId = instanceId;
_logger = loggerFactory.CreateLogger($"ClawdDotNet.UI.Chat.{agentConfig.AgentId}");
InitializeComponent();
Text = $"Chat {agentConfig.DisplayName}";
Load += async (_, _) => await InitWebViewAsync();
}
public frm_chat()
{
_agentConfig = new AgentConfig();
_engine = null!;
_instanceId = "";
_logger = LoggerFactory.Create(_ => { }).CreateLogger("Design");
InitializeComponent();
}
private async Task InitWebViewAsync()
{
try
{
await webView_chat2.EnsureCoreWebView2Async();
var uiPath = EmbeddedUiManager.GetExtractedPath();
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal", uiPath,
CoreWebView2HostResourceAccessKind.DenyCors);
_bridge = new WebViewBridge(webView_chat2, _logger);
_bridge.MessageReceived += OnBridgeMessage;
webView_chat2.CoreWebView2.Navigate(
$"https://ui.clwd.internal/chat.html?agent={_agentConfig.AgentId}");
await Task.Delay(500);
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentListUpdate,
AgentId: _agentConfig.AgentId,
Extra: new
{
agents = new[]
{
new
{
agentId = _agentConfig.AgentId,
displayName = _agentConfig.DisplayName,
model = _agentConfig.Model
}
}
}));
await LoadChatHistoryAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "frm_chat WebView2 init failed");
}
}
private async void OnBridgeMessage(BridgeMessage msg)
{
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
switch (msg.Type)
{
case BridgeTypes.UserMessage:
if (msg.Content is not null)
await HandleUserMessageAsync(msg.Content);
break;
case BridgeTypes.RunNow:
await HandleUserMessageAsync("Führe deine zugewiesenen Aufgaben aus.");
break;
case BridgeTypes.AbortRun:
_engine.AbortChat(_agentConfig.AgentId);
break;
}
}
private async Task HandleUserMessageAsync(string text)
{
if (_bridge is null) return;
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: text,
Extra: new { role = "user", timestamp = DateTime.Now }));
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatTyping,
AgentId: _agentConfig.AgentId));
_ = Task.Run(async () =>
{
try
{
var result = await _engine.ChatAsync(
_agentConfig, text, _instanceId, CancellationToken.None,
source: ChatSource.WebView);
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: result.FinalMessage ?? "[Keine Antwort]",
Extra: new { role = "assistant", timestamp = DateTime.Now }));
}
catch (Exception ex)
{
_logger.LogError(ex, "Chat failed");
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: $"[Fehler: {ex.Message}]",
Extra: new { role = "assistant", timestamp = DateTime.Now }));
}
});
}
private async Task LoadChatHistoryAsync()
{
if (_bridge is null) return;
var history = _engine.GetChatHistory(_agentConfig.AgentId);
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatHistory,
AgentId: _agentConfig.AgentId,
Extra: history.Select(e => new
{
role = e.Role,
content = e.Content,
timestamp = e.Timestamp
}).ToArray()));
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bridge?.Dispose();
base.OnFormClosed(e);
}
}
-3083
View File
File diff suppressed because it is too large Load Diff
-1171
View File
File diff suppressed because it is too large Load Diff
-1585
View File
File diff suppressed because it is too large Load Diff
-306
View File
@@ -1,306 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="splitContainer1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="splitContainer1.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 3</value>
</data>
<data name="splitContainer1.Orientation" type="System.Windows.Forms.Orientation, System.Windows.Forms">
<value>Horizontal</value>
</data>
<data name="toolStrip_agentSettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1884, 25</value>
</data>
<data name="toolStrip_agentSettings.Text" xml:space="preserve">
<value>toolStrip2</value>
</data>
<data name="dgv_agentlist.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Bottom</value>
</data>
<data name="dgv_agentlist.Location" type="System.Drawing.Point, System.Drawing">
<value>0, 26</value>
</data>
<data name="dgv_agentlist.Size" type="System.Drawing.Size, System.Drawing">
<value>1884, 310</value>
</data>
<data name="pg_agentsettings.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="pg_agentsettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1884, 547</value>
</data>
<data name="splitContainer1.Size" type="System.Drawing.Size, System.Drawing">
<value>1884, 887</value>
</data>
<assembly alias="mscorlib" name="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="splitContainer1.SplitterDistance" type="System.Int32, mscorlib">
<value>336</value>
</data>
<data name="splitContainer1.TabIndex" type="System.Int32, mscorlib">
<value>0</value>
</data>
<data name="btn_showInstanceManager.Text" xml:space="preserve">
<value>Show Instance Manager</value>
</data>
<data name="instanceToolStripMenuItem.Text" xml:space="preserve">
<value>Instances</value>
</data>
<data name="label_openRouterStatus.Text" xml:space="preserve">
<value>toolStripStatusLabel1</value>
</data>
<data name="toolStripStatusLabel1.Text" xml:space="preserve">
<value>|</value>
</data>
<data name="label_openRouterCredits.Text" xml:space="preserve">
<value>toolStripStatusLabel2</value>
</data>
<data name="statusStrip1.Location" type="System.Drawing.Point, System.Drawing">
<value>0, 992</value>
</data>
<data name="statusStrip1.Size" type="System.Drawing.Size, System.Drawing">
<value>1898, 32</value>
</data>
<data name="tabControl1.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>Top, Left</value>
</data>
<data name="webView_chat.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="webView_chat.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 3</value>
</data>
<data name="webView_chat.Size" type="System.Drawing.Size, System.Drawing">
<value>1884, 887</value>
</data>
<data name="tabPage_Chat.Size" type="System.Drawing.Size, System.Drawing">
<value>1890, 893</value>
</data>
<data name="rtb_log.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="rtb_log.Location" type="System.Drawing.Point, System.Drawing">
<value>0, 34</value>
</data>
<data name="rtb_log.Size" type="System.Drawing.Size, System.Drawing">
<value>1890, 859</value>
</data>
<data name="label_InfoLogModule.Size" type="System.Drawing.Size, System.Drawing">
<value>77, 29</value>
</data>
<data name="label_InfoLogModule.Text" xml:space="preserve">
<value>Module:</value>
</data>
<data name="cb_LogModule.Size" type="System.Drawing.Size, System.Drawing">
<value>221, 34</value>
</data>
<data name="toolStripSeparator1.Size" type="System.Drawing.Size, System.Drawing">
<value>6, 34</value>
</data>
<data name="label_InfoLogLevel.Size" type="System.Drawing.Size, System.Drawing">
<value>85, 29</value>
</data>
<data name="label_InfoLogLevel.Text" xml:space="preserve">
<value>LogLevel:</value>
</data>
<data name="cb_LogLevel.Size" type="System.Drawing.Size, System.Drawing">
<value>220, 34</value>
</data>
<data name="btn_logfolder.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAEESURBVEhL3ZKvDoJQFId5Dk0238GGr2DXbqBa3Cw6uy+g
RZozaTM4gxvZQHA6kc05/2CAetzP7bIrF0Hk3iLbx91O+L5xLloQBKQSDS/TNMkwDKnAGQYw0HVdKnAK
Ae/+kIK0wPV0JHvWI3veo4M1kR+wBjot24UQFpEW4OUAX6I2MJMc2K2GoRzrwp18HXDOHjUXp9cZFaeR
GoC0OnWpODq8zqyRxMDGvYdyRtbIx8B6f6Py2HmT/xKJDVRqjY/ytAhm/FwI1FtdKg23gjCOaITdFz8X
An3rIoiSYDL+Z2BzrDl3gMmiPwPAmrHu3IEksG6sXVkAwPlHAZVovu93VKKpfp4ISreGcqlKAwAAAABJ
RU5ErkJggg==
</value>
</data>
<data name="btn_logfolder.ImageTransparentColor" type="System.Drawing.Color, System.Drawing">
<value>Magenta</value>
</data>
<data name="btn_logfolder.Text" xml:space="preserve">
<value>Open LogFolder</value>
</data>
<data name="toolStrip3.Size" type="System.Drawing.Size, System.Drawing">
<value>1890, 34</value>
</data>
<data name="toolStrip3.Text" xml:space="preserve">
<value>toolStrip3</value>
</data>
<data name="tabPage_Log.Size" type="System.Drawing.Size, System.Drawing">
<value>1890, 893</value>
</data>
<data name="pg_appsettings.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="pg_appsettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1870, 843</value>
</data>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="tabPage_appsettings.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
</data>
<data name="tabPage_appsettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1876, 849</value>
</data>
<data name="tabPage_appsettings.Text" xml:space="preserve">
<value>General Settings</value>
</data>
<data name="pg_instancesettings.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="pg_instancesettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1870, 843</value>
</data>
<data name="tabPage_instanceettings.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
</data>
<data name="tabPage_instanceettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1876, 849</value>
</data>
<data name="tabPage_instanceettings.Text" xml:space="preserve">
<value>Instance Settings</value>
</data>
<data name="tabControl2.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="tabControl2.Size" type="System.Drawing.Size, System.Drawing">
<value>1884, 887</value>
</data>
<data name="tabPage_GeneralSettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1890, 893</value>
</data>
<data name="tabPage_AgentSettings.Size" type="System.Drawing.Size, System.Drawing">
<value>1890, 893</value>
</data>
<data name="tabControl1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Bottom</value>
</data>
<data name="tabControl1.Size" type="System.Drawing.Size, System.Drawing">
<value>1898, 931</value>
</data>
<data name="$this.Text" xml:space="preserve">
<value>ClawdDotNet - InstanceName</value>
</data>
</root>
-3104
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -67,6 +67,13 @@ public sealed class AppHost : IAsyncDisposable
public TelegramClientManager? TelegramClient { get; private init; }
public OpenRouterStatusService? Status { get; private init; }
/// <summary>
/// Die taegliche Sicherung. Immer gesetzt — ob sie etwas tut, entscheidet
/// <c>AutoBackupEnabled</c> bei jedem Durchlauf neu, damit eine Aenderung in den
/// Einstellungen ohne Neustart greift.
/// </summary>
public BackupScheduler? Backups { get; private set; }
/// <summary>
/// Anbindung ans Deploymentcenter (Heartbeat, Fehler-Stream, Bugtracker, Updates).
/// Null, wenn Adresse oder Token fehlen.
@@ -211,6 +218,16 @@ public sealed class AppHost : IAsyncDisposable
await host.ConnectTelegramAsync(callbacks, logger);
await host.StartDeploymentcenterAsync(loggerFactory, ct);
// Die Oberflaeche bietet die taegliche Sicherung an und schreibt Uhrzeit und
// Zielordner in die Einstellungen — gestartet wurde der Takt dazu bisher nicht,
// die Einstellung lief also ins Leere.
host.Backups = new BackupScheduler(
instancePath, instance.InstanceName, settings,
loggerFactory.CreateLogger("ClawdDotNet.Backup"));
host.Backups.Start();
host._shutdown.Add(host.Backups.DisposeAsync);
return new StartupResult(host, null);
}
@@ -13,6 +13,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
<!-- Kein Aufruf im Code: Microsoft.Data.Sqlite bringt das Buendel selbst mit, aber
in 2.1.11 - und darin steckt die SQLite-Fassung mit GHSA-2m69-gcr7-jv3q
(NU1903, hoch). Der direkte Verweis hebt core, provider und die native
Bibliothek gemeinsam auf 2.1.13 an; einzeln angehoben liefen die drei
auseinander. Faellt weg, sobald Microsoft.Data.Sqlite selbst nachzieht. -->
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.13" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
@@ -340,12 +340,3 @@ public abstract partial class PageViewModel(string title) : ViewModelBase
{
public string Title { get; } = title;
}
/// <summary>
/// Platzhalter, solange ein Bereich noch nicht portiert ist.
/// </summary>
public sealed partial class PlaceholderPageViewModel(string title, string note)
: PageViewModel(title)
{
public string Note { get; } = note;
}
@@ -37,19 +37,12 @@ public sealed partial class SettingsPageViewModel : PageViewModel
[ObservableProperty]
private int _statusCheckIntervalSeconds = 60;
// ─── Backup ───
[ObservableProperty]
private string _backupDirectory = "";
[ObservableProperty]
private bool _autoBackupEnabled;
[ObservableProperty]
private string _autoBackupTime = "03:00";
[ObservableProperty]
private int _backupKeepCount = 14;
// Die Sicherungs-Einstellungen (Zielordner, Uhrzeit, Anzahl) stehen bewusst
// NICHT hier, sondern allein auf der Sicherungs-Seite. Sie lagen frueher
// doppelt vor: Die Felder waren aus der Ansicht bereits verschwunden, wurden
// aber weiter beim Start gelesen und beim Speichern zurueckgeschrieben - wer
// die Uhrzeit auf der Sicherungs-Seite aenderte und danach hier speicherte,
// bekam den alten Wert zurueck.
// ─── Deploymentcenter ───
[ObservableProperty]
private string _deploymentcenterUrl = "https://dc.mhdf.de";
@@ -123,10 +116,6 @@ public sealed partial class SettingsPageViewModel : PageViewModel
LogRefreshIntervalMs = s.LogRefreshIntervalMs;
OpenRouterBaseUrl = s.OpenRouterBaseUrl;
StatusCheckIntervalSeconds = s.StatusCheckIntervalSeconds;
BackupDirectory = s.BackupDirectory;
AutoBackupEnabled = s.AutoBackupEnabled;
AutoBackupTime = s.AutoBackupTime;
BackupKeepCount = s.BackupKeepCount;
DeploymentcenterUrl = s.DeploymentcenterUrl;
DeploymentcenterToken = s.DeploymentcenterToken;
DeploymentcenterEnvironment = s.DeploymentcenterEnvironment;
@@ -155,10 +144,6 @@ public sealed partial class SettingsPageViewModel : PageViewModel
s.LogRefreshIntervalMs = LogRefreshIntervalMs;
s.OpenRouterBaseUrl = OpenRouterBaseUrl.Trim();
s.StatusCheckIntervalSeconds = StatusCheckIntervalSeconds;
s.BackupDirectory = BackupDirectory.Trim();
s.AutoBackupEnabled = AutoBackupEnabled;
s.AutoBackupTime = AutoBackupTime.Trim();
s.BackupKeepCount = BackupKeepCount;
s.DeploymentcenterUrl = DeploymentcenterUrl.Trim();
s.DeploymentcenterToken = DeploymentcenterToken.Trim();
s.DeploymentcenterEnvironment = DeploymentcenterEnvironment.Trim();
@@ -1,19 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="400"
x:Class="ClawdDotNet.Desktop.Views.PlaceholderPageView"
x:DataType="vm:PlaceholderPageViewModel">
<Border Classes="card" Margin="16" VerticalAlignment="Top">
<StackPanel Spacing="6">
<TextBlock Classes="heading" Text="{Binding Title}" />
<TextBlock Classes="caption" Text="{Binding Note}" TextWrapping="Wrap" />
<TextBlock Classes="caption" Margin="0,8,0,0"
Text="Dieser Bereich ist noch nicht portiert." />
</StackPanel>
</Border>
</UserControl>
@@ -1,8 +0,0 @@
using Avalonia.Controls;
namespace ClawdDotNet.Desktop.Views;
public partial class PlaceholderPageView : UserControl
{
public PlaceholderPageView() => InitializeComponent();
}
@@ -11,8 +11,16 @@
<PackageReference Include="Npgsql" Version="10.0.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageReference Include="MongoDB.Driver" Version="3.8.1" />
<!-- Kein Aufruf im Code, trotzdem noetig: MongoDB.Driver zieht SharpCompress in
0.30.1 herein, und diese Fassung hat eine bekannte Luecke (NU1902,
GHSA-6c8g-7p36-r338). Der direkte Verweis hebt sie an. Beim Aufraeumen am
2026-08-23 einmal entfernt und sofort wieder eingesetzt: Ohne ihn meldet
die Pruefung auf verwundbare Pakete das alte Paket zurueck.
Snappier stand daneben und ist ersatzlos entfallen: Der Treiber loest es
ohnehin auf dieselbe Fassung 1.3.1 auf, der Verweis hob also nichts an. -->
<PackageReference Include="SharpCompress" Version="0.48.0" />
<PackageReference Include="Snappier" Version="1.3.1" />
</ItemGroup>
<ItemGroup>