Aus Richards Ideensammlung, nach Sichtung des vorhandenen Codes: - FileRW-Papierkorb + Cleanup-Job: delete verschiebt nach .trash statt endgueltig zu loeschen, Aufraeumen ueber IToolJobProvider/ToolJobScheduler. Macht Loeschen reversibel und erlaubt damit, FileRW.delete im Staging von Approve auf Auto herunterzustufen. - AgentInspector: lesende Aufsicht ueber andere Agenten. Beleg-basiert (Audit/Receipts/Taskboard) statt datei-basiert; harte Allowlist, damit AgentSettings.json mit den API-Keys nicht in einen LLM-Kontext geraet. - AgentEditor-Haertung: Identity/Soul-Aenderungen ueber die vorhandene StagingPolicy freigabepflichtig machen, Selbstbearbeitung sperren, Audit + restore ergaenzen. Das Tool selbst existiert bereits. - WebSearch: Suche als eigenes Tool, Lesen bleibt bei WebFetch hinter der Domain-Whitelist. Kein agent-reach (Klartext-Cookies, Fremdprozess). Trennung Rechercheagent / handelnder Agent gegen Prompt Injection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
233 lines
8.9 KiB
Markdown
233 lines
8.9 KiB
Markdown
# Umsetzungsplan: FileRW-Papierkorb + Cleanup-Job
|
|
|
|
> 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.
|