Der Agent kennt sich mit dem aus, was er kennt
Ich hatte Widening von Anfang an eingebaut — im Agent, client-seitig, weil ich dachte der Agent ist die Intelligenz und weiß was er als nächstes braucht. Er hat's nie benutzt. Wie ich erkannt habe, dass Widening zum User gehört, nicht zum Agent — und was daraus wurde.
Widening war mir immer klar.
Es ist eine offensichtliche Eigenschaft eines Systems, in dem jeder Command einzeln approved wird: irgendwann muss es einen Weg geben, eine Klasse von Commands in einem Schritt zu approven. Sonst sitzt man bei ls und cat die ersten zehn Minuten nur im Prompt-Fenster. Ich habe das in der ersten Version von OpenApe eingebaut. Client-seitig. Im Agent.
Das war mein Fehler, und ich brauche einen ganzen Artikel, um zu erklären warum.
Die Annahme
Die client-seitige Variante sah so aus: der Agent kann in seinem Grant-Request zusätzlich zur konkreten Aktion ein breiteres Pattern beantragen. Statt "gib mir die Erlaubnis, ls /home/patrick einmal auszuführen" hätte der Agent sagen können "gib mir die Erlaubnis, ls {path} auf allem unterhalb von /home/patrick/ zu machen, für die nächste Stunde." Ich approve einmal. Er hat was er braucht. Kein weiterer Prompt in dieser Session.
Die Annahme hinter dem Design: Der Agent ist die Intelligenz. Er sieht seine aktuelle Aufgabe, er weiß welche Commands er voraussichtlich brauchen wird, er beantragt proaktiv einen passenden Scope. Der User approved einmal, nicht dreißig Mal. Der Agent ist derjenige, der den Kontext hat — er weiß, dass er gleich ls, cat, head und vielleicht ein grep auf einem Verzeichnisbaum braucht. Niemand sonst weiß das zu diesem Zeitpunkt.
Das war auf dem Papier elegant. Es hat auch genau einmal funktioniert — in einem Testskript, das ich selbst geschrieben habe, um zu zeigen, dass der Widening-Endpoint tut was er soll.
Was der Agent nicht tut
In echten Sessions hat der Agent das nie benutzt. Nicht einmal.
Er hat ls angefragt. Approved. ls ausgeführt. Dann cat package.json. Approved. cat ausgeführt. Dann ls auf einem anderen Verzeichnis. Approved. ls ausgeführt. Dann git status. Approved. git status. Dann cat package.json nochmal, weil er die Ausgabe nicht mehr im Kontext hatte. Approved. Ausgeführt. Dreißig Mal durch, kein einziger Widening-Request dabei.
Das hat mich eine Weile gewurmt. Ich dachte zuerst, ich hätte die Tool-Definition schlecht gestaltet, oder der System-Prompt wäre unklar darüber, wann Widening angebracht ist. Habe beides nachgeschärft. Keine Änderung.
Dann habe ich gemerkt: Der Agent kennt sich mit dem aus, was er kennt. Er kann ein ls formulieren, weil ls zu seiner Welt gehört. Er kann ein cat package.json formulieren. Er kann sogar einen git log --oneline --since=yesterday formulieren. Was er nicht macht, ist tiefer im Tool-Use-Flow innezuhalten und eine Meta-Anfrage zu stellen — "im Hinblick auf die nächsten fünfzehn Minuten dieser Session wäre folgender breiterer Scope effizient". Das ist nicht sein Denk-Modus. Tool-Use ist reaktiv, pro Step. Meta-Reflexion über den eigenen Workflow findet nicht im Flow statt.
Der Agent ist eine gute Schreibkraft. Er ist kein Product Owner seines eigenen Arbeitsprozesses.
Wer ist der Genervte?
Das ist der Punkt, an dem die Architektur gekippt ist.
Wenn der Agent Widening nicht selbst applied, ist die Frage: wer ist überhaupt genervt von fehlendem Widening? Der Agent nicht. Der Agent hat kein Nerv-System. Der Agent kriegt Approve → Execute → Approve → Execute und macht einfach weiter.
Genervt bin ich. Der User. Der, der sitzt und auf seinem Handy zum zehnten Mal Approve tippt für ls.
Wenn aber ich derjenige bin, dem Widening einen Nutzen bringt, dann ist Widening eine User-Entscheidung. Keine Agent-Entscheidung. Der User weiß was dieser Agent in den nächsten Wochen voraussichtlich tun wird — er hat den Agent eingerichtet, weiß wofür er ihn einsetzt, hat eine Vorstellung der Aufgaben. Der Agent weiß nur, was er im aktuellen Tool-Use-Step gerade tut. Die Widening-Entscheidung braucht Kontext über den Agent, nicht Kontext im Agent.
Das ist der Shift. Widening gehört nicht in den Agent. Es gehört auf den Server. Und es gehört zum User.
Standing Grants
Das Ding heißt im OpenApe-Vokabular jetzt Standing Grant. Ein Grant-Template, das der User selbst anlegt — bevor der Agent irgendetwas angefragt hat. Er beschreibt: welcher Agent, welche CLI, welche Action, bis zu welchem Risk, optional auf welcher Resource, optional mit Ablaufdatum. Eine Row in der grants-Tabelle mit status='approved' und decided_by = <ich>. Kein Agent ist daran beteiligt. Der Grant existiert, bevor jemand ihn braucht.
Wenn ein Agent dann einen regulären Request an POST /api/grants schickt, läuft der durch folgende Kette:
- Reuse-Check. Gibt es bereits einen approved Grant mit exakt denselben Details? Wiederverwenden.
- Standing-Grant-Check (neu). Gibt es einen Standing Grant, dessen Pattern die Request-Details covered? Neuen Grant mit
status='approved'erzeugen,decided_by = <SG-Owner>,decided_by_standing_grant = <SG-ID>als Audit-Pointer. - Similarity-Check. Gibt es einen ähnlichen Grant (gleiche CLI+Action, andere Resource)? Dem Approver als Kontext zeigen.
- HITL. Fällt alles davor durch: Notification an den Approver, Approve-Seite im Browser.
Step 2 ist der Shift. Der Agent-Request wird server-seitig gegen eine Bibliothek von user-definierten Pre-Authorizations geprüft. Und bei Miss lande ich in meinem Browser.
Der Audit-Trail bleibt vollständig. Jeder Grant steht weiter in der Tabelle. Zusätzlich steht da decided_by_standing_grant, also kann ich rückwirkend nachvollziehen: dieser Grant war gegen Standing Grant #42 gematcht, den ich am 18. April erstellt habe. Nichts verschwindet. Nur der Prompt verschwindet — für Pattern, die ich explizit pre-authorized habe.
Den client-seitigen Widening-Code habe ich nicht gelöscht. Er liegt in einem Branch und wartet darauf, dass Agents in ein paar Jahren eventuell doch meta-reflektieren. Aktuell ist er toter Code.
Safe Commands: wenn der User der Neue ist
Es gibt einen Fall, den das Modell noch nicht abdeckt: den ganz frischen Agent am ersten Tag. Null Standing Grants. Jeder Request ein Prompt. Genau das Problem, das ich lösen wollte, nur diesmal für die Einrichtungs-Phase.
Die ehrliche Variante wäre gewesen, dem User zu sagen: dein Agent läuft jetzt, bitte verbringe die nächsten zehn Minuten damit, Standing Grants einzurichten, danach ist es ruhiger. Das macht niemand. Niemand liest Setup-Dokumentation bis zum Ende, niemand richtet präemptiv Policies ein.
Deshalb kommen vierzehn Defaults mit. Bei Agent-Enrollment legt der IdP automatisch Standing Grants für diese Commands an:
ls, cat, head, tail, wc, file, stat, which, echo, date, whoami, pwd, find, grep
Jedes als risk=low, ohne Resource-Constraint, ohne Ablauf. Alle vierzehn sind read-only, nicht-mutierend, nicht-netzwerkend, nicht-credential-berührend. rm ist nicht dabei. curl ist nicht dabei. ssh ist nicht dabei. git ist nicht dabei. Die Liste enthält was man tippt um zu sehen, was ist — und nichts davon, was man tippt um zu verändern, was ist. Das ist die Linie, die ich scharf halten will.
Die vierzehn sind eine Vorweg-Entscheidung, die ich als Maintainer für alle User treffe, die keine eigene getroffen haben. Wer sie will, kriegt sie. Wer sie nicht will, deaktiviert die Gruppe mit einem Toggle. Wer bestehende Agents retroaktiv seed'en will, findet auf /agents einen Bulk-Apply-Modal.
Auto-Approvals über diese Gruppe kriegen in der Activity-Liste ein Safe cmd Badge. Das ist keine Kosmetik. Das ist, weil ich an einem Monat später rückwirkend unterscheiden können will: ist dieser Grant durchgelaufen, weil ich ihn persönlich gepflegt habe, oder weil er in der vom Maintainer definierten Defaults-Gruppe war?
Wenn die Vierzehn nicht reichen
Nach ein paar Tagen hatte ich Daten darüber, welche Commands weiter prompten. Der häufigste war git status. Dann git log. Dann git diff. Keiner davon ist in den Defaults, weil git als CLI auch git push, git commit, git reset --hard kennt — und eine Default-Liste, die das alles zulässt, wäre nicht mehr safe.
Ich hätte jetzt einen Standing Grant pro Read-Subcommand anlegen können. Mache ich nicht, weil ich nicht so über Git denke. Ich denke alles read-only bei Git ist okay. Das ist eine User-Entscheidung auf der Klassen-Ebene, und User-Entscheidungen auf der Klassen-Ebene sind genau was Standing Grants sein sollen — wenn die Sprache es hergibt.
Dafür kam Glob. cliAuthorizationDetailCovers() — die Funktion, die entscheidet ob ein gewährter Scope einen eingehenden Request covered — behandelt * in granted Selector-Values als POSIX-Shell-Glob. Ein Standing Grant mit resource_chain_template: "file://{path:/home/patrick/projects/*}" matched /home/patrick/projects/openape/README.md und /home/patrick/projects/blog/2026-04-21.md, aber nicht /etc/passwd. Selector-Werte ohne * bleiben literale Gleichheit. Alle bestehenden Standing Grants matchen weiter wie vorher.
Im UI sitzt das hinter einem Wizard, den ich mobile-first gebaut habe. Erster Schritt: tipp ein Beispiel-Kommando ein, das du zuletzt gebraucht hast. Der Wizard resolved das gegen die registrierte Shape und zieht die typed Slots heraus. Zweiter Schritt: jeden Slot auf Literal, Any oder Pattern setzen, mit Live-Preview die ein paar Beispiele zeigt, die der Pattern match bzw. nicht match. Dritter Schritt: Risk-Cap, optionale Duration, Begründung.
Die meisten Approvals, die ich heute mache, mache ich vom Handy. Wenn das Authoring von Pre-Authorizations am Desktop gebunden wäre, wäre es genau ausserhalb des Moments, in dem ich es tun will.
Zwei Approval-Layer
Das Eigentliche, das mir beim Umbau klar wurde, ist nicht die Mechanik. Es ist der Layer-Schnitt.
Es gibt zwei Ebenen, auf denen Approval stattfinden kann. Die eine ist Pattern: eine User-Entscheidung, welche Klasse von Dingen pre-authorized ist. Die andere ist Command: eine live Entscheidung, ob dieses eine konkrete Ding jetzt gerade okay ist.
Die naheliegende Alternative — Cache — erweitert Command zeitlich (dieser approved Command gilt noch fünf Minuten für alle ähnlichen), ohne eine neue Ebene einzuziehen. Das verwandelt einmal approved in immer approved, für N Minuten. sudo-timestamp. sudoers mit timestamp_timeout. Für Menschen handhabbar, für einen Agent kollabiert die Unterscheidung zwischen einem approved Kommando und allen Kommandos in den nächsten N Minuten vollständig.
Standing Grants ziehen die neue Ebene explizit ein. Pattern ist ein erstklassiges Artefakt: UI, Owner, Audit-Log, Revoke-Funktion, Inventar. Ich kann auf /agents/claude@home reingehen und sehen: diesem Agent habe ich drei Standing Grants erteilt. Eines für git.status, eines für Safe Commands, eines für file://projects/*. Jedes einzelne revokebar, ohne die anderen zu berühren. Command bleibt live, per Kommando, Approver-gebunden. Alles, was kein Pattern covered, landet in der Approve-Seite.
Die Frage ist nicht wie lange gilt ein Approval. Die Frage ist welche Klasse bin ich bereit, vorher zu beschreiben.
Die Intelligenz war im falschen Layer
Zurück zum Anfang. Die client-seitige Version hat die Widening-Entscheidung dem Agent überlassen, weil ich dachte, Agent-Intelligenz läuft auch über Meta-Reflexion über den eigenen Workflow. Das tut sie nicht, zumindest nicht in aktuellen Tool-Use-Flows. Der Agent kennt sich mit dem aus, was er kennt — dem konkreten nächsten Command. Er denkt nicht über das Muster dahinter.
Die Intelligenz über das Muster war an der falschen Stelle. Sie gehört zum User. Der User hat den Workflow-Kontext — dieser Agent ist mein Coding-Agent, der liest viel, schreibt gelegentlich, dieser andere Agent ist ein Sync-Job, der einen engen Scope hat, aber oft läuft. Der User macht aus diesem Kontext bewusste Klassen-Aussagen. Der Agent führt seine konkreten Tool-Use-Steps aus, und für die, die in einer der Klassen liegen, passiert das Approval stillschweigend.
Das ist dasselbe Pattern wie im Rest von OpenApe: Infrastructure over Instructions. Ich mache nicht den Agent schlauer. Ich hänge die Struktur anders. Widening-Entscheidungen werden nicht vom Agent getroffen, sie werden vom User getroffen, und die Infrastruktur sorgt dafür, dass beide Seiten voneinander profitieren, ohne direkt zu verhandeln.
Hygiene
Was nach zwei Wochen sichtbar geworden ist: ich habe mehr Standing Grants als ich erwartet hatte. Irgendwann braucht es zeig mir alle, die ich in den letzten N Tagen erteilt und nie benutzt habe und einen One-Click-Revoke. Das ist kein Sicherheits-Feature, das ist Reinigung. Ohne die verwässert das Modell — und ein verwässertes Modell ist nach kurzer Zeit kein Modell mehr, sondern nur eine lange Liste von ja aus der Vergangenheit.
Der Schnitt
Nicht jedes Kommando braucht einen Approval. Aber jedes approved Pattern braucht einen menschlichen Autor.
Der Autor des Patterns bin ich — mit Namen, mit Audit-Row, mit Revoke-Knopf. Der Approver des einzelnen Commands bin entweder ich (wenn kein Pattern greift) oder das Pattern, das ich gebaut habe (wenn eines greift). An keiner Stelle ist der Agent selbst im Authorizierungs-Pfad. Das war vorher anders — nicht in der Theorie, aber in dem stillen Verlass darauf, dass der Agent schon selbst Widening-Anfragen stellen würde. Hat er nicht.
Im vorigen Artikel hieß es: nicht der User wird approved, sondern der Crossing. Standing Grants ändern daran nichts. Sie machen nur, dass das Modell weiter funktioniert, wenn der Agent hundertmal pro Stunde etwas tut — nicht weil der Agent schlauer geworden ist, sondern weil ich auf der richtigen Ebene die richtigen Aussagen machen kann.
Code: github.com/openape-ai/openape, MIT-lizenziert. Die Mechanik lebt in @openape/grants, das UI in openape-free-idp.