# 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/<yyyyMMdd_HHmmss>/<originalpfad>
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/<agentId>/<zeitstempel>/`. 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<ToolJobDefinition> 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.
