Ctrl K
ComunidadGuías…

docs/METIS_MODERNIZATION_PLAN.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Metis AI – priorisierter Modernisierungsplan

> **Status:** Snapshot **21 Aug 2026**. Reihenfolge weiter nützlich; Live-P0/P1 stehen in [PRODUCTION-AUDIT.md](./PRODUCTION-AUDIT.md).

**Stand:** 2026-08-21  
**Basis:** [METIS_ARCHITECTURE_AUDIT.md](./METIS_ARCHITECTURE_AUDIT.md)  
**Prinzip:** Kein Big Bang. Jede Stufe ist rückwärtskompatibel, messbar, durch Feature Flags abschaltbar und besitzt einen Rollback-Pfad.

## 1. Verbindliche Arbeitsregeln

Vor jeder Implementierungswelle:

1. Arbeitsbaum und bestehende Änderungen inventarisieren.
2. Datenbanksicherung mit Restore-Probe erstellen.
3. Eigene Branch pro Slice anlegen; keine Vermischung mit den bereits vorhandenen fünf Änderungen.
4. Migrationen expand/contract ausführen, niemals Spalten oder Daten im ersten Schritt löschen.
5. Tests zuerst bzw. zusammen mit der Änderung liefern.
6. `lint`, `typecheck`, Tests, isolierten Clean Build und relevante Fault-Injection bestehen.
7. Canary/Feature Flag, Metriken und dokumentierten Rollback definieren.
8. Keine Produktionsdienste für Entwicklungs-/Lasttests verwenden.

Prioritätsdefinition:

- **P0:** verhindert sicheren autonomen oder Multi-User-Betrieb;
- **P1:** verhindert verlässliche Orchestrierung, Recovery oder Betrieb;
- **P2:** Skalierung, Wartbarkeit und Produktqualität nach P0/P1.

## 2. Ziel-SLOs

| Bereich | Ziel |
|---|---|
| Enqueue p95 bei 20 parallelen Runs | < 50 ms |
| Claim-Duplikate | 0 |
| doppelte externe Side Effects nach Crash/Retry | 0 für idempotente/receipt-fähige Tools |
| Cancel bis terminal p95 | < 5 s; harter Kill < 15 s |
| verlorene durable Events | 0 |
| SSE-Reconnect | vollständiges Replay ab Cursor |
| Cross-Tenant-Zugriffe | 0 in Negativsuite |
| Tool außerhalb Manifest | 0; bereits in Discovery unsichtbar |
| Build/Releasetore | lint + typecheck + tests + clean build grün |
| DB-Lock-Fehler | 0 in normaler Last; gemessene Lock-Wait-Metrik |
| Provider Contract | jeder aktive Adapter besteht Stream/Tool/Error/Cancel/Timeout |

## 3. P0 – Security Boundary zuerst

### P0.1 Remote-Approval serverseitig machen

**Problem:** `remote_client_terminal` bietet `approved` als Modellargument an und reicht es bis `requestRemoteClient` durch.

**Änderungen:**

- `approved` vollständig aus MCP-Toolschemas und Modellargumenten entfernen.
- Tabellen `approval_requests` und `approval_decisions` ergänzen.
- Request bindet `owner_id`, `run_id`, `tool_call_id`, `client_id`, Aktion, kanonischen Args-Hash, Ablaufzeit und Nonce.
- UI entscheidet über authentifizierte API.
- Gateway akzeptiert nur eine serverseitig erzeugte Approval-ID und konsumiert sie atomar genau einmal.
- Änderung der Args invalidiert die Approval.
- Late/Replayed Approvals werden abgelehnt und auditiert.

**Tests:** Modell setzt boolesches Approval; falscher User; geänderte Args; abgelaufen; doppelt konsumiert; Parent-/Child-Verwechslung.

**Done:** Kein modellkontrollierter Wert kann eine Approval ersetzen.

### P0.2 Redirect-sicheren SSRF-Schutz implementieren

**Scope:** `app/api/link-preview/route.ts`, gemeinsamer URL-Security-Helper.

**Status (goal-loop iteration 5):** Implemented for link previews and the
browser request guard. Redirects are followed manually with validation at
every hop, DNS results are checked completely, and response bodies are read
with a hard byte limit. Regression coverage is in `tests/url-security.test.ts`.

**Änderungen:**

- `fetch(..., { redirect: "manual" })`.
- Maximal 3–5 Redirects; jeden Hop neu validieren.
- DNS A/AAAA vollständig prüfen; Loopback, Private, Link-local, CGNAT, Multicast, Metadata-Endpunkte und IPv4-mapped IPv6 sperren.
- Verbindungen nach Möglichkeit an validierte IP binden oder sicheren Egress Proxy verwenden.
- Content-Type prüfen und Body während des Lesens begrenzen, nicht erst nach `response.text()`.
- Link-Preview und Browser verwenden denselben getesteten Policy-Kern.

**Tests:** 30x auf `127.0.0.1`, `169.254.169.254`, IPv6, encoded IP, Rebinding, Redirectloop, großer Body.

### P0.3 Legacy-Authentifizierung fail-closed machen

**Scope:** `lib/auth.ts`, Deployment-Konfiguration.

**Änderungen:**

- `x-chat-password` als allgemeine Benutzeranmeldung deaktivieren.
- Falls Migrationskompatibilität zwingend ist: nur über separaten internen Listener/mTLS und fest gebundenen Migrationsaccount; `x-chat-username` ignorieren.
- Proxy-Trust explizit konfigurieren; Forwarded Header nur vom bekannten Proxy akzeptieren.
- Session-Revoke/Rotation und Cleanup ergänzen.
- Gemeinsames Rate Limit in Redis/DB mit IP- und Account-Schlüssel.

**Tests:** Globalpasswort + anderer Username, gespoofte Forwarded-IP, abgelaufene/revokte Session, Cross-User.

### P0.4 Tenant Ownership schließen

**Scope:** Chats, Jobs, Events, Attachments, Browser, Subagents, Automationen, Remote Clients.

**Änderungen:**

- neue Owner-Felder `NOT NULL`;
- Legacy-`NULL`-Rows in expliziter, auditierter Migration einem bekannten Owner zuordnen oder quarantänisieren;
- `OR user_id IS NULL` aus authentifizierten Queries entfernen;
- alle Reads/Writes mit Owner-Join bzw. Composite Key;
- DB-Constraints/FKs;
- negative Route- und Repository-Tests für zwei Benutzer.

**Rollout:** erst Shadow-Queries/Telemetry, dann Migration, dann Feature Flag auf strict, zuletzt alten Fallback entfernen.

### P0.5 CapabilityManifest einführen

**Neue Komponente:** `lib/capabilities/*`.

**Status (goal-loop iteration 6):** The internal gateway now uses an explicit
tool-category catalog for discovery and execution. Name-based regex inference
was removed; unknown tools are denied under a manifest unless an exact tool
override grants them. Manifest categories and boolean overrides are validated
before discovery or execution. Focused tests verify that all built-in tools are
classified, memory and plan tools remain isolated, and malformed categories
fail closed.

**Status (goal-loop iteration 7):** Capability manifests are now persisted in
SQLite by `(owner_id, run_id, attempt_id)` with an immutable hash check. The
same manifest is reused when an MCP context is reconstructed, and the gateway
validates optional server/tool-scoped child-MCP grants during server listing,
tool discovery, schema listing, and invocation. The child-grant field is
currently opt-in so existing manifests preserve configured MCP behavior; the
remaining rollout step is to compile grants from the configured registry and
mode before making the field mandatory and fail-closed.

**Status (goal-loop iteration 8):** The rollout is now fail-closed at the
manifest boundary: every newly compiled manifest contains an explicit
`childMcpGrants` map, the gateway rejects legacy manifests without it, and
child-MCP discovery/invocation deny servers and tools absent from that map.
Child-MCP connection and in-flight connection caches are also scoped by
`owner_id` so owner-specific registry entries cannot reuse another owner's
live connection. Existing call sites default to an empty map until configured
server/tool grants are compiled from installation policy.

**Manifestinput:** User/Rolle, Mode, Workspace, Provider/Model, Automation, Incognito, Remote Client, Run/Attempt.

**Änderungen:**

- Toolkatalog vor Providerstart filtern.
- Keine verbotenen Toolnamen/-schemas in Model-Context.
- Gateway prüft exakt dasselbe Manifest anhand von `policy_version` und Hash.
- Child-MCP Discovery liefert nur server/tool-scoped Grants.
- Toolkategorie ist Metadatum, keine Namensregex.
- `call_mcp_tool` darf nur explizit freigegebene Server/Tools aufrufen.
- Manifest pro Attempt persistieren.

**Initiale Rollen:**

| Rolle | Grants |
|---|---|
| Coding Reader | repo/fs read, diff |
| Coding Executor | Reader + patch + Tests; Shell separat |
| Research | Web/Papers/Notes; keine Shell |
| Browser Reader | Navigate/Snapshot |
| Browser Actor | Reader + mutierende Browseraktionen, Approval nach Policy |
| Reviewer | Diff/Tests/Findings; kein Remote/Browser-Write |
| Automation | deklarierte non-interaktive Grants |
| Remote Executor | exakt ein Client und Actionscope |

### P0.6 Disposable Sandbox für Executor

**Änderungen:**

- Rootless Container/MicroVM pro Run oder pro isolierter Run-Gruppe.
- Nur Workspace/Artifacts explizit mounten; Secrets und Browserprofile nicht mounten.
- Read-only Rootfs, tmpfs, UID-Mapping, Prozess-/CPU-/RAM-/Disk-/Zeitlimits.
- Standardmäßig kein Netzwerk; per Capability Egress-Allowlist.
- Providerzugang über Secret Broker/Proxy mit kurzlebigem Token.
- Shell und Child-MCP in derselben Policy-Domain.
- Sandbox-ID im Attempt und Trace.

**Migration:** zuerst Coding Runs hinter Flag, dann Research/Automation; Hostpfad als kontrollierter Fallback bis Parität erreicht ist.

## 4. P1 – kanonische Orchestrierung und Zuverlässigkeit

### P1.1 Einen gemeinsamen Run-Service schaffen

`POST /api/chat` und `POST /api/runs` rufen denselben `RunService.start()` auf. Dieser besitzt Auth, Idempotenz, Messageinsert, Runinsert und Outboxevent.

**DB-Invariante:** partieller Unique Index auf aktiven Run pro Chat.

### P1.2 Run-State-Machine

**Tabellen/Typen:**

- `runs(id, owner_id, chat_id, status, version, current_attempt_id, idempotency_key, ...)`
- `run_attempts(id, run_id, provider, model, context_snapshot_id, sandbox_id, ...)`
- `run_leases(run_id, worker_id, token, expires_at)`
- `run_dependencies(parent_run_id, child_run_id, state)`

Transitions erfolgen ausschließlich über compare-and-set:
`UPDATE runs SET status=?, version=version+1 WHERE id=? AND version=? AND status IN (...)`.

Chat-`runStatus` wird Projection und darf nicht mehr Runtime-Source-of-Truth sein.

**Status (goal-loop iteration 9):** The existing durable job path now has an
explicit transition table and a monotonic per-job revision embedded in the
stored job record. Claims, normal updates, heartbeats, requeues, and model
handoffs use revision-aware writes; callers can supply an expected revision
and stale writers are rejected. This is an incremental guard for the current
`jobs` aggregate, not yet the separate normalized `runs`/`run_attempts` schema
described above.

**Status (goal-loop iteration 10):** An additive `job_leases` table now issues
per-claim worker IDs, fencing tokens, and expirations. The token is passed only
to the isolated worker process; worker updates and heartbeats are rejected when
the lease is stale, terminal/switching transitions release the lease, and the
worker requeues expired running leases from the durable checkpoint. This
reduces late-writer risk in the current aggregate while normalized
`runs`/`run_attempts` records and full lease ownership remain future work.

### P1.3 Durable Events + Live Fanout trennen

- Append-only `run_events(run_id, seq, type, payload_ref, created_at)`.
- Unique `(run_id, seq)`.
- PostgreSQL-Outbox oder zunächst SQLite-Outbox.
- Große Payloads in Object Storage/Artifacts.
- Live-Events über Redis Streams/PubSub/WebSocket; Verlust dort ist akzeptabel.
- SSE bekommt Cursor und liest bei Reconnect durable Events nach.
- Keine globale „neueste 10.000“-Löschung.

**Status (goal-loop iteration 11):** `appendRunEvent` now applies the same
active-worker-lease check before inserting durable events. Late events from a
stale or cancelled isolated child are dropped with a structured
`dropped: "stale_lease"` result, while control-plane recovery events without a
worker token remain allowed. This closes the durable event path incrementally;
chat projections, tool receipts, and sequence-level event invariants remain.

**Status (goal-loop iteration 12):** Direct worker-side chat projections now
validate the child job lease before `createChat`, `updateChat`, `appendMessage`,
and `upsertMessage`. A stale child cannot create or mutate chat transcript
state through these local persistence paths. HTTP control-plane routes remain
separate and require their own run-aware fencing before they can be considered
fully covered.

**Status (goal-loop iteration 13):** The internal chat and workspace mutation
routes now require a valid worker ID/token pair bound to the supplied job ID.
The MCP gateway propagates those headers for the targeted chat/workspace calls,
and invalid or expired leases return HTTP 409 before persistence. Internal
file, notes, browser, question, subagent, automation, and memory routes remain
outside this slice.

### P1.4 Tool-Receipts und Exactly-once-Näherung

Für jede mutierende Toolaktion:

1. Call mit stabiler `idempotency_key`;
2. `tool_call_started` durable;
3. Executor/Tool prüft bereits vorhandenes Receipt;
4. Side Effect;
5. Receipt mit Resultathash;
6. `tool_call_finished`.

Nicht-idempotente Drittanbieter ohne Idempotency Support werden bei unklarem Crashzustand `requires_review`, nicht blind retried.

### P1.5 End-to-End-Cancellation

- `cancellation_requested_at` separat vom Terminalstatus;
- AbortSignal API → Orchestrator → Provider → MCP → Tool → Browser/Remote;
- Kindprozessgruppe nach Grace Deadline terminieren;
- Lease/Fencing verhindert Late Writes;
- terminale Runs akzeptieren keine Text-/Tool-Events mehr;
- Cancel von Parent propagiert als Dependency-Operation.

### P1.6 Scheduler/Subagent-Ressourcenmodell

- Leases mit Worker-ID, Expiry und Fencing Token;
- periodischer Reaper, nicht nur Startup-Recovery;
- separate Pools/Quotas für interactive, background, browser, subagent;
- Parent-Wait verbraucht keinen Executor-Slot;
- keine 30-Minuten-HTTP-Anfrage; Child Completion als durable Event/Dependency;
- Unique Key für Reviewer;
- Taskhash enthält normalisierten Titel + Prompt + Manifest;
- Workspace-Locks oder getrennte Worktrees und expliziter Merge/Review.

### P1.7 ProviderAdapter normalisieren

```ts
interface ProviderAdapter {
  probeCapabilities(): Promise<CapabilityEvidence>;
  start(input: RunAttemptInput): AsyncIterable<CanonicalProviderEvent>;
  cancel(attemptId: string, deadline: Date): Promise<void>;
  resume(checkpoint: ProviderCheckpoint): AsyncIterable<CanonicalProviderEvent>;
  close(): Promise<void>;
}
```

Kanonische Events: `started`, `text_delta`, `reasoning_delta`, `tool_call_started`, `tool_call_finished`, `usage`, `rate_limit`, `error`, `finished`.

- Codex/Antigravity: Dispatcher entweder wirklich auf SDK/CLI umstellen oder Registry ehrlich auf OAuth-AI-SDK ändern.
- Tote `runCodex`-/`runAntigravity`-Pfade entfernen, sobald Migration belegt ist.
- „verified“ nur nach echter Probe mit Evidenz/Timestamp.
- Retry nur für klassifizierte transiente Fehler, mit Jitter und Idempotenz.
- Circuit Breaker und per Provider Quota.

### P1.8 SQLite stabilisieren, bevor migriert wird

- Heartbeat/status nur typed Columns;
- inkrementelle Message-/Part-/Eventwrites;
- kein Voll-Chat-Checkpoint pro Delta/Tool;
- ein asynchroner, gebatchter Eventwriter;
- keine `Atomics.wait`-Backoffs in API-Eventloops;
- Lock Wait, Transaction Duration, WAL Size, Busy Count messen;
- Queuezählungen/JSON-Extraktion aus Enqueue entfernen oder indexieren;
- Active-Run-Constraint in DB.

Go/No-Go nach Lasttest. Bleiben p95/Recovery außerhalb SLO, beginnt P2-Postgresmigration.

### P1.9 Frontend entkoppeln

Aus `app-shell.tsx` extrahieren:

- Chat Repository/Query Cache;
- Run Store/Reducer;
- Composer State Machine;
- Browser Store;
- Navigation;
- Subagent/Automation Projections.

SSE ist Primärkanal, Snapshotfetch Recovery. Polling nur als Health-Fallback mit Backoff/Jitter. Optimistische Updates erhalten stabile IDs und werden durch Eventsequenzen reconciled.

### P1.10 Release-/Build-Hygiene

- den einen Lintfehler und 17 Warnungen beheben/entscheiden;
- alle 41 Testdateien bewusst in Scripts klassifizieren: unit, integration, live;
- `.next*`, `node_modules`, `data`, Provider-Sessions und Browserprofile aus Buildkontext;
- `@cursor/sdk` und sonstige Floating-Versionen pinnen;
- ignoriertes pnpm-Feld in aktuelle Config migrieren;
- Clean Checkout CI: install frozen → lint → typecheck → tests → build;
- SBOM, Dependency Scan und Migrations-Dry-Run.

## 5. P2 – Datenplattform, Workflows und Qualität

### P2.1 Conversation Normalization

Tabellen: `messages`, `message_parts`, `tool_calls`, `tool_results`, `attachments`, `chat_projections`. Dual-write hinter Flag, Backfill, Read-Compare, Cutover, später JSON-Felder auslaufen lassen.

### P2.2 PostgreSQL + Queue + Object Storage

**Reihenfolge:**

1. Postgres Schema und Outbox;
2. dual-write Runs/Events;
3. Read-Compare;
4. Queue/Leases;
5. Messages;
6. Artifacts/Object Storage;
7. SQLite read-only Fallback/Export;
8. erst nach Restore-/Rollback-Probe Altpfad entfernen.

Redis ist für Leases, Rate Limits und Live-Fanout; canonical state bleibt Postgres.

### P2.3 Context Compiler

Budgetierte Segmente: Instructions, Recent Messages, Summary, Toolschemas, Toolresults, Memories/Notes, Attachments und Provideroverhead. Persistiert werden Inputs, Tokenestimate, Kürzungen, Summary-Version und Hash. Tools werden lazy nach Taskklasse/Manifest geladen.

### P2.4 Durable Workflow Graph

Planner, Executor, Approval, Wait, Reviewer und Synthesis als explizite Nodes. Deterministische Schritte bleiben Code; nur unsichere Entscheidungen gehen ans Modell. Jeder Node ist checkpointbar und replaybar. Automations-UI-Graph wird erst dann ausführbar, wenn Typen, Versionierung und Migration definiert sind.

### P2.5 Observability/Evaluation

OpenTelemetry-Spans über `run_id`, `attempt_id`, `tool_call_id`, Provider Request ID, Sandbox und DB-Transaction.

Dashboards:

- Queuewait und Laufzeit;
- SQLite/Postgres Lockwait;
- Providerlatency/429/Fehler;
- Toollatenz/Timeout/Approval;
- Contextgröße/Compaction;
- Kosten/Tokens;
- Cancel-/Recovery-Latenz;
- Duplicate/Rejected Late Events.

Trajectory-Evals prüfen nicht nur „Tests grün“, sondern Exploration, Side Effects, Verifikation, Policy und Wiederholungen.

## 6. Testprogramm als eigener Deliverable

### Testprojekte

| Suite | Inhalt |
|---|---|
| `architecture` | Dependency-Regeln, keine direkten Chatstatuswrites, Manifestpflicht |
| `lifecycle` | alle Zustände, Resume, Cancel, Crashboundary |
| `tools` | Schema, Capability, Approval, Receipt, Timeout |
| `providers` | Contract Fixtures je Adapter |
| `security` | SSRF, Tenant, Prompt Injection, Secrets, Sandbox |
| `stress` | 20 Runs, gleiche Chats, Multiworker, Eventflood |
| `browser-live` | isolierte Browserinstanz und Testaccounts |
| `migration` | expand/contract, Backfill, Rollback, Restore |

### Pflichtsimulationen

- 20 parallele Agent Runs mit Fakeprovider;
- 20 Sends auf einen Chat;
- Toolfehler vor/nach Side Effect;
- Provider-Timeout und 429;
- Worker SIGKILL an jeder Boundary;
- Netzwerkverlust im Stream und MCP;
- forcierter Datenbank-Lock;
- Parent mit 8 Kindern bei knappen Slots;
- Browser-Action-Timeout/Frame-Backpressure;
- SSE Disconnect/Reconnect.

Jede Simulation prüft DB-Invarianten, Eventsequenz, Side-Effect-Count, Terminalstatus und offene Prozesse/Handles.

## 7. Lieferreihenfolge

| Welle | Umfang | Abhängigkeit | Releasekriterium |
|---|---|---|---|
| 0 | Baseline, Branch, Backup/Restore, Metriken, Testklassifikation | keine | reproduzierbarer Clean Build |
| 1 | P0.1–P0.3: Approval, SSRF, Legacy Auth | Welle 0 | Securitytests grün |
| 2 | P0.4–P0.5: Ownership, CapabilityManifest | Welle 1 | Cross-Tenant 0; versteckte Tools 0 |
| 3 | P0.6: Sandbox | Welle 2 | Escape/Egress-Suite grün |
| 4 | RunService + State Machine + Leases | Welle 2 | Lifecycle/Fault-Suite grün |
| 5 | Durable Events + Receipts + Cancel | Welle 4 | Replay/Crash SLOs |
| 6 | Provideradapter + Subagent Scheduler | Welle 5 | Contract-/Fanout-Suite |
| 7 | Frontend/Release Hygiene | parallel ab Welle 4 | UI E2E + Clean CI |
| 8 | Normalisierung/Postgres/Object Storage | Messwerte nach Welle 5 | Dual-read parity + Rollback |
| 9 | Workflow Graph/Context/Evals | stabile Datenbasis | Benchmark + Kosten/SLO |

## 8. Nicht tun

- keinen sofortigen Komplettrewrite;
- SQLite nicht ohne Messwerte einfach durch fünf neue Systeme ersetzen;
- keine Approval als Promptregel oder Boolean;
- keinen vollständigen Toolkatalog laden und nur auf Call-Zeit hoffen;
- Providerfähigkeiten nicht aus Registrydeklarationen als „verified“ ausgeben;
- keine Crash-Retries für unklare Side Effects;
- keine Produktionsdaten für Stress-/Browser-/Faulttests;
- alte JSON-Felder erst nach Dual-Read-Parität und Rollbackfenster entfernen.

## 9. Definition of Done der Gesamtmodernisierung

Metis gilt als robuste Agent-Plattform, wenn:

1. jeder Run genau einen kanonischen, replaybaren Zustand besitzt;
2. jede riskante Aktion an ein kleines CapabilityManifest und gegebenenfalls ein echtes Approval-Receipt gebunden ist;
3. Executor, Browser und Secrets durch starke Isolation getrennt sind;
4. Crash, Timeout, Cancel und Netzwerkverlust keine doppelten Side Effects oder unklaren aktiven Runs erzeugen;
5. Provider dieselbe Event-/Error-/Cancel-Semantik erfüllen oder ihre Abweichung explizit deklarieren;
6. 20 parallele Runs die SLOs erfüllen;
7. alle Cross-Tenant- und SSRF-Negativtests grün sind;
8. ein Clean Checkout ohne Runtime-Artefakte reproduzierbar baut;
9. jede Migration restore- und rollback-getestet ist;
10. UI und Operatoren einen vollständigen, forensisch brauchbaren Run-Trace sehen.