# 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. > Reihenfolge: **Zuerst umsetzen** — kleinster Eingriff, entschärft ein reales > Risiko und ist Voraussetzung dafür, `FileRW.delete` im Staging von > `Approve` auf `Auto` herunterzustufen (siehe Abschnitt 6). --- ## 1. Ausgangslage `HandleDeleteAsync` in `src/ClawdDotNet.Tools.FileRW/FileRWTool.cs:318` löscht sofort und endgültig: ```csharp if (File.Exists(path)) { File.Delete(path); ... } else if (Directory.Exists(path)) { Directory.Delete(path, true); ... } ``` Ein einziger Tool-Call mit `path: "."` räumt damit den kompletten Workspace eines Agenten ab — ohne Rückweg. Gleichzeitig fehlt dem Agenten jede Möglichkeit, gefahrlos aufzuräumen: Jede Aufräumaktion ist irreversibel. Vorhandene Schutzmechanismen, die erhalten bleiben müssen: | Mechanismus | Ort | Verhalten | |---|---|---| | Workspace-Einsperrung | `WorkspacePath.Resolve` | absolute Pfade, `..`, ADS (`:`) verboten | | Zugriffsstufen | `IsActionAllowed` | `delete` im Shared nur bei `sharedAccessLevel = Admin` | | Geschützte Pfade | `IsPathProtected` | `protectedPaths` sind append-only, kein Löschen | | Datei-Locks | `GetFileLock` | pro Pfad ein `SemaphoreSlim` | --- ## 2. Zielverhalten ``` delete → verschiebt nach .trash// list_trash → listet Papierkorb-Einträge mit Originalpfad und Löschzeitpunkt restore → holt einen Eintrag an seinen Originalpfad zurück purge → löscht endgültig (nur Admin, ausdrücklicher Opt-in) ``` Papierkorb-Layout (im jeweiligen Workspace-Root): ``` .trash/ ├── 20260805_142233/ │ ├── _meta.json ← Originalpfad, Zeitpunkt, Agent, Typ │ └── notizen.md ← der gelöschte Inhalt (Datei oder Verzeichnis) └── 20260805_151002/ ├── _meta.json └── alte-recherche/ ``` `_meta.json`: ```json { "originalPath": "recherche/notizen.md", "workspace": "personal", "deletedAt": "2026-08-05T14:22:33Z", "deletedBy": "senior_developer", "type": "file" } ``` Der Zeitstempel ist der Ordnername — damit braucht der Cleanup-Job keine Metadatei zu lesen, um das Alter zu bestimmen (`_meta.json` ist Komfort, keine Voraussetzung). Bei Kollision wird `_1`, `_2` … angehängt, analog `HandleStockAddAsync`. --- ## 3. Sicherheitsanforderungen Diese Punkte sind **nicht optional** — ohne sie öffnet der Papierkorb neue Lücken: 1. **`.trash` ist für alle direkten Aktionen gesperrt.** `read`, `write`, `append`, `delete`, `copy` mit einem Pfad in `.trash/` werden abgelehnt. Sonst wäre der Papierkorb ein Ablageort, über den die Endungsprüfung (`personalAllowedExtensions`) umgangen werden kann: eine `.exe` „löschen" und aus dem Papierkorb an beliebiger Stelle wieder herausholen. Zugriff ausschließlich über `list_trash` / `restore` / `purge`. 2. **`list` blendet `.trash` aus.** Sonst verschmutzt der Papierkorb jede Verzeichnisauflistung und damit den Kontext des Agenten. 3. **Geschützte Pfade bleiben unantastbar.** Die bestehende Prüfung in `HandleDeleteAsync` greift **vor** dem Verschieben — ein `protectedPath` wandert auch nicht in den Papierkorb. 4. **`restore` prüft das Ziel erneut vollständig**: `WorkspacePath.Resolve` auf den Originalpfad, Endungsprüfung, `IsPathProtected`, `IsActionAllowed`. Der Originalpfad aus `_meta.json` ist **Eingabedatum, keine Wahrheit** — eine manipulierte Metadatei darf keinen Ausbruch ermöglichen. 5. **Shared Workspace: ein Papierkorb je Agent** — `.trash///`. Sonst sieht und restauriert Agent A die gelöschten Dateien von Agent B, und `purge` eines Agenten trifft alle. Im Personal Workspace entfällt die Ebene (dort ist nur ein Agent). 6. **`purge` nur bei `sharedAccessLevel = Admin`** (Shared) bzw. explizit im Personal Workspace. Der Regelweg zum endgültigen Löschen ist der Cleanup-Job, nicht der Agent. 7. **Kein Verschieben über Laufwerksgrenzen annehmen.** `Directory.Move` scheitert, wenn Workspace und `.trash` auf verschiedenen Volumes lägen — das ist per Konstruktion nicht der Fall (`.trash` liegt im Workspace-Root), aber der Fehlerfall wird sauber gemeldet statt zu einer Teilkopie zu führen. --- ## 4. Cleanup-Job `FileRWTool` implementiert zusätzlich `IToolJobProvider` (`src/ClawdDotNet.Core/Tools/IToolJobProvider.cs`). Die Infrastruktur ist vollständig vorhanden — `ToolJobScheduler` kennt Cron, `RunOnStart` und manuelles Auslösen; es braucht **keinen neuen Scheduler**. ```csharp public IReadOnlyList GetJobDefinitions() => [ new("filerw_trash_cleanup", "Papierkorb aufräumen", "Löscht Papierkorb-Einträge, die älter als retentionDays sind") ]; ``` `ExecuteJobAsync` bekommt `workspacePath` und `agentId` bereits übergeben (siehe Signatur des Interfaces). Der Job: 1. liest `retentionDays` aus `toolConfig` (Default **14**, Minimum 1), 2. durchläuft `.trash/*` in Personal- **und** Shared-Workspace, 3. löscht Ordner, deren Zeitstempel älter als die Aufbewahrungsfrist ist, 4. gibt **immer** `ToolJobResult.NoAction(logSummary)` zurück — der Agent wird nie geweckt. Aufräumen ist kein Ereignis, das ein LLM-Run wert wäre (und kein Token kosten soll). Der Shared-Papierkorb wird nur bereinigt, wenn der Job-Agent dort Admin-Rechte hat; andernfalls nur der eigene Unterordner. Damit räumt nicht jeder Agent bei jedem Tick fremde Einträge ab. Beispielkonfiguration im Agenten: ```json "toolJobs": [ { "jobId": "trash-daily", "toolName": "FileRW", "jobTypeId": "filerw_trash_cleanup", "cron": "0 3 * * *", "enabled": true, "runOnStart": false } ] ``` **Achtung Zeitzone:** `ToolJobScheduler.RunToolJobAsync` rechnet mit `DateTime.Now` (lokal), die Papierkorb-Zeitstempel oben sind UTC. Das Altersvergleich im Job daher konsequent in UTC durchführen (`DateTime.UtcNow`), nicht mischen. Dieselbe Falle ist bereits aus der Watchdog-Integration bekannt. --- ## 5. Umsetzungsschritte ### Slice 1 — Papierkorb im Tool (Kern) - [ ] `TrashPath`-Helfer analog `WorkspacePath`: baut und validiert `.trash`-Pfade, kapselt die Agent-Ebene im Shared Workspace - [ ] `HandleDeleteAsync` auf Verschieben umstellen (Datei **und** Verzeichnis) - [ ] `.trash`-Sperre in `GetAndValidatePath` (greift für alle direkten Aktionen) - [ ] `.trash` aus `HandleListAsync` filtern - [ ] `_meta.json` schreiben (über `AtomicFile`, wie beim Stock-Index) - [ ] Tests: `tests/ClawdDotNet.Tools.Tests/FileRW/TrashTests.cs` ### Slice 2 — list_trash / restore / purge - [ ] Drei Aktionen im `InputSchema` und im `switch` ergänzen - [ ] `restore` mit vollständiger Zielprüfung (siehe 3.4) - [ ] `purge` mit Admin-Prüfung - [ ] `Description` des Tools ergänzen, damit das LLM den Papierkorb kennt - [ ] Tests: Restore an geschützten Pfad, Restore mit manipulierter `_meta.json`, Restore mit inzwischen belegtem Zielpfad ### Slice 3 — Cleanup-Job - [ ] `IToolJobProvider` an `FileRWTool` - [ ] `retentionDays` in `FileRWToolSettings` (`Models/ToolSettingsViewModels.cs:68`) + Designer-Property - [ ] Job-Definition in der UI auswählbar (läuft über die bestehende Job-Verwaltung, kein neuer Dialog) - [ ] Tests mit `FakeTimeProvider` (`tests/ClawdDotNet.Core.Tests/Infrastructure/FakeTimeProvider.cs`) ### Slice 4 — Dokumentation - [ ] `docs/ToolDevelopmentGuide.md`: FileRW-Abschnitt (Zeile ~221) auf die neuen Aktionen aktualisieren - [ ] Prompt-Hinweis für Agenten: „Löschen ist reversibel, der Papierkorb wird nach X Tagen geleert" — sonst bleibt der Agent unnötig vorsichtig --- ## 6. Folgeentscheidung: Staging herunterstufen `StagingPolicy.DefaultRules` (`src/ClawdDotNet.Core/Staging/StagingPolicy.cs`) führt `FileRW.delete` heute als `Approve` — jede Aufräumaktion braucht eine menschliche Freigabe. Das ist richtig, **solange Löschen endgültig ist**. Mit dem Papierkorb ist es das nicht mehr. Empfehlung nach Slice 2: ```csharp ["FileRW.delete"] = StagingDecision.Auto, // reversibel über .trash ["FileRW.purge"] = StagingDecision.Approve // endgültig → Freigabe ``` Damit kann der Agent selbstständig aufräumen (genau der Wunsch aus der Ideensammlung), ohne dass irreversible Aktionen ungefragt durchgehen. --- ## 7. Abnahmekriterien - Ein gelöschter Ordner liegt vollständig im Papierkorb und ist per `restore` wiederherstellbar. - `read`/`write` auf einen `.trash`-Pfad wird abgelehnt. - `restore` einer `.exe` in einen Workspace mit `allowedExtensions` ohne `.exe` wird abgelehnt. - Ein `protectedPath` lässt sich weiterhin nicht löschen. - Agent B sieht im Shared-Papierkorb nicht die Einträge von Agent A. - Nach Ablauf von `retentionDays` ist der Eintrag beim nächsten Job-Tick weg, ohne dass ein LLM-Run stattgefunden hat.