disclaw/docs/development-plan.md
Nick Tabeling 69e0b7d727 initial
2026-04-08 14:21:19 +02:00

647 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DisClaw — Entwicklungsplan
Autor: Senior Developer
Datum: 2026-04-08
Basis: `ARCHITECTURE.md`, `CLAUDE.md`, `docs/ai-engineer-analyse.md`, `docs/cli-feature-answers.md`, aktueller Stand unter `src/`.
Der Plan ist umsetzungsnah. Er legt fest, was gebaut wird, wie, in welcher Reihenfolge, und wo der Code erweiterbar bleiben muss. Zeitangaben gibt es bewusst nicht — nur logische Reihenfolge und Abhängigkeiten.
## Revision-Log
- 2026-04-08: Initialversion.
- 2026-04-08: AI-Engineer-Antworten eingearbeitet (siehe `docs/cli-feature-answers.md`). Workspace-Location auf `~/.disclaw/workspaces/` (außerhalb DisClaw-Repo) geändert, Windows-Spawn-Fix via `cross-spawn` ergänzt, Phase 2 (`--resume`) freigegeben, `.claude/settings.json`-Schema und Hooks-Layout präzisiert, Abschnitt 6 durch Verweis auf Antworten-Dokument ersetzt.
---
## 1. Status Quo
### Was existiert und funktioniert (vermutlich)
- `src/index.ts` — Bootstrap, lädt Env und Config, initialisiert DB, startet Bot, Graceful Shutdown.
- `src/config/loader.ts` — Lädt `.env` und `disclaw.yaml`.
- `src/db/database.ts``better-sqlite3`-Wrapper, `workspaces` und `conversations` Tabellen mit Prepared Statements, Migrationen via `IF NOT EXISTS`, WAL und Foreign Keys aktiv.
- `src/db/schema.sql` — SQL-Schema (separat vorhanden, aber Migration läuft aktuell aus dem Inline-String in `database.ts`, Duplikation).
- `src/bot.ts` — Discord Client mit Intents `Guilds`, `GuildMessages`, `MessageContent`. Auto-Erstellung/Adoption des Management-Channels. Slash-Command-Registrierung pro Guild. Basic `messageCreate` Dispatch.
- `src/router.ts` — Channel→Workspace Lookup, naives 2000-Zeichen-Splitting an Line-Breaks, Typing-Indikator alle 5 s, schreibt User- und Assistant-Messages in `conversations`.
- `src/commands/new-agent.ts` — Slash-Command `/new-agent` mit Name-Validierung (Regex), Channel-Create, Workspace-Erstellung, DB-Insert.
- `src/agent/identity.ts``agent.yaml`, `CLAUDE.md` und `.claude/settings.json` Templates werden beim Create geschrieben.
- `src/agent/runner.ts` — Spawnt `claude -p "<prompt>" --output-format json`, baut History als Freitext, parst JSON-Output in drei Formaten, Timeout, AbortController-Support.
### Was kaputt oder problematisch ist
1. **P0 Security**: `src/agent/runner.ts:187` setzt `shell: true` — direkter Command-Injection-Vektor über Discord-User-Input (siehe AI-Engineer-Analyse §4.1).
2. **P0 Secret Leak**: `src/agent/runner.ts:181` vererbt `process.env` vollständig an den Kindprozess → `DISCORD_BOT_TOKEN` und andere Secrets sind im Claude-Prozess sichtbar (§4.5).
3. **Kein Queueing pro Channel**: Mehrere gleichzeitige Nachrichten im selben Channel starten parallele `claude`-Prozesse im gleichen `cwd`. Race Conditions bei Tool-Calls und Session-Updates sind garantiert (§2 "Per-Channel-FIFO-Queue").
4. **Agent-Name-Regex unzureichend gegen Path Traversal**: Kein expliziter `path.resolve`-Containment-Check in `new-agent.ts`. Regex akzeptiert `a-a` aber kein explizites Path-Check-Gate existiert (§4.3).
5. **Message-Splitting zerreißt Codeblöcke**: `router.ts:splitMessage()` respektiert keine ``` ``` ``` Fences (§2).
6. **History-Injektion statt `--resume`**: `runner.ts:buildPrompt()` konkateniert Historie als Freitext — kein Prompt-Caching, falsche Rollenzuordnung (§1).
7. **`.claude/settings.json` Template ist zu permissiv**: `allow: ["Read", "Write", "Edit", "Bash", ...]` ohne Pfad-Restriktionen und ohne `deny`-Liste. Kein Hook-Guard.
8. **`CLAUDE.md` Template hat keine Prompt-Injection-Resistenz-Klausel**.
9. **Keine User-Rate-Limits, kein globales Concurrency-Cap**.
10. **Keine Tests**. Kein `devDependencies`-Eintrag für ein Test-Framework.
11. **Keine strukturierten Logs**. Nur `console.log`. Schwierig zu filtern/parsen.
12. **Keine `.env.example`** im Repo (zu verifizieren bei `ls`).
13. **Doppelte Schema-Quelle**: `src/db/schema.sql` und Inline-`SCHEMA_SQL` in `database.ts` können divergieren.
14. **Typing-Refresh auf 5 s**: Discord-Typing läuft ~10 s, 5 s ist okay aber unnötig häufig; 8 s reicht (§2).
### Was komplett fehlt
- `/list-agents`, `/delete-agent` Commands.
- Session-ID-Persistenz (`--resume`).
- Streaming-Output.
- Attachment-Handling.
- PreToolUse-Hook / Audit-Log.
- Profile-basiertes Workspace-Scaffolding (researcher/developer/writer/ops).
- Per-Channel-Queue, globaler Concurrency-Semaphore.
- Structured Logger (pino o. ä.).
- Zod-Validation für Config und CLI-JSON-Output.
- Tests (Unit und Integration).
- `docs/cli-feature-probe.md` — Test-Script, das das tatsächliche Verhalten der installierten `claude` CLI gegen unsere Annahmen verifiziert.
---
## 2. Technische Entscheidungen (locked in)
### 2.1 Context-Persistenz: Hybrid (`--resume` primär, DB-Rehydrierung Fallback)
**Was**: Session-ID pro Workspace in der DB persistieren. Runner ruft `claude -p "<msg>" --resume <session-id> --output-format json --bare` auf. Wenn keine Session-ID existiert oder `--resume` fehlschlägt, wird aus den letzten N DB-Einträgen ein Bootstrap-Prompt gebaut und eine neue Session gestartet. Die neue `session_id` aus dem JSON-Output wird **nach jedem Run** in der DB aktualisiert (defensiv gegen Fork-Verhalten älterer CLI-Versionen).
**Warum**: Prompt Caching von Anthropic greift nur bei stabilen Präfixen innerhalb einer Session. Neue Session pro Nachricht = 0 % Cache-Hit, jede CLAUDE.md wird neu tokenisiert. Mit `--resume` erwartete Einsparung 6090 % Input-Tokens (siehe ai-engineer-analyse.md §1). Bestätigt über `cli-feature-answers.md` Frage 1+2: `-p` + `--resume` ist offiziell dokumentiert, `session_id` liegt **top-level** im JSON-Output.
**Wie**:
- Neue DB-Spalten in `workspaces`: `claude_session_id TEXT`, `session_updated_at TEXT`. Migration additiv (`ALTER TABLE ... ADD COLUMN`).
- Runner-API bekommt einen `SessionStore`-Typ injiziert (damit der Runner testbar bleibt).
- Session-ID lesen: `JSON.parse(stdout).session_id` (top-level). Nach **jedem** Run per `store.set(ws.id, json.session_id)` persistieren, auch wenn die ID identisch zur vorigen ist — fängt Fork-Verhalten (`--fork-session`, CLI-Upgrades, `cleanupPeriodDays`-Expiry) transparent auf.
- Fallback bei `--resume` Fehler: Error-String auf `session .* not found` oder ähnlich matchen → `claude_session_id = NULL` setzen und erneut starten ohne `--resume` (optional mit `--continue` als zweiter Fallback-Stufe).
- Die `conversations`-Tabelle bleibt Source of Truth für Rehydrierung nach Bot-Neustart oder vollständigem Session-Verlust.
- Metriken: `usage`-Block aus JSON-Output parsen (`cache_read_input_tokens`, `cache_creation_input_tokens`, `input_tokens`, `output_tokens`) und pro Run loggen. Exakter Pfad (`result.usage.*` oder top-level `usage.*`) wird empirisch via Probe-Script verifiziert.
```ts
// src/agent/runner.ts (Skizze)
export interface SessionStore {
get(workspaceId: number): string | null;
set(workspaceId: number, sessionId: string): void;
clear(workspaceId: number): void;
}
async function runOnce(ws: Workspace, msg: string, store: SessionStore): Promise<RunResult> {
const sid = store.get(ws.id);
const args = [
"-p", msg,
"--output-format", "json",
"--bare",
"--append-system-prompt-file", path.join(ws.workspace_path, "CLAUDE.md"),
];
if (sid) args.push("--resume", sid);
const res = await spawnClaude(args, ws.workspace_path);
if (res.ok) {
// Immer updaten — fängt Fork/Cleanup-Edge-Cases ab
if (res.sessionId) store.set(ws.id, res.sessionId);
return res;
}
if (sid && isResumeFailure(res.error)) {
store.clear(ws.id);
return runOnce(ws, msg, store); // einmaliger Retry ohne --resume
}
throw res.error;
}
```
**Status**: Freigegeben. `cli-feature-answers.md` Frage 1+2 bestätigt, dass `-p "..." --resume <id> --output-format json` offiziell dokumentiert ist und `session_id` als Top-Level-Feld im JSON-Output liegt. Kein Blocker mehr. Das Probe-Script verifiziert lediglich noch die genauen Pfade des `usage`-Blocks und bleibt als Regression-Check im Repo.
### 2.2 Prozessmodell: Spawn pro Nachricht (beibehalten)
**Was**: Ein Bot-Prozess, ein `claude`-Kindprozess pro Nachricht, terminiert nach Antwort.
**Warum**: Persistente Prozesse sind im MVP nicht vertretbar (150300 MB RSS pro idle Agent, Supervisor nötig, Windows-Signal-Semantik fragil, interaktiver Modus ist TTY-first). Spawn-pro-Nachricht ist einfacher, isolierter, crash-resistenter. Der Kaltstart-Overhead wird durch `--resume` + Prompt Caching teilweise kompensiert.
**Wie**: Bestehender `runner.ts`-Ansatz, aber mit `shell: false`, sanitizedEnv, Windows-Pfad-Auflösung, SessionStore, und abstrahiertem Event-Stream-Return-Type, damit Phase 5 (Streaming) ohne API-Bruch nachgerüstet werden kann.
**Windows-Spawn-Entscheidung (locked in)**: `cross-spawn` als Dependency. Grund: `claude` wird auf Windows als `claude.cmd`-Wrapper installiert (`%APPDATA%\npm\claude.cmd`), nicht als `.exe`. Seit Node 20.12 (CVE-2024-27980) blockt Node `child_process.spawn("foo.cmd", args, { shell: false })` bzw. verlangt explizites Arg-Quoting — `spawn("claude", args)` scheitert außerdem, weil Node ohne `shell: true` keine `.cmd`-Extension im PATH-Lookup anhängt. `cross-spawn` abstrahiert beides (löst `.cmd`-Wrapper auf, quoted Args korrekt, identisches Verhalten Linux/Mac/Windows).
**Trade-off**: Eine Mini-Dependency (~30 kB, keine transitiven Deps) gegen ~50 Zeilen Windows-Workaround-Code (where-lookup + `.cmd`-Wrapper-Parsing + direkter `node cli.js`-Spawn). Die Eigenbau-Variante bleibt als Fallback dokumentiert, falls `cross-spawn` jemals ausfällt, aber ist für den MVP nicht die erste Wahl — der Pragmatik-Gewinn wiegt die Dependency auf.
**Zusätzlich**: Beim Bot-Start einmalig `CLAUDE_PATH` aus `.env` lesen; falls leer, `where claude` (Windows) / `which claude` (Unix) automatisch ausführen, Ergebnis in-memory cachen. Fehlermeldung mit klarem Hinweis auf `CLAUDE_PATH`, falls nicht auffindbar. `windowsHide: true`, `stdio: ["pipe", "pipe", "pipe"]`, `child.stdout.setEncoding("utf8")`.
### 2.3 Per-Channel-Queue
**Was**: FIFO-Queue pro `channel_id`, die alle Runs im gleichen Channel serialisiert. Parallele Channels bleiben parallel.
**Warum**: Siehe §1.3 Status Quo — ohne Queue laufen race-anfällige parallele Runs im selben Workspace.
**Wie**: In-Memory `Map<channelId, Promise<void>>` im Router. Neue Tasks werden an die letzte Promise der Queue angehängt. Kein externer Queue-Service, kein Redis — MVP-tauglich. Zusätzlich: globaler Semaphore (max. 4 parallele `claude`-Prozesse) über ein simples Counting-Primitive.
```ts
// src/runtime/channel-queue.ts
export class ChannelQueue {
private queues = new Map<string, Promise<unknown>>();
enqueue<T>(channelId: string, task: () => Promise<T>): Promise<T> {
const prev = this.queues.get(channelId) ?? Promise.resolve();
const next = prev.catch(() => undefined).then(task);
this.queues.set(channelId, next);
next.finally(() => {
if (this.queues.get(channelId) === next) this.queues.delete(channelId);
});
return next;
}
}
```
### 2.4 Message-Splitting: Eigenbau, codeblock-aware
**Was**: Eigene Funktion `splitForDiscord(text, limit=1900)`, die Fences (``` ```) erkennt, offene Blöcke beim Split schließt und im nächsten Chunk wieder öffnet. Bei Ausgaben >8 k Zeichen: stattdessen als `.md`-Attachment senden.
**Warum**: Keine externe Library erfüllt das 1:1 (die üblichen "split-to-discord"-Pakete sind unmaintained oder fehlerhaft bei Fences). Eigenbau ist ~60 Zeilen und testbar. Limit 1900 statt 2000 lässt Puffer für Prefix `(1/3)`.
**Wie**: Siehe Algorithmus-Skizze in `docs/ai-engineer-analyse.md` §2 "2000-Zeichen-Splitting". Ablegen unter `src/discord/split.ts` mit Unit-Tests.
### 2.5 Workspace-Isolation: Permissions + PreToolUse-Hook
**Was**: `.claude/settings.json` bekommt ein Default-Deny für Pfade außerhalb `./` und für gefährliche Bash-Muster. Zusätzlich PreToolUse-Hook, der jeden Tool-Call mit absolutem Pfad gegen `workspace_abs_path` matched und abbricht, wenn außerhalb.
**Warum**: Permissions allein reichen nicht, weil `Read(./**)` sich mit `cd ..` via Bash aushebeln lässt. Der Hook ist die zweite Verteidigungslinie und gleichzeitig der Audit-Log-Hook.
**Wie**:
- `src/agent/templates/settings.default.json` als Template mit Platzhalter `{{workspace_abs}}`.
- Schema-Struktur wie in `cli-feature-answers.md` Frage 3 bestätigt: Permissions unter `permissions.allow` / `permissions.ask` / `permissions.deny`, mit zusätzlich `permissions.defaultMode` und `permissions.additionalDirectories`. Hooks als dreistufige Struktur: `hooks.<EventName>[].matcher` + `hooks.<EventName>[].hooks[].{type, command, if, timeout}`.
- Hook-Script als Node-File: `src/agent/hooks/guard-tool.ts`, das beim Workspace-Create nach `./.claude/hooks/guard-tool.cjs` gerendert wird (compile-to-CJS damit der Hook ohne Build-Step ausführbar ist).
- Template-Beispiel (das tatsächliche Schema):
```json
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": ["Read", "Write(./**)", "Edit(./**)", "Bash(git diff *)"],
"ask": ["Bash(git push *)"],
"deny": [
"Read(../**)", "Write(../**)", "Edit(../**)",
"Read(./.env)", "WebFetch",
"Bash(rm -rf *)", "Bash(curl * | sh)",
"Bash(ssh *)", "Bash(scp *)",
"Bash(* /etc/*)", "Bash(* ~/.ssh/*)",
"Bash(powershell -Command *)"
],
"defaultMode": "acceptEdits"
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Read|Write|Edit",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-tool.cjs\"",
"timeout": 10
}
]
}
]
},
"cleanupPeriodDays": 90
}
```
- **Headless-Gotcha**: Im `-p`-Modus bricht ein nicht durch `permissions.allow` gedecktes Tool den Run ab (kein interaktiver Prompt). Das `allow`-Set muss die für das Profil nötigen Tools explizit enthalten — dokumentieren im README und in jedem Profile-Template.
### 2.6 Secrets/Env-Handling
**Was**: `sanitizedEnv()` Funktion, die Secret-artige Variablen entfernt, bevor der `claude`-Prozess gestartet wird. Whitelist statt Blacklist wo möglich.
**Warum**: Siehe §4.5 der Analyse — sonst kann jeder Agent per `echo $DISCORD_BOT_TOKEN` den Bot-Token exfiltrieren.
**Wie**:
- Neue Datei: `src/runtime/env.ts`, exportiert `sanitizedEnv(extra?: Record<string, string>)`.
- Entfernt: `DISCORD_BOT_TOKEN`, `DISCORD_CLIENT_SECRET`, `DISCORD_PUBLIC_KEY`, alles matching `/^(DISCLAW_SECRET_|SECRET_|TOKEN_|API_KEY_?)/i`, plus eine konfigurierbare Block-Liste in `disclaw.yaml`.
- Fügt hinzu: `CI=true`, `DISCLAW_AGENT=1`, `DISCLAW_AGENT_NAME=<name>`.
### 2.9 Workspace-Location: Standardmäßig außerhalb des DisClaw-Repos
**Was**: Workspaces liegen per Default unter `~/.disclaw/workspaces/<agent>/` (Linux/Mac) bzw. `%USERPROFILE%\.disclaw\workspaces\<agent>\` (Windows). Der Pfad ist über `disclaw.yaml``workspaces_root` konfigurierbar, der Default ist aber **immer absolut außerhalb des DisClaw-Repo-Roots**. Das bisherige `./workspaces/` im Repo wird nicht mehr als Default verwendet (nur noch als Opt-in für Development/Tests — dann mit Warning-Log beim Bot-Start).
**Warum**: Siehe `cli-feature-answers.md` Frage 8. Claude Code walkt beim Start parent directories hoch und **konkateniert** alle gefundenen `CLAUDE.md`-Dateien in den System-Prompt. Liegt der Agent-Workspace unter `C:\Code\side\disclaw\workspaces\<agent>\`, dann sieht Claude zwangsläufig auch:
1. `C:\Code\side\disclaw\CLAUDE.md` (DisClaw-Projekt-Identität)
2. `~/.claude/CLAUDE.md` (User-Scope, developer-spezifisch)
3. ggf. weitere Parent-Scope-Dateien.
Das kontaminiert die Agent-Identität: Der Agent "weiß" plötzlich, dass er Teil von DisClaw ist, und erbt den Ton/Anweisungen der Developer-eigenen User-CLAUDE.md. Weder reproduzierbar noch sicher.
**Zwei Verteidigungslinien, kombiniert**:
1. **Strukturell**: Workspaces liegen außerhalb des DisClaw-Repos. Dadurch existiert oberhalb keine Projekt-CLAUDE.md. `~/.claude/CLAUDE.md` bleibt ein Rest-Leak-Vektor.
2. **Explizit**: Alle CLI-Calls nutzen `--bare` (skippt CLAUDE.md-Walk-Up, Hooks-Discovery, Skills, Plugins, MCP, Auto-Memory komplett) **und** die Agent-Identität wird über `--append-system-prompt-file <ws>/CLAUDE.md` explizit injiziert. Das ist reproduzierbar und zukunftssicher — laut Doku wird `--bare` ohnehin der zukünftige Default für `-p`.
**Konsequenz für bestehenden Code**:
- `disclaw.yaml`-Default: `workspaces_root: "~/.disclaw/workspaces"` (Tilde-Expansion + `path.resolve` gegen `os.homedir()`).
- `src/config/loader.ts`: Tilde-Expansion implementieren, absolute Pfade per `path.resolve` normalisieren.
- `src/agent/runner.ts`: Immer `--bare` + `--append-system-prompt-file` an den Call anhängen.
- `src/commands/new-agent.ts`: Workspace unter dem neuen Root anlegen, Channel-Name und DB-Einträge identisch wie vorher.
- **Migrationshinweis für bestehende Workspaces**: Ein `/migrate-workspaces`-Command (Phase 6+) oder manuelle Instruktion im README: Bestehende `./workspaces/<agent>/`-Ordner nach `~/.disclaw/workspaces/<agent>/` verschieben, DB-Spalte `workspace_path` per `UPDATE` anpassen, Channel-Mapping bleibt. Im MVP reicht ein dokumentierter manueller Schritt.
- **Dev-Opt-in**: Wenn `workspaces_root` innerhalb des DisClaw-Repos liegt (erkannt via `path.relative(repoRoot, wsRoot)` startet nicht mit `..`), loggt der Bot beim Start eine Warnung und setzt zusätzlich `claudeMdExcludes` im gerenderten `.claude/settings.json` mit absoluten Pfaden für die DisClaw-Projekt-CLAUDE.md und alle `.claude/rules/**`-Globs des Parents.
**Quelle / Status**: Bestätigt via `cli-feature-answers.md` Frage 8 mit Zitat aus `code.claude.com/docs/en/memory`. Konfidenz: hoch.
### 2.7 Logging und Error-Handling
**Was**: `pino` als strukturierter JSON-Logger. Child-Logger pro Komponente (`bot`, `router`, `runner`, `commands.new-agent`). Error-Handler-Strategie: Fehler werden geloggt (mit Stack), der Discord-Channel bekommt eine **sanitized** Nachricht (keine absoluten Pfade, keine Stack Traces).
**Warum**: `console.log` ist bei mehreren Agenten und paralleler Ausführung unlesbar. Pino ist schnell, JSON-first, und Standard in Node-Server-Apps. Pretty-Printing nur in dev via `pino-pretty`.
**Wie**:
- Neue Datei: `src/runtime/logger.ts`, exportiert `rootLogger` und `childLogger(component: string)`.
- Sanitizer: `src/runtime/sanitize.ts`, entfernt absolute Pfade (Regex auf `ROOT_DIR`), Tokens, Home-Pfad.
### 2.8 Test-Strategie: Vitest, Unit-first
**Was**: `vitest` als Test-Runner. Unit-Tests für reine Funktionen (splitForDiscord, sanitizedEnv, parseClaudeJsonOutput, agent-name-validation, channel-queue). Integration-Tests mit gemockten Discord- und CLI-Clients. Keine E2E-Tests im MVP (zu fragil gegen echte Discord-API).
**Warum**: Vitest hat minimale Config, läuft nativ mit TypeScript via esbuild, und ist für diese Projektgröße angemessener als Jest.
**Wie**:
- `tests/unit/` für reine Funktionen.
- `tests/integration/` für Router + Runner mit Mock-Spawner, Mock-DiscordChannel.
- Mock-Strategie für `claude` CLI: Ein Fake-Spawner der ein `Fake-Claude`-Script aufruft (`tests/fixtures/fake-claude.mjs`), das deterministische JSON-Antworten ausgibt. So testet man die echten spawn-Pfade, ohne von einer echten Claude-Installation abhängig zu sein.
- Discord: `@discordjs/core`-Mocks oder manuell via Interface-Abstraktion — siehe §5 "Discord-Adapter".
---
## 3. Tools und Libraries
### Wird verwendet
| Paket | Version | Zweck | Begründung |
|---|---|---|---|
| `discord.js` | ^14.16.0 | Discord-Client | Schon drin, de-facto Standard. |
| `better-sqlite3` | ^11.0.0 | SQLite-DB | Schon drin, synchrone API passt zum Handler-Modell. |
| `dotenv` | ^16.4.0 | `.env`-Loader | Schon drin. |
| `yaml` | ^2.6.0 | YAML-Parser für `disclaw.yaml` und `agent.yaml` | Schon drin. |
| `zod` | ^3.23 | Runtime-Validierung für Config, CLI-JSON-Output, Slash-Command-Args | Sichert Grenzen zwischen untrusted Input und Code. Klein, ohne Runtime-Abhängigkeiten. |
| `cross-spawn` | ^7 | Plattformübergreifender `child_process.spawn`-Ersatz | Löst Windows-`.cmd`-Wrapper korrekt auf (`claude.cmd`), quoted Windows-Args sicher, umgeht CVE-2024-27980 (Node 20.12+). Ohne Shell, also kein Command-Injection-Risiko. ~30 kB, keine transitiven Deps. Siehe §2.2. |
| `@types/cross-spawn` | ^6 | Typdefinitionen | devDep. |
| `pino` | ^9 | Strukturiertes Logging | Schnell, JSON-first, Child-Logger. |
| `pino-pretty` | ^11 | Dev-only Pretty-Printer | Nur in `devDependencies`. |
| `vitest` | ^2 | Test-Runner | Native TS, schnell, minimal Config. |
| `@types/node` | ^22 | Typdefinitionen | Schon drin. |
| `typescript` | ^5.7 | Compiler | Schon drin. |
### Wird explizit NICHT verwendet
- **Kein ORM (Prisma/Drizzle)**: Overkill für 2 Tabellen. `better-sqlite3`-Prepared-Statements sind schnell und typisiert genug via manuelle Interfaces.
- **Kein Express/Fastify**: Kein HTTP-Endpoint im MVP. Wenn später ein Web-Dashboard dazu kommt, Fastify.
- **Kein Redis/BullMQ**: Single-Process-Queue reicht. Externe Queues erst bei Multi-Instance-Deployment.
- **Kein `discord-markdown` o. ä.**: Splitting ist simpel genug für Eigenbau und genauer kontrollierbar.
- **Kein Jest**: Vitest ist leichter, schneller, direkt TS-fähig.
- **Kein `@anthropic-ai/sdk` oder `claude-agent-sdk`**: ADR-001 sagt CLI-only.
- **Kein `winston`**: Pino ist schneller und hat bessere Child-Logger-Semantik.
---
## 4. Phasenplan
Jede Phase ist in sich lauffähig. Abhängigkeiten sind explizit genannt. Nummerierung ist nicht chronologisch, sondern logisch.
### Phase 0 — Härtung (P0, blockiert jede Live-Nutzung)
**Ziele**: Sicherheitslücken schließen, bevor irgendein echter Discord-Server angeschlossen wird.
**Tasks**:
1. `src/agent/runner.ts`: `shell: true` entfernen; stattdessen `cross-spawn` verwenden (siehe §2.2). Neue Helper-Datei `src/runtime/resolve-claude.ts`: liest `CLAUDE_PATH` aus `.env` oder führt einmalig `where claude`/`which claude` aus, cached das Ergebnis in-memory, wirft eine klare Fehlermeldung bei Nichtfund.
2. `src/runtime/env.ts` (neu): `sanitizedEnv()` wie in §2.6. In `runner.ts` statt `...process.env` verwenden.
3. **Workspace-Root-Umstellung (§2.9)**: `disclaw.yaml`-Default auf `~/.disclaw/workspaces` umstellen. `src/config/loader.ts`: Tilde-Expansion + `path.resolve`. `src/commands/new-agent.ts`: Workspace unter dem neuen Root anlegen. Warning-Log, wenn der konfigurierte Root innerhalb des DisClaw-Repos liegt. Manueller Migrationshinweis für bestehende `./workspaces/<agent>/`-Ordner im README.
4. `src/commands/new-agent.ts`: Name-Regex behalten, zusätzlich Path-Containment-Check:
```ts
const root = path.resolve(config.workspaces_root);
const wsPath = path.resolve(root, name);
if (!wsPath.startsWith(root + path.sep)) throw new Error("Path traversal");
```
5. `src/agent/identity.ts`: `.claude/settings.json`-Template härten — Schema wie in §2.5 (mit `permissions.allow/ask/deny/defaultMode` und dreistufigem `hooks`-Block). Deny für `../**`, Deny-Patterns für gefährliche Bash-Kommandos.
6. `src/agent/identity.ts`: `CLAUDE.md`-Template bekommt die Prompt-Injection-Resistenz-Klausel (Punkt 5 aus §3 der Analyse).
7. `src/agent/runner.ts`: CLI-Call immer mit `--bare --append-system-prompt-file <ws>/CLAUDE.md` aufbauen (siehe §2.9), damit Parent-`CLAUDE.md`-Leaks strukturell ausgeschlossen sind — auch für Dev-Mode-Workspaces innerhalb des Repos.
8. `tests/unit/sanitize-env.test.ts`, `tests/unit/path-traversal.test.ts`, `tests/unit/resolve-claude.test.ts` (mocked), `tests/unit/workspace-root-resolve.test.ts` (Tilde-Expansion, Warning bei Repo-internem Pfad).
**Definition of Done**:
- Kein `shell: true` mehr im Code; Spawning läuft über `cross-spawn` mit resolvtem Claude-Pfad.
- Ein Test demonstriert, dass `DISCORD_BOT_TOKEN` nicht im Child-Env landet.
- Ein Test demonstriert, dass `name: "../etc"` als Slash-Command-Input abgewiesen wird.
- `.claude/settings.json` in einem frisch erstellten Workspace enthält die Deny-Liste und das korrekte `hooks.PreToolUse[].matcher`-Layout.
- `CLAUDE.md` enthält den Prompt-Injection-Resistenz-Satz.
- Default-`workspaces_root` zeigt auf `~/.disclaw/workspaces`, Tilde wird korrekt expandiert, Warning bei repo-internem Pfad.
- CLI-Calls enthalten `--bare` und `--append-system-prompt-file <ws>/CLAUDE.md`; ein Test (mit Fake-Claude) verifiziert, dass kein Parent-CLAUDE.md-Leak auftritt.
**Abhängigkeiten**: Keine. Erste Phase.
### Phase 1 — Solides MVP
**Ziele**: Ein Agent antwortet zuverlässig auf Discord, mehrfache Messages im selben Channel werden serialisiert, Fehler sind verständlich, Logs sind strukturiert, Grundstruktur ist testbar.
**Tasks**:
1. `src/runtime/logger.ts` (neu): Pino-Setup, `rootLogger`, `childLogger(component)`. In `index.ts`, `bot.ts`, `router.ts`, `runner.ts`, `commands/new-agent.ts` `console.*` ersetzen.
2. `src/runtime/sanitize.ts` (neu): `sanitizeForDiscord(text, rootDir)` — entfernt Abspfade.
3. `src/runtime/channel-queue.ts` (neu): `ChannelQueue`-Klasse wie in §2.3. In `router.ts` integrieren.
4. `src/runtime/concurrency.ts` (neu): `Semaphore(max: number)`-Klasse. Globaler Cap aus `disclaw.yaml``max_concurrent_agents: 4`.
5. `src/discord/split.ts` (neu): `splitForDiscord()` wie in §2.4. `router.ts` verwendet ihn.
6. `src/agent/runner.ts`: API-Refactoring auf `RunResult = { text: string, sessionId?: string, toolCalls?: ToolCall[] }`. Return-Type vorbereiten für spätere Streaming-Unterstützung (`runAgent()` gibt heute `Promise<RunResult>`, später zusätzlich `runAgentStream()`).
7. `src/runtime/zod-schemas.ts` (neu): Zod-Schemas für `disclaw.yaml`, `agent.yaml`, Claude-CLI-JSON-Output. `parseClaudeJsonOutput()` validiert via Zod.
8. `src/config/loader.ts`: Validierung über Zod statt `as` Cast.
9. `src/db/database.ts`: Schema-Inline-String entfernen, stattdessen `src/db/schema.sql` laden (Single Source of Truth).
10. `tests/unit/split-for-discord.test.ts` (Fences, lange Zeilen, Fallback), `tests/unit/channel-queue.test.ts`, `tests/unit/parse-claude-output.test.ts`.
11. `tests/integration/router.test.ts`: mockt DB, DiscordChannel (Interface), Spawner — verifiziert das End-to-End-Verhalten bei parallelen Nachrichten (Reihenfolge, keine Race).
12. `.env.example` hinzufügen.
13. `package.json`: `test`-Script, `devDependencies` für vitest, pino-pretty, zod, pino.
14. `docs/cli-feature-probe.md` (neu): Manuelles Regression-Script, das die kritischen Annahmen aus `cli-feature-answers.md` (Session-ID top-level, `usage`-Block-Pfade, stream-json-Event-Typen, Session-Slug-Regel) gegen die lokal installierte CLI verifiziert und Resultate dokumentiert. Läuft als Sanity-Check bei CLI-Upgrades.
**Definition of Done**:
- Bot startet, erstellt Management-Channel, akzeptiert `/new-agent`, antwortet im Agent-Channel korrekt auf eine Nachricht.
- Drei schnell hintereinander gesendete Nachrichten im gleichen Channel werden in exakter Reihenfolge verarbeitet (verifiziert im Integration-Test).
- Log-Output ist strukturiertes JSON (mit `pino-pretty` in dev).
- `npm test` läuft grün, coverage für `split`, `channel-queue`, `sanitize-env`, `parse-claude-output`, `router`.
- Kein `console.log` mehr im Produktionscode (nur in Tests/Probe-Scripts erlaubt).
**Abhängigkeiten**: Phase 0.
### Phase 2 — Context-Effizienz (`--resume`) — FREIGEGEBEN
**Ziele**: Prompt-Cache-Hits, drastisch gesenkte Input-Tokens, saubere Historie-Semantik.
**Status**: Freigegeben durch `cli-feature-answers.md` Frage 1+2. `-p` + `--resume` ist dokumentiert, `session_id` liegt top-level im JSON-Output, Session-ID bleibt beim Resume stabil (es sei denn `--fork-session`).
**Tasks**:
1. DB-Migration: `ALTER TABLE workspaces ADD COLUMN claude_session_id TEXT; ALTER TABLE workspaces ADD COLUMN session_updated_at TEXT;`. In `schema.sql` pflegen, in `database.ts` als Migration-Step addieren (simpler `try { ALTER } catch {}`-Block reicht).
2. `src/db/database.ts`: Neue Methoden `setSession(workspaceId, sessionId)`, `getSession(workspaceId)`, `clearSession(workspaceId)`. `setSession` aktualisiert auch `session_updated_at` mit aktuellem ISO-Timestamp.
3. `src/agent/runner.ts`: Session-Flow wie in §2.1. **Nach jedem erfolgreichen Run** `store.set(ws.id, json.session_id)` aufrufen — auch wenn die ID gleich geblieben ist (defensive Update-after-run). Fallback-Pfad implementieren: Bei `--resume`-Fehler `store.clear` + Retry ohne `--resume` (optional `--continue` als Zwischenstufe).
4. Die History-Freitext-Injektion nur noch als Cold-Start-Rehydrierung verwenden (strukturierter als heute, mit klaren `<turn role="user|assistant">` Blocks).
5. `tests/integration/session-resume.test.ts`: Fake-Claude gibt beim ersten Call eine `session_id` top-level zurück, der zweite Call muss diese via `--resume <id>` wieder verwenden. Fehlschlag simulieren → Fallback-Pfad wird getroffen. Edge-Case: Fake-Claude gibt beim zweiten Call eine **neue** `session_id` zurück (Fork-Szenario) → DB muss auf neue ID updaten.
6. Metrik-Logging: `usage`-Block aus JSON-Output parsen. Die exakten Pfade (`result.usage.cache_read_input_tokens` vs. top-level `usage.cache_read_input_tokens`) werden im Probe-Script empirisch verifiziert und dann hartcodiert. Log-Line: `log.info({ input_tokens, cache_read, cache_create, output_tokens }, "claude_run")`.
7. Optional (Nice-to-have): Neue Tabelle `agent_runs(workspace_id, ts, input_tokens, cache_read, cache_create, output_tokens, duration_ms)` für Pro-Agent-Metriken.
**Definition of Done**:
- Zweite Nachricht im gleichen Channel verwendet dieselbe `session_id` (verifiziert via Fake-Claude).
- Session-ID überlebt Bot-Neustart.
- Bei bewusstem Löschen der Session-Datei reagiert der Runner mit automatischem Fallback, speichert neue Session-ID, keine User-sichtbare Fehlermeldung.
- `session_id` wird nach jedem Run in der DB geschrieben, auch wenn identisch.
- Log-Line zeigt gecachte Tokens.
**Abhängigkeiten**: Phase 0, Phase 1.
### Phase 3 — UX-Feinschliff
**Ziele**: Discord-Ausgabe fühlt sich gut an. Lange Antworten, Codeblöcke, Attachments, Typing-Refresh.
**Tasks**:
1. `src/router.ts`: Typing-Refresh-Intervall auf 8 s. Typing wird nur so lange ausgesendet, wie der `runAgent`-Call läuft.
2. `src/discord/send-response.ts` (neu): `sendResponse(channel, text, { asAttachmentAfter = 8000 })`. Wenn Text > Schwelle → als `.md`-Attachment senden.
3. Attachment-Support eingehend: `router.ts` prüft `message.attachments`. Download in `workspaces/<agent>/.disclaw-inbox/<ts>-<name>.ext`. Im Prompt referenziert. Nach erfolgreicher Antwort: TTL-Cleanup-Task.
4. Input-Limit: User-Nachricht > 4000 Zeichen → Reaction `⚠️` und Hinweis im Channel.
5. Reaction `👀` sobald Message in Queue landet; `✅` bei Erfolg; `❌` bei Fehler. Alle `.catch(() => {})`, nicht blockierend.
6. `tests/unit/send-response.test.ts`, `tests/unit/attachment-inbox.test.ts` (mit tmp-Dir).
**Definition of Done**:
- Lange Antworten werden korrekt gesplittet ohne kaputte Fences.
- Sehr lange Antworten werden als `.md`-File gesendet.
- Bilder aus Discord landen im Workspace und Claude kann sie via Read-Tool sehen.
- Typing-Indikator bleibt die ganze Laufzeit sichtbar, ohne "pausen".
**Abhängigkeiten**: Phase 1.
### Phase 4 — Skills und Permissions (Profile)
**Ziele**: Agents sind pro Rolle sinnvoll voreingestellt. Hook-Guard ist aktiv. Audit-Log läuft.
**Tasks**:
1. `src/agent/profiles/` (neu): Ordner mit `researcher.md`, `developer.md`, `writer.md`, `ops.md`, `sandboxed.md` als `CLAUDE.md`-Templates und `*.settings.json` als `.claude/settings.json`-Templates. Platzhalter `{{name}}`, `{{role}}`, `{{workspace_abs}}`.
2. `src/agent/profiles/index.ts`: `ProfileRegistry` — maps name → `{ claudeMd: Template, settings: Template, hooks: Template[] }`.
3. `src/commands/new-agent.ts`: Neuer Option-Parameter `profile` mit Choices. Default = `developer`.
4. `src/agent/identity.ts`: `setupAgentWorkspace()` nimmt `profile` und rendert Templates aus der Registry.
5. `src/agent/hooks/guard-tool.cjs` (generiert beim Create): Node-Script, das als PreToolUse-Hook jeden Tool-Call gegen den Workspace-Pfad prüft, außerhalb → exit 2 mit Block-Reason. Jeder Call wird zusätzlich in `workspaces/<agent>/.disclaw-audit.jsonl` geschrieben. Event-Typen gemäß `cli-feature-answers.md` Frage 3: `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `Stop`. Wir nutzen primär `PreToolUse` mit `matcher: "Bash|Read|Write|Edit"`.
6. `.claude/settings.json`-Template referenziert den Hook im dreistufigen Layout (`hooks.PreToolUse[].hooks[].{type, command, timeout}`), siehe §2.5.
7. `tests/unit/profile-registry.test.ts`, `tests/integration/hook-guard.test.ts` (mit Fake-Claude, der einen bösartigen Tool-Call simuliert).
**Definition of Done**:
- `/new-agent name:r profile:researcher` erstellt einen Agenten mit read-only-Permissions.
- Ein Hook-Test verifiziert, dass `Bash(cat ../../other/secret.txt)` abgewiesen und im Audit-Log protokolliert wird.
- Alle fünf Profile sind vorhanden und passen zu ihrer §3-Tabelle der Analyse.
**Abhängigkeiten**: Phase 0 (Settings-Template). Settings-Schema ist durch `cli-feature-answers.md` Frage 3 verifiziert.
### Phase 5 — Streaming
**Ziele**: Antworten erscheinen inkrementell im Discord-Channel.
**Tasks**:
1. `src/agent/runner.ts`: Zweiter Export `runAgentStream(): AsyncIterable<StreamEvent>`. Intern: `claude -p "<msg>" --output-format stream-json --verbose --include-partial-messages --bare --append-system-prompt-file <ws>/CLAUDE.md` (plus `--resume` wie in Phase 2). Die Flags `--verbose` und `--include-partial-messages` sind laut `cli-feature-answers.md` Frage 4 **zwingend**, sonst emittiert die CLI nur das Final-`result`-Event ohne Text-Deltas.
2. NDJSON-Parser: Zeilenweise `JSON.parse`. Dispatch nach `.type`: `system` (init/api_retry, skip), `assistant` (Text- und Tool-Use-Blocks im Native-API-Format), `user` (Tool-Results), `stream_event` (partial deltas — interessant ist `.event.delta.type === "text_delta"` mit `.event.delta.text`), `result` (Final-Event, enthält session_id und usage). Parser **defensiv** gegen unbekannte Typen (skip statt crash) — CLI-Updates können neue Subtypes einführen.
3. `src/discord/stream-to-discord.ts` (neu): Nimmt einen `AsyncIterable<StreamEvent>`, akkumuliert Text-Deltas, editiert die Bot-Message alle ~1200 ms (Discord-Rate-Limit), rolled auf neue Message bei >1900 Zeichen. Bei `result`-Event: finalen Stand commitet.
4. `router.ts`: Feature-Flag `streaming: true/false` aus `disclaw.yaml`. Fallback auf Bulk-Mode bei Fehlern.
5. `tests/integration/streaming.test.ts` mit Fake-Claude, der ein NDJSON-Scripted-Log auf stdout schreibt (system → assistant → mehrere stream_events mit text_deltas → result).
**Definition of Done**:
- Streaming ist per Flag aktivierbar und funktioniert End-to-End.
- Edits respektieren Discord-Rate-Limits (min. 1200 ms zwischen Edits).
- Bei Fehler im Stream wird die Message sauber finalisiert.
**Abhängigkeiten**: Phase 1 (Runner-API vorbereitet). Phase 2 (Session-Resume bleibt auch im Stream-Modus aktiv).
### Phase 6+ — Zukunft
Klar: nicht jetzt planen, aber Pfade offenhalten.
- **`/list-agents`, `/delete-agent`, `/agent-config`**: Command-Handler-Interface aus §5 macht das zu einer 30-Minuten-Sache pro Command. `/delete-agent` muss Channel + Workspace + DB-Einträge + `~/.claude/projects/<hash>/`-Session atomar löschen.
- **User-Ratelimit**: Token-Bucket in `src/runtime/ratelimit.ts`, Map<userId, Bucket>.
- **Daily-Budget pro Agent**: DB-Tabelle `agent_usage(workspace_id, day, runs)`, Check vor jedem Run.
- **Agent-zu-Agent**: Ein Agent kann eine Nachricht an einen anderen Agent-Channel adressieren → Router erkennt und delegiert.
- **Docker-Isolation**: `DockerRunner` als zweite Implementierung des `AgentEngine`-Interface (§5).
- **Web-Dashboard**: Fastify-Server, der `DisclawDatabase` teilt.
- **Alternative AI-Engine**: Ein `ClaudeSdkEngine` gegen `claude-agent-sdk` für Nutzer ohne Claude-Pro-Abo. Gleiches `AgentEngine`-Interface.
---
## 5. Erweiterbarkeit (Fokus!)
Der Code ist so zu strukturieren, dass neue Features ohne Core-Änderungen andocken. Kernmechanismen:
### 5.1 Schichtentrennung
```
src/
bot.ts Discord-Adapter (Event → Router)
router.ts Routing + Queue + Sanitize (kennt Runner abstrakt)
discord/ Alles Discord-nahe: split, send, stream-to-discord
agent/
engine.ts AgentEngine-Interface (siehe 5.6)
runner.ts ClaudeCliEngine: implements AgentEngine
identity.ts Workspace-Create (delegiert an Profile)
profiles/ Templates pro Profil
hooks/ Pre/Post-Hook-Scripts
commands/
index.ts CommandRegistry (siehe 5.2)
new-agent.ts
list-agents.ts (später)
delete-agent.ts (später)
runtime/
channel-queue.ts
concurrency.ts
env.ts
logger.ts
sanitize.ts
ratelimit.ts (später)
db/
database.ts Repository-Stil, keine Business-Logik
schema.sql
config/
loader.ts
```
Regel: `runtime/`, `db/`, `discord/` sind Leaf-Module, kennen sich nicht gegenseitig über die Grenzen hinweg. `agent/` kennt `runtime/` und `db/`-Typen, aber **nicht** `discord.js`. `router.ts` ist der einzige Ort, der Discord und Agent zusammenbringt.
### 5.2 Plugin-Interface für Slash-Commands
Neues Interface, das neue Commands an einer Stelle registriert:
```ts
// src/commands/types.ts
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
import { DisclawDatabase } from "../db/database";
import { DisclawConfig } from "../config/loader";
export interface CommandContext {
db: DisclawDatabase;
config: DisclawConfig;
managementChannelId: string | null;
}
export interface DisclawCommand {
definition: SlashCommandBuilder;
handle(interaction: ChatInputCommandInteraction, ctx: CommandContext): Promise<void>;
}
```
```ts
// src/commands/index.ts
import { newAgentCommand } from "./new-agent";
// import { listAgentsCommand } from "./list-agents";
export const commands: DisclawCommand[] = [
newAgentCommand,
// listAgentsCommand,
];
```
`bot.ts` registriert alle Commands in einer Schleife und dispatched via `commandName`-Lookup. Neuer Command = neue Datei + ein Array-Eintrag. Keine Änderung an `bot.ts` nötig.
### 5.3 Profile-Templates als Registry
```ts
// src/agent/profiles/index.ts
export interface ProfileTemplate {
name: string;
description: string;
renderClaudeMd(ctx: { name: string; role: string }): string;
renderSettings(ctx: { workspaceAbs: string; name: string }): Record<string, unknown>;
hooks?: HookDefinition[];
}
export const profiles: Record<string, ProfileTemplate> = {
developer: developerProfile,
researcher: researcherProfile,
writer: writerProfile,
ops: opsProfile,
sandboxed: sandboxedProfile,
};
```
Ein neuer Profil-Typ (z. B. `data-scientist`) ist eine neue Datei + ein Registry-Eintrag. `identity.ts` muss nicht angefasst werden.
### 5.4 Hook-System (Pre/Post Message, Pre/Post Spawn)
```ts
// src/agent/hooks.ts
export type HookEvent =
| { type: "preMessage"; workspace: Workspace; message: string }
| { type: "postMessage"; workspace: Workspace; response: string }
| { type: "preSpawn"; workspace: Workspace; args: string[] }
| { type: "postSpawn"; workspace: Workspace; result: RunResult };
export type Hook = (event: HookEvent) => Promise<void | { cancel?: boolean; reason?: string }>;
export class HookBus {
private hooks: Hook[] = [];
register(h: Hook) { this.hooks.push(h); }
async emit(e: HookEvent): Promise<{ cancel: boolean; reason?: string }> { ... }
}
```
`router.ts` und `runner.ts` emittieren an einen global injizierten `HookBus`. Beispiel-Hooks: Audit-Logger, Ratelimit, Daily-Budget, Metriken. Alle außerhalb des Core-Codes, einfach pluggable.
**Wichtig**: Die `.claude/`-Hooks (PreToolUse etc.) sind ein anderer Mechanismus — die werden von Claude Code selbst gefeuert. Das HookBus hier ist DisClaw-intern.
### 5.5 MCP-Server pro Workspace (vorbereitet, später aktiviert)
Das Profile-Template kann optional eine `mcpServers`-Sektion in die `.claude/settings.json` rendern. Die Liste der verfügbaren MCPs ist in `src/agent/mcp/registry.ts` zentral gepflegt. Aktivierung pro Profil deklarativ:
```ts
developerProfile.mcpServers = ["git"];
opsProfile.mcpServers = ["docker", "k8s"];
```
Der Renderer fügt die korrekten Stanzas in `settings.json` ein. Kein Code-Change im Runner nötig.
### 5.6 Abstraktion der AI-Engine
```ts
// src/agent/engine.ts
export interface AgentEngine {
run(opts: RunOptions): Promise<RunResult>;
runStream?(opts: RunOptions): AsyncIterable<StreamEvent>;
}
```
`ClaudeCliEngine` ist heute die einzige Implementierung. Später könnten `ClaudeSdkEngine` (direkte API) oder `DockerClaudeCliEngine` (CLI in Container) als Drop-in-Alternativen dazu kommen. Der Router kennt nur das Interface.
### 5.7 Discord-Adapter-Abstraktion (für Testbarkeit)
Alle Discord-Interaktionen im Router gehen über ein kleines Interface `ChannelSink`:
```ts
export interface ChannelSink {
send(text: string): Promise<{ id: string }>;
edit(messageId: string, text: string): Promise<void>;
sendTyping(): Promise<void>;
sendFile(name: string, content: Buffer): Promise<void>;
react(messageId: string, emoji: string): Promise<void>;
}
```
In Produktion wraps das einen `TextChannel`. In Tests ist es ein Mock. So sind Router-Tests komplett ohne `discord.js` möglich.
---
## 6. Offene Fragen an den AI Engineer
Die ursprünglichen 10 Fragen wurden am 2026-04-08 vom AI Engineer beantwortet. Siehe `docs/cli-feature-answers.md` für Volltext, Quellen und Konfidenzangaben.
**Verbleibende Rest-Unsicherheiten** (alle empirisch via Probe-Script zu bestätigen, kein Blocker für Phase-Start):
1. **Exakte JSON-Pfade des `usage`-Blocks** (`result.usage.cache_read_input_tokens` vs. top-level `usage.cache_read_input_tokens`): Die Existenz der Felder ist durch das API-Layer garantiert, aber der Output-Shape der CLI wird in Phase 2 per Probe-Script empirisch verifiziert und dann im Parser hartcodiert.
2. **`stream-json` Event-Schema-Stabilität**: Die groben Typen (`system`, `assistant`, `user`, `stream_event`, `result`) sind dokumentiert, aber Subtypes und Feld-Layouts können zwischen CLI-Versionen driften. Mitigation: Parser defensiv (skip unknown), Regression-Test im Probe-Script.
3. **Session-Slug-Transformation auf Linux/Mac**: Windows-Slug-Regel (`C:\Code\side\disclaw` → `C--Code-side-disclaw`) ist empirisch bestätigt, Linux-Regel (`/home/nick/...` → `-home-nick-...`) ist aus Analogie abgeleitet. Muss auf Linux-Runner geprüft werden, sobald `/delete-agent` implementiert wird (Phase 6+).
4. **Multimodal-Passthrough ohne Tool-Round-Trip**: `@./path/image.png` im Prompt funktioniert via Read-Tool + Vision; ein direkter Vision-Input-Block ohne Round-Trip existiert im CLI-`-p`-Modus nicht und würde eine Migration zum Agent SDK erfordern. Scope-Entscheidung: bleibt Phase 6+.
---
## 7. Risiken und Mitigation
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| `--resume` funktioniert nicht wie erwartet in der installierten CLI | Niedrig | Hoch (Phase 2 blockiert) | Per `cli-feature-answers.md` Frage 1+2 offiziell dokumentiert und bestätigt. Probe-Script als Regression-Check bleibt. Fallback-Pfad (DB-Rehydrierung + `--continue`) bleibt als Degradation verfügbar. |
| CLAUDE.md-Walk-Up kontaminiert Agenten-Identität (Parent-Projekt-CLAUDE.md oder `~/.claude/CLAUDE.md` leaked in System-Prompt) | Mittel (strukturell gegeben, wenn nicht mitigiert) | Hoch (Reproduzierbarkeit, Sicherheit, Identitäts-Leak) | §2.9: Workspaces außerhalb des DisClaw-Repos (`~/.disclaw/workspaces/`) **plus** `--bare` + `--append-system-prompt-file` bei jedem Call. Integration-Test mit Fake-Claude, der den empfangenen System-Prompt echoed, und Assertion, dass ausschließlich die Workspace-`CLAUDE.md` drin ist. Für Dev-Mode-Workspaces innerhalb des Repos zusätzlich `claudeMdExcludes` im Settings. |
| Windows-Spawn schlägt auf frischer Node-20.12+-Installation fehl (`claude.cmd`, CVE-2024-27980) | Mittel (default), niedrig (nach Phase 0) | Hoch (DisClaw startet auf Windows gar nicht) | §2.2: `cross-spawn` als Dependency, `where claude`-Lookup mit absolutem Pfad-Cache, klare Fehlermeldung wenn nicht auffindbar. CI-Test auf Windows-Runner (GitHub Actions `windows-latest`) verifiziert Smoke-Test-Spawn. |
| Command Injection bleibt unentdeckt in einem Codepfad | Niedrig (nach Phase 0) | Kritisch | `shell: false` als Lint-Regel im CI. Code-Review-Checkliste. Integration-Test mit bösartigem Input. |
| Prompt-Injection bricht Permissions aus | Mittel | Hoch | Defense in depth: CLAUDE.md-Klausel, settings.json deny, PreToolUse-Hook, Audit-Log. Keine davon allein reicht. |
| `.claude/settings.json`-Schema ändert sich zwischen CLI-Versionen | Mittel | Mittel | Schema-Version beim Workspace-Create detektieren (`claude --version` parsen), Template pro Release pflegen. Warnung bei unbekannter Version. |
| Parallele Nachrichten im selben Channel korrumpieren Workspace-Dateien | Hoch (ohne Queue) | Hoch | Per-Channel-Queue in Phase 1. Integration-Test. |
| Secret-Exfiltration via `echo $DISCORD_BOT_TOKEN` | Hoch (aktuell) | Kritisch | `sanitizedEnv()` in Phase 0. Test, der den resultierenden Env-Set prüft. |
| Discord-Rate-Limits bei Streaming | Niedrig | Mittel | Min. 1200 ms zwischen Edits, neue Message bei Überlauf. discord.js 429-Handling nutzen. |
| Speicher-Leaks bei Long-Running-Bot-Prozess | Niedrig | Mittel | Pino-Logs + `process.memoryUsage()` alle 5 min als Debug-Log. `channelQueues`-Map räumt sich selbst auf. |
| Zwei DisClaw-Instanzen gegen dieselbe DB/Workspace | Niedrig | Hoch | Lock-File im Workspaces-Root beim Start. Fehlschlag, wenn Lock existiert und PID noch läuft. |
| Benutzer editieren `CLAUDE.md` manuell mit Injection | Mittel | Niedrig (der Benutzer "schadet" sich selbst) | Dokumentiert, kein technischer Fix. |
| PreToolUse-Hook-Script ist fehlerhaft und blockt legitime Calls | Mittel | Mittel | Hook schreibt Audit-Log auch bei Block; `/reset-permissions`-Command später; im MVP manuelles Editieren der Settings. |
| Test-Suite wird träge, wenn echte `claude` CLI involviert ist | Mittel | Niedrig | Unit-Tests nutzen Fake-Spawner. Echte-CLI-Tests nur in `tests/probe/` und manuell. |