Plugins fassen Ihren Token nie wieder an


Sie installieren ein Voice-Plugin. Es braucht Ihre ChatGPT-Anmeldung, um ein Realtime-Session-Secret auszustellen. Ein paar Tage lang funktioniert alles. Dann ist Codex eines Morgens abgemeldet. Sie melden sich neu an, und am nächsten Tag passiert es wieder. Sie schieben es auf das Netzwerk, den Browser, vielleicht Ihr Konto — alles außer dem Plugin, das schließlich nur Ihre Anmeldung „benutzt“ hat. Was wirklich passiert ist: Das Plugin und Hermes wollten beide dasselbe Credential erneuern, und Codex-Refresh-Tokens sind Einweg. Wer ihn als Zweiter ausgibt, hält einen toten Token, und die ganze Token-Familie wird mit ihm widerrufen.

Das ist kein bösartiges Plugin. Es ist der natürliche Weg für ein Plugin, das eine Anmeldung braucht, und die offizielle Dokumentation nennt dasselbe Voice-Plugin als Beispiel. PR #136117 (am 10. Oktober 2026 in main gemergt und in v0.21.7 ausgeliefert) wählte einen dritten Weg: Der Core stellt die Anfrage. Das Plugin liest, erneuert und schreibt das Credential überhaupt nicht mehr — es bekommt nur noch die Antwort.

Wie es zum Fehler kommt: ein Einweg-Token, zwei Ausgeber

Die Mechanik lohnt sich auszubuchstabieren, denn die Verantwortung ist eindeutig, sobald man sie sieht:

  • Codex-Refresh-Tokens werden durch Nutzung ungültig, das Erneuern muss also serialisiert werden. Hermes weiß das seit einer Weile und erneuert unter einem Lock — die Regelliste für diese Provider-Klasse ist _SINGLE_USE_REFRESH_PROVIDERS.
  • Ein Plugin, das die ChatGPT-Anmeldung braucht, neigte früher dazu, seinen eigenen kleinen Reader für ~/.codex/auth.json mitzubringen: Wenn der Token kurz vor dem Ablaufen aussah, POST an auth.openai.com/oauth/token mit der Client-id der Codex CLI, dann die neuen Tokens zurückschreiben.
  • Diese beiden Refresh-Implementierungen teilen kein Lock. Wenn sie gleichzeitig erneuern, wird derselbe Refresh-Token zweimal ausgegeben. Der Server wertet das als Missbrauch und invalidiert die gesamte Token-Familie.

Das Ergebnis ist das Symptom, dessen Diagnose Stunden kostet: In Ihrer Konfiguration hat sich nichts geändert, niemand hat ein Passwort vertippt, und Sie sind einfach abgemeldet. Und es kommt wieder, nachdem Sie sich neu angemeldet haben, weil der zweite Refresher des Plugins immer noch da ist.

Die neue Form: der Core stellt die Anfrage

Das neue öffentliche Modul hermes_cli/plugin_provider_requests.py bietet credentialed_provider_request(). Es ist bewusst schlicht — eine importierbare Funktion in derselben Form wie hermes_cli.urllib_security.open_credentialed_url —, damit sie sowohl aus register(ctx)-Code als auch aus Dashboard-Plugins (dashboard/plugin_api.py) funktioniert.

Zwei Schritte für Plugin-Autoren. Erstens: die Anmeldung in plugin.yaml deklarieren:

name: live-voice
requires_auth:
  - openai-codex

Dann die Anfrage an den Core übergeben und die Credential-Datei nicht mehr anfassen:

from hermes_cli.plugin_provider_requests import ProviderNotSignedIn, credentialed_provider_request

def mint_client_secret(session: dict) -> dict:
    try:
        resp = credentialed_provider_request(
            "openai-codex", "POST", "https://api.openai.com/v1/realtime/client_secrets",
            json={"session": session}, timeout=15)
    except ProviderNotSignedIn as exc:
        return {"ok": False, "error": str(exc)}  # die Meldung nennt den Anmeldebefehl
    if resp.status != 200:
        return {"ok": False, "error": f"client secret failed ({resp.status}): {resp.text[:300]}"}
    return {"ok": True, **resp.json()}

Zurück kommt eine ProviderResponse (status, headers, body, plus .text und .json()) — ohne Token darin. Der zu löschende Code ist Ihr selbstgebauter Reader und Refresher: Aus Sicht des Plugins existiert der Refresh-Schritt nicht mehr.

Vier harte Regeln

Das ist kein Allzweck-Proxy. Es trägt vier Grenzen, jede beantwortet einen echten Vorfall:

  • Nur deklarierte Plugins. Der Aufrufer wird als das Plugin-Verzeichnis identifiziert, das auf dem Call-Stack liegt; der Core liest die plugin.yaml dieses Plugins. Ein Plugin, das den Provider nicht unter requires_auth aufführt — oder Code, der zu keinem installierten Plugin gehört —, bekommt einen PermissionError. Den bloßen Helper an run_in_executor zu übergeben, hilft auch nicht: Auf dem Stack liegt kein Plugin-Frame, der Call wird also abgelehnt. Rufen Sie ihn aus Ihrer eigenen Funktion auf.
  • Nur feste Origins. Für openai-codex wird der Token ausschließlich an https://chatgpt.com und https://api.openai.com geschickt. Jede andere URL wird vor jedem Netzwerk-I/O abgelehnt. Ein Profil, dessen Codex-Credential auf ein eigenes Gateway geleitet wird, wird ebenfalls abgelehnt — dieser Schlüssel gehört dem Gateway, nicht diesem Pfad.
  • Keine Redirects. follow_redirects=False. Eine 3xx-Antwort kommt unverändert zurück, ein Authorization-Header kann also nie zu einem dritten Host gelotst werden.
  • Profilgebundene Credentials. Es löst über resolve_codex_runtime_credentials() auf, denselben gelockten, profilgebundenen und pool-bewussten Pfad, den der Chat nutzt. Ohne Anmeldung bekommen Sie ProviderNotSignedIn, dessen Meldung den Befehl nennt, den der Nutzer braucht (hermes auth add openai-codex, oder die Codex-Anmeldung in der Desktop-App).

Was sich für die Person ändert, die Plugins installiert

Ein guter Teil des Werts liegt hier in der Sichtbarkeit zum Installationszeitpunkt:

  • requires_auth erscheint im Installations-Prompt und in hermes plugins show <name>.
  • Nützlicher noch: Es nimmt am Consent-Flow teil, wenn ein Katalog-Pin aktualisiert wird. Wenn die neue Version Tools, Hooks, Python-Abhängigkeiten, Host-Fähigkeiten, eine Provider-Anmeldung (requires_auth) oder eine Desktop-UI-Hälfte hinzufügt, die die installierte Version nicht hatte, zeigt die CLI das Delta und fragt j/N, bevor sich etwas bewegt; die Update-Buttons von Desktop und Dashboard zeigen dieselbe Bestätigung. Eine Ablehnung — oder eine nicht-interaktive Session, in der niemand antworten kann — lässt das Plugin auf seinem alten Pin.

Mit anderen Worten: „Dieses Plugin hat still seinen eigenen Zugriff erweitert“ ist jetzt eine Frage, die der Update-Pfad laut stellt.

Was es nicht ist: das ist keine Sandbox

Die offizielle Dokumentation ist darin deutlich, und man sollte es Nutzern wortwörtlich wiederholen: Das ist Consent und Sichtbarkeit, keine Sandbox. Plugin-Code läuft im selben Prozess, kann also weiterhin jede Datei lesen, die Sie lesen können. Der Review-Prozess lehnt Plugins ab, die das tun, aber das ist eine Review-Einschränkung, keine Durchsetzung auf Kernel-Ebene.

Die einzigen harten Garantien sind die vier Regeln oben: Attribuierung, Origin, Redirects und der Pfad der Credential-Auflösung. Lesen Sie es als „ein Plugin kann sich beim Benutzen Ihrer Anmeldung nicht mehr als der Core ausgeben“, nicht als „das Plugin ist eingesperrt worden“.

Usage-Panels und Abmelden laufen ebenfalls über den Core

Zwei Fallen, die die Dokumentation ausdrücklich schließt:

  • Rufen Sie Usage-Endpunkte nicht selbst auf. Nutzen Sie agent.account_usage.fetch_account_usage("openai-codex", read_only=True). Es liefert einen Snapshot (Plan, Session- und Wochenfenster mit used_percent und reset_at) und erneuert oder rotiert nie ein Credential.
  • Abmelden gehört zu Hermes, nicht zu einem Plugin. Verweisen Sie Nutzer auf hermes auth logout openai-codex, statt Credential-Dateien zu löschen — sie zu löschen ist genau der Anfang dieser ganzen Geschichte.

Was jetzt zu tun ist

  • Plugin-Autoren: Wenn Ihr Plugin auth.json liest oder einen Codex-Token selbst erneuert, löschen Sie diesen Code, rufen Sie stattdessen credentialed_provider_request() auf und ergänzen Sie requires_auth in plugin.yaml. Alle, die es laufen lassen, werden von diesem Race nicht mehr abgemeldet.
  • Alle anderen: Die Fähigkeit steckt in v0.21.7 (getaggt am 11. Oktober). Um zu sehen, ob ein installiertes Plugin sie übernommen hat, gibt hermes plugins show <name> das deklarierte requires_auth aus. Zum umgebenden Sicherheitsmodell siehe unseren Plugin-Katalog-Leitfaden und den Artikel zur CI-Supply-Chain des Katalogs.

Die Kurzfassung

  • „Plugins sollten Ihren Token nicht halten“ ist keine Pedanterie; es ist die Schlussfolgerung aus einem konkreten Bug: ein Einweg-Refresh-Token, von zwei Refreshern ausgegeben, und ein Nutzer, der abgemeldet ist, ohne zu wissen warum.
  • Der Fix war nicht, Plugins einen kleineren Schlüssel zu geben. Er war, den Schlüssel wegzunehmen: Der Core stellt die Anfrage und hängt Authorization an, und das Plugin sieht nur die Antwort.
  • Die vier Regeln — deklarieren, feste Origins, keine Redirects, profilgebundene Credentials — machen daraus Mechanik statt Konvention.
  • Sichtbarkeit zählt genauso viel: requires_auth taucht bei der Installation, bei der Inspektion und in der Update-Bestätigung auf, ein Plugin kann also keine Anmeldung mehr still hinzufügen.
  • Und behalten Sie die Grenze im Kopf: Consent und Sichtbarkeit, keine Sandbox.