# Telegram-Plugin-Patch: Aria-V3-Gateway (Phase E)

Prüfbarer Patch gegen das installierte Plugin `claude-plugins-official/telegram/0.0.6`. **Nichts davon ist produktiv
eingespielt.** Die Produktivdatei wurde nur gelesen.

| Reifegrad | Stand |
|---|---|
| Entwicklungsfertig | ja (Patch, Modul, Tests, Doku) |
| Integrationsgetestet | offline: echte Prod-Datei + echter Patch + echte grammy-/MCP-Bibliotheken + echte Python-Bridge, Telegram-API und MCP-stdio durch Test-Doubles ersetzt (`test/`) |
| Produktionsbereit | nein: Owner-Freigaben offen (`docs/OFFENE-ENTSCHEIDUNGEN.md` E5, E8, E26–E35), Codex nur MockRunner (E9), sequenzielle Verarbeitung in `active` (E27) |
| Produktiv aktiviert | nein |

## Dateien

| Datei | Inhalt |
|---|---|
| `server.prod-0.0.6.ts` | byte-gleiche Kopie der Produktivdatei (sha256 `a20b9d6d…`, inkl. lokalem Supabase-Patch), Herkunft in `SOURCES.md` |
| `server.ts` | gepatchte Datei: **2 Hunks, +6/−1 Zeilen** (5 neue Zeilen inkl. 2 Kommentar- und 1 Leerzeile, 1 geänderte Zeile) |
| `server.ts.patch` | `diff -u` Prod → gepatcht (vom Test gegen einen frischen Diff geprüft) |
| `aria-gateway.ts` | Gateway-Anbindung: Konfiguration, Bridge-Prozess (Protokoll v1), Antwortprüfung, Entscheidung |
| `bunfig.toml` | `install.auto = "disable"` (auch in der Repo-Wurzel und in `telegram/`): ein versehentliches `bun server.ts` mit einem dieser Verzeichnisse als Startverzeichnis scheitert am Import, bevor Code läuft (geprüft mit lokalem Registry-Stub: 0 Zugriffe). bun liest die Datei **nicht** aus Elternverzeichnissen; ein Start aus einem anderen Verzeichnis ist nicht geschützt |
| `test/` | Tests, Mock-Bridge, Mutationsproben (`run-tests.sh`, `mutationsproben.py`) |

Die alten Dateien unter `telegram/` (`server.ts` als Vollkopie mit Shadow-Hook, `shadow-python-async.ts`,
`real-handler-test/`, `telegram-handler-test/`, `fault-test/`) sind **abgelöst** und werden nicht mehr verwendet
(Aufräumen: E35). Die alte Vollkopie lädt den Token selbst und beendet per PID-Datei den Produktiv-Poller: nie starten.

## Der Patch

```diff
@@ -22,6 +22,7 @@
 import { join, extname, sep } from 'path'
+import { ariaGateway } from './aria-gateway.ts' // aria-v3-gateway (Phase E): ARIA_GATEWAY_MODE=off (Default) = Prod-Verhalten
@@ -958,9 +959,13 @@
   const imagePath = downloadImage ? await downloadImage() : undefined
 
+  // aria-v3-gateway (Phase E): einziger Einhängepunkt, nach gate()=deliver und Permission-Intercept.
+  // off: kein Aufruf. shadow: sofort true (nur Protokoll). active: false = nicht an Claude weiterleiten.
+  const ariaForward = ariaGateway.enabled ? await ariaGateway.inbound({ bot, ctx, text, imagePath, attachment }) : true
+
   // image_path goes in meta only — ...
-  mcp.notification({
+  if (ariaForward) mcp.notification({
```

Alles andere (Token-Laden, PID-Datei, `bot.start`-Schleife, `gate()`, Pairing, Permission-Intercept, Inline-Buttons,
`/start` `/help` `/status`, Supabase-Log, Fehlerbehandlung) ist unverändert. Der Supabase-Block hängt bewusst **nicht**
an `ariaForward`: Jede zugestellte Nachricht wird wie heute protokolliert.

## Modi (`ARIA_GATEWAY_MODE`)

| Modus | Bridge-Aufruf | Weiterleitung an Claude | Antworten per Bot |
|---|---|---|---|
| `off` (Default, auch bei ungültigem Wert) | keiner, kein `await` | exakt wie Prod | keine |
| `shadow` | asynchron, `mode: "shadow"`, Handler wartet nicht | **immer unverändert** (auch bei Timeout, Absturz, Müll, Überlast) | keine |
| `active` | synchron, `mode: "execute"` | nur bei `forward_to_claude_channel` mit passender Bindung oder Router-Ausfall + `claude_status_quo` | Codex-Antwort, Sperr-/Fehlererklärung, Moduswechsel |

`active` setzt die Gateway-Antwort so um:

| `handling` | Wirkung |
|---|---|
| `forward_to_claude_channel` | bestehender `mcp.notification`-Weg mit dem **von server.ts selbst gebauten** Payload (identisch mit Prod). Vorher Abgleich: `forward.params` muss diesem Payload entsprechen (Meta exakt; Inhalt exakt oder bei `/claude <Text>` der freigegebene Rest), Clearance an `job_id`, Absender, Chat und Backend `claude` gebunden; sonst Router-Ausfall. Optionaler Hinweis (`reply_text`) wird vorher gesendet |
| `reply` | `reply_text` per `bot.api.sendMessage` (Teile ≤ 4096 Zeichen), keine Weiterleitung |
| `blocked` / `failed` | Erklärung (`reply_text` oder kurzer Standardtext), **keine** Weiterleitung |
| `drop` | nichts (nicht autorisiert, Duplikat) |
| `not_intercepted` | kurzer Hinweis, keine Weiterleitung (nur Randfälle, siehe unten) |
| `error`, Prozessfehler, ungültige Antwort, Shadow-Antwort auf `execute` | Router-Ausfall → `ARIA_GATEWAY_ON_FAILURE` |

**Verweigerungen kippen nie:** `reply`, `blocked`, `failed`, `drop`, `not_intercepted` führen nie zur Weiterleitung,
auch nicht über die Ausfallregel, auch nicht, wenn das Senden der Erklärung scheitert, und auch nicht, wenn eine
sonst ungültige Antwort erkennbar eine Verweigerung enthält (`handling` oder `planned_handling`). Tests:
`decideActive: verweigerte Freigaben …`, `active: Zusatzfälle > verweigerte Freigaben …`; Mutationsproben T03, T04.

Router-Ausfall (`ARIA_GATEWAY_ON_FAILURE`, nur `active`): `claude_status_quo` (Default) = heutiger Weg an Claude;
`block` = nicht weiterleiten, kurze Störungsmeldung. Ungültiger Wert → `block`. Owner-Entscheidung E5.

Duplikate: `active` verarbeitet je `(chat_id, message_id)` höchstens einmal pro Prozess (auch bei Router-Ausfall);
dauerhaft über Neustarts entscheidet die Python-Idempotenz. `off`/`shadow` leiten Duplikate wie Prod zweimal weiter.

## Umgebungsvariablen

| Variable | Default | Bedeutung |
|---|---|---|
| `ARIA_GATEWAY_MODE` | `off` | `off`, `shadow`, `active`; jeder andere Wert = `off` (+ einmalige Meldung) |
| `ARIA_GATEWAY_ON_FAILURE` | `claude_status_quo` | `claude_status_quo` oder `block`; ungültig = `block` |
| `ARIA_GATEWAY_BRIDGE` | – (Pflicht für shadow/active) | absoluter Pfad zu `python/bin/aria_bridge.py`; fehlt er, ist jede Nachricht ein Router-Ausfall |
| `ARIA_GATEWAY_CONFIG` | – (Pflicht) | absoluter Pfad zur Gateway-Konfiguration (`allowed_modes` muss `shadow` bzw. `execute` enthalten) |
| `ARIA_GATEWAY_PYTHON` | `/usr/bin/python3` | absoluter Interpreterpfad |
| `ARIA_GATEWAY_TIMEOUT_MS` | shadow 3000, active 15000 | 100 … 600000; in `active` blockiert der Aufruf die Update-Schleife (E27) |

Gelesen wird bei jeder Nachricht aus `process.env` (also nach dem `.env`-Laden des Plugins). Eine Änderung wirkt erst
nach einem Neustart des Plugin-Prozesses, weil sich die Umgebung eines laufenden Prozesses nicht von außen ändert.

Bridge-Aufruf: `<PYTHON> -I <BRIDGE> --config <CONFIG>`, `cwd /`, Umgebung nur `PATH=/usr/bin:/bin`, eigene
Prozessgruppe (bei Timeout/Limit `SIGKILL` an die Gruppe), stdout ≤ 1 MiB, stderr ≤ 64 KiB (verworfen, nie
protokolliert), Anfrage ≤ 1 MiB, höchstens 3 Prozesse gleichzeitig (shadow: Überlauf wird übersprungen und
protokolliert; active: Warteschlange bis 32, begrenzt durch die Frist).

Protokoll (stderr des Plugins, eine Zeile je Nachricht, **ohne Nachrichtentext**):
`aria-gateway: {"v":1,"phase":"decision","mode":…,"sender_id":…,"chat_id":…,"chat_type":…,"message_id":…,"handling":…,"planned_handling":…,"backend":…,"code":…,"job_id":…,"outcome":…,"forwarded":…}`.

## Wer sieht welche Nachricht (geprüft mit echtem grammy 1.41.1)

1. `bot.command('start'|'help'|'status')` ist vor `bot.on('message:*')` registriert und ruft `next()` nicht auf.
   grammy matcht nur `entities` einer **Textnachricht** mit `bot_command` an Offset 0, auch `/status@aria_test_bot`.
   Diese Nachrichten erreichen `handleInbound` nie (Tests: `/start, /help, /status … erreichen das Gateway nie`, Spur
   `/status@aria_test_bot`). In Gruppen verwirft `dmCommandGate` sie still (Prod-Verhalten).
   **Nicht** konsumiert: Captions (`Foto mit Caption /help` geht an Claude) und `/start@anderer_bot`.
2. `callback_query:data` (Berechtigungs-Buttons) läuft in einem eigenen Handler, nie über das Gateway.
3. `handleInbound`: `gate()` (Allowlist, Pairing, Gruppen) → `drop`/`pair` → Permission-Intercept
   (`PERMISSION_REPLY_RE`) → Typing/Ack → Foto-Download → **Gateway** → Claude-Notification → Supabase.
4. `/auto`, `/codex`, `/claude` sind **nicht** bei grammy registriert. Sie erreichen `handleInbound` wie heute,
   also nur nach `gate()` (Allowlist bleibt maßgeblich, Reihenfolge unverändert). Erkannt werden sie ausschließlich
   vom Python-Gateway (einziger Befehlsparser `commands.py`, Modusbefehle nur für Besitzer, Modus je Chat in
   `modes.json` bzw. `shadow-modes.json`). Nur `active` fängt sie ab und antwortet per Bot; `shadow` protokolliert die
   geplante Antwort und leitet wie heute an Claude weiter; `off` = Prod (Text an Claude).

Konflikte und Randfälle (bewertet in `docs/OFFENE-ENTSCHEIDUNGEN.md`):

- Kein Namenskonflikt mit `/start` `/help` `/status`; das Telegram-Befehlsmenü (`setMyCommands`) bleibt bei diesen drei (E32).
- Heute erhält Claude `/codex …` als Text; in `active` sieht Claude Modusbefehle nicht mehr.
- Semantische Verwechslungsgefahr mit `/mode auto` (Autonomie-Stufe der Claude-Sitzung, Skill `aria-mode-toggle`):
  `/auto` ist die Modellwahl des Gateways; `/mode auto` bleibt eine normale Nachricht an Claude.
- Caption `/help`, `/status@anderer_bot` usw.: Python antwortet `not_intercepted`; `active` leitet nicht weiter und
  sendet einen Hinweis (Prod würde weiterleiten). `/codex@anderer_bot` gilt in Python als eigener Befehl, weil das
  Protokoll keinen Bot-Namen überträgt (E31).
- Ein Foto mit Caption `/codex` setzt in `active` den Modus; das Foto wird nicht verarbeitet (Python-Semantik, E31).

## Einbau und Rollback

Nur als Plan, Schritte in `docs/MIGRATION.md` und `docs/ROLLBACK.md`. Kurz: Backup der Prod-Datei, **zuerst**
`aria-gateway.ts` in das Plugin-Verzeichnis, dann `server.ts` (oder `patch -p1 < server.ts.patch`), Start mit
`ARIA_GATEWAY_MODE=off`, Neustart, Prüfungen; dann `shadow`, später `active`. Rollback: `ARIA_GATEWAY_MODE=off` (oder
Variable entfernen) + Neustart; vollständig: Prod-Datei zurück (sha256 `a20b9d6d…`) + Neustart.
**Plugin-Updates überschreiben den Patch** (wie den Supabase-Patch): nach jedem Update Marker `aria-v3-gateway` prüfen (E26).

## Tests

```
telegram/plugin/test/run-tests.sh            # alle 8 Testläufe, je eigener bun-Prozess, env -i
ARIA_TEST_KEEP=1 telegram/plugin/test/run-tests.sh modes.test.ts   # einzelne Datei, Arbeitsverzeichnis behalten
python3 -I telegram/plugin/test/mutationsproben.py <ergebnis.json> [T01 …]   # Mutationsproben (sauberer Worktree)
```

Ergebnisse und Zahlen: `docs/TESTERGEBNISSE.md`, Mutationsproben: `docs/MUTATIONSPROBEN.md`.

## Bekannte Grenzen

- `active` wartet im Update-Handler; grammy (`bot.start`) verarbeitet Updates **sequenziell**. Ein langer
  Gateway-Aufruf verzögert alle folgenden Updates, auch Berechtigungsantworten (Test
  `dokumentierte Einschränkung …`). Vor echter Codex-Ausführung muss die Zustellung asynchron werden (E27).
- `delivery_report` an das Gateway wird noch nicht gesendet (E29); die Clearance-MAC wird im Plugin nicht geprüft (E15).
- Codex-Antworten erscheinen weder bei Claude noch im Supabase-Log (E10); gesperrte Nachrichten landen weiter im
  Supabase-Log (E28).
- Typprüfung: `aria-gateway.ts` ist mit `deno check` (offline) fehlerfrei; `server.ts` ist offline nicht vollständig
  prüfbar (`npm:@types/node` fehlt; auch die unveränderte Prod-Datei meldet ohne Node-Typen 4 Umgebungsfehler).
- Der stdin-Fehlerpfad der Bridge ist unter bun 1.3.13 nicht beobachtbar (bun meldet kein `error`), Mutationsprobe T16.
