Die Spec folgt dem Code
Ich wollte ein Protokoll für SP-übergreifenden Datenzugriff schreiben und dann dagegen bauen. Es kam umgekehrt: erst über fünf Service Provider implementiert und auditiert, dann §4/§5/§6 an das angeglichen, was sich als richtig erwiesen hat. Der neue IdP-Mechanismus, den die Spec annahm, war schon da — nur anders geformt.
Ich hatte ein Protokoll geschrieben. SP Data Access Profile: wie ein Service Provider Daten von einem anderen abruft, im Namen eines Menschen, über delegierte Grants. DDISA-Trust-Doktrin, Scope-Katalog, ein Abschnitt zu delegation, einer zu standing, einer zu consume. Sauber nummeriert, §1 bis §6. Der Plan war: Spec steht, jetzt bauen wir dagegen.
Das Bauen hat die Spec umgeschrieben. Nicht weil ich schlampig spezifiziert hätte, sondern weil drei Annahmen beim ersten echten Durchlauf gekippt sind. Am Ende habe ich §4, §5 und §6 nachträglich auf das reconciled, was die Implementierung schon bewiesen hatte. Kein neuer IdP-Mechanismus. Die Spec ist dem Code hinterhergelaufen, und das war hier nicht das Versäumnis — es war der ehrlichere Weg.
Was die Spec angenommen hat
Die Spec ging davon aus, dass für delegierten Cross-SP-Zugriff ein neuer Mechanismus im Identity-Provider nötig ist. §4 beschrieb, wie eine Delegation am IdP entsteht. §5 beschrieb, wie der Ziel-SP die delegierten Scopes prüft — offline, aus dem Token selbst, weil das Token die Scopes inline trägt. §6 beschrieb consume und revoke als etwas, das ich noch bauen müsste.
Drei Annahmen, alle plausibel auf dem Papier. Alle drei haben den ersten Smoke nicht überlebt.
Was die Implementierung gezeigt hat
Derselbe Verstoß, in jedem SP
Bevor irgendein delegierter Pfad funktionieren konnte, musste die Issuer-Auflösung stimmen. Jeder SP verifiziert eingehende Tokens gegen einen Issuer. In allen fünf SPs — timetrack, tasks, plans, preview, chat — stand der Issuer hartkodiert im Code:
// vorher, in jedem SP dieselbe Zeile
const ISSUER = "https://id.openape.ai";
verifyJWT(token, { issuer: ISSUER });
Das ist gegen die eigene DDISA-Doktrin. Welcher IdP für ein Subject zuständig ist, steht im _ddisa-Record der Subject-Domain — nicht in einer Konstante im SP. Der Fix:
// nachher: Issuer aus dem _ddisa-TXT-Record der Subject-Domain,
// nicht aus einer Konstante
verifyJWT(token, { issuer: issuerFromSubjectDdisa });
Behavior-preserving, weil hofmann.eco per _ddisa ohnehin auf id.openape.ai zeigt — der Effekt ist heute identisch. Aber der Verstoß war uniform. Fünf SPs, dieselbe falsche Zeile — kopiert, nicht je neu entschieden. Das ist genau die Sorte Befund, die ein Audit liefert und eine Spec nicht: nicht was soll gelten, sondern was gilt tatsächlich überall gleich falsch. Das wurde §2.1.
Der Mechanismus war schon da
§4 und §6 beschrieben, was ich im free-idp noch bauen müsste — Delegation, Standing Grants, Consume, Revoke. Ich habe vor dem Umbau ein Audit gemacht, bewusst, weil ein Eingriff in den produktiven IdP einen großen Blast-Radius hat. Das Audit hat gezeigt: das ist alles schon vollständig da, im IdP-Auth-Code und in @openape/grants. Delegation, Standing, Consume, Revoke — fertig, getestet, produktiv.
Das M5-Risiko, das ich im Plan als „riskanter IdP-Umbau" stehen hatte, war unbegründet. M5 war nicht Umbau. M5 war Doku — die Spec an einen Mechanismus angleichen, der schon stand. Die eigentliche Arbeit lag SP-seitig in M4. Hätte ich spec-first weitergemacht, hätte ich einen IdP-Mechanismus gebaut, der schon existierte, nur anders geformt als meine Spec ihn sich ausgedacht hatte.
Das Delegations-Token sieht anders aus als gedacht
§5 nahm an, der Ziel-SP könne delegierte Scopes offline prüfen, weil sie inline im Token stehen. Echtes End-to-End hat das widerlegt: das Delegation-AuthZ-JWT trägt keine inline-Scopes. Es trägt eine grant_id-Referenz, und das aud ist der Ziel-SP, nicht apes-cli. Die Offline-Scope-Annahme der Spec hält nicht — der Grant muss dereferenziert werden, die Scopes leben nicht im Token.
Das hätte das Audit nicht gefunden. Audit-first hat den unnötigen IdP-Umbau verhindert; aber dass die Token-Form nicht zur Spec passt, zeigt sich erst, wenn ein echter Request durch alle Schichten geht. Zwei verschiedene Werkzeuge, zwei verschiedene Klassen von Fehlern.
Wie §5 jetzt aussieht
Statt offline-Scope-Validierung beschreibt §5 jetzt einen Chokepoint: jede Methode mappt auf einen Scope, die Subset-Prüfung passiert beim Token-Exchange, das Token hat eine kurze TTL. Der SP fragt nicht das Token „welche Scopes hast du", er fragt „darf diese Methode mit diesem Grant". Das ist nicht das, was ich in die Spec geschrieben hatte. Es ist das, was sich beim Bauen als die richtige Stelle für die Grenze erwiesen hat.
Dieselbe Logik landete in jedem SP als dieselbe Folge von Commits: Issuer aus DDISA auflösen (§2.1), Scope-Katalog in /.well-known/openape.json deklarieren (§3), delegierte Scopes am Chokepoint erzwingen (§5). Fünfmal dieselbe Reihenfolge, weil die Spec am Ende beschrieb, was die fünf SPs schon taten.
Was weggefallen ist
Der Abschnitt „neuer IdP-Mechanismus". Es gibt keinen. §4 und §6 referenzieren jetzt delegation, standing und consume, wie sie real funktionieren, statt einen Mechanismus zu beschreiben, den ich gebaut hätte, wenn ich nicht vorher nachgesehen hätte.
Die Offline-Scope-Annahme aus §5. Gestrichen, ersetzt durch den Chokepoint.
Der Commit, der das festhält, sagt es nüchtern: reconcile SP Data Access §4/§5/§6 with implemented delegation/standing/consume (no new IdP mechanism). Das „no new IdP mechanism" ist die ganze Pointe in vier Wörtern.
Spec-first ist die Predigt
Jeder predigt spec-first. Spec steht, dann baust du dagegen, der Code kann nicht von der Wahrheit abweichen, weil die Spec die Wahrheit ist. Das ist eine gute Disziplin für Schnittstellen, an denen mehrere Parteien gleichzeitig bauen und niemand den anderen brechen darf.
Hier hat genau eine Partei gebaut, und die Spec war eine Hypothese darüber, wie ein System funktionieren würde, das es noch nicht gab. Eine Hypothese, die an drei Stellen falsch war: einmal, weil ein Mechanismus schon existierte; einmal, weil ein Verstoß uniform durch fünf Codebasen lief; einmal, weil die Token-Form nicht zur Annahme passte. Hätte ich die Spec als Wahrheit behandelt, hätte ich gegen drei falsche Annahmen gebaut.
Eine Spec, die beschreibt, was nachweislich läuft, ist vertrauenswürdiger als eine Spec, die vorschreibt, was laufen soll. Die erste ist ein Protokoll von Tatsachen. Die zweite ist eine Wette. Ich habe nicht geplant, die Spec dem Code folgen zu lassen — ich habe geplant, es andersrum zu machen. Aber wenn der Code dir zeigt, dass deine Spec an drei Stellen falsch ist, dann ist die Spec zu korrigieren keine Niederlage. Es ist das Einzige, was die Spec überhaupt noch wert macht.
Code: github.com/openape-ai/openape, MIT-lizenziert. Das SP Data Access Profile lebt im protocol-Repo, der Issuer-Fix (§2.1) und der Scope-Chokepoint (§5) in jedem SP einzeln.