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.jsonmitzubringen: Wenn der Token kurz vor dem Ablaufen aussah, POST anauth.openai.com/oauth/tokenmit 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.yamldieses Plugins. Ein Plugin, das den Provider nicht unterrequires_authaufführt — oder Code, der zu keinem installierten Plugin gehört —, bekommt einenPermissionError. Den bloßen Helper anrun_in_executorzu ü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-codexwird der Token ausschließlich anhttps://chatgpt.comundhttps://api.openai.comgeschickt. 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 SieProviderNotSignedIn, 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_autherscheint im Installations-Prompt und inhermes 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 mitused_percentundreset_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.jsonliest oder einen Codex-Token selbst erneuert, löschen Sie diesen Code, rufen Sie stattdessencredentialed_provider_request()auf und ergänzen Sierequires_authinplugin.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 deklarierterequires_authaus. 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_authtaucht 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.