Capabilities, Grants & Vertrauen
Das Berechtigungsmodell für Plugins, die drei Vertrauensstufen und die Sicherheitsinvarianten, die kein Plugin aufweichen kann.
Capabilities sind Anfragen, Grants sind Berechtigungen
Das capabilities-Array eines Manifests ist eine Anfrage: die Menge der Host-Operationen, die das Plugin jemals anfordern darf. Die Grants eines Betreibers sind die Berechtigung: welche dieser Anfragen für diese Installation erlaubt sind.
manifest capabilities ⊇ operator grants ⊇ what actually runs
Grants können das Manifest ausschließlich einschränken. Es gibt keinen Mechanismus, mit dem ein Betreiber, eine Connected App, ein API-Key oder ein Plugin eine Capability über das im Manifest Deklarierte hinaus ausweiten könnte. Das Deklarieren einer Capability gewährt sie nie.
Capability-Namen sind kleingeschriebene domain:action-Paare mit Exact-Match-Semantik. Wildcards sind nicht Teil des Vertrags. Der Namensraum ist offen — eine neue Contribution-Naht erfordert keine neue @owlat/plugin-kit-Major-Version, nur um ein Capability-Literal hinzuzufügen — und der Host bleibt die alleinige Instanz, die entscheidet, welche Capabilities er kennt und durchsetzt.
Das Vokabular
| Capability | Gewährt |
|---|---|
send:transport | Einen Send-Transport beitragen |
send:gate | Ein ausschließlich einschränkendes Autonomy-Gate beitragen |
agent:step | Einen Schritt der Inbox-Agent-Pipeline beitragen |
draft:strategy | Eine Strategie zur Entwurfserzeugung beitragen |
automation:trigger / automation:step / automation:condition | Zur jeweils passenden Automation-Registry beitragen |
webhooks:publish | Namensraum-gebundene Webhook-Events veröffentlichen |
imports:provide | Einen Import-Provider beitragen |
scheduler:cron | Einen Hintergrund-Cron mit festem Intervall registrieren |
ui:navigation / ui:settings | Ein Sidebar- oder Einstellungsziel hinzufügen |
worker:enqueue | Reserviert für gesandboxte Tier-3-Jobs; es wird kein Host-Enqueue-Adapter ausgeliefert |
llm:invoke | Verbrauchsgemessene Textgenerierung über Owlats LLM-Dispatch |
plugin-storage:read / plugin-storage:write | Namensraum-gebundener JSON-Key/Value-Storage |
Connected Apps fordern zusätzlich API-Scopes an, die auf einem plugin-gebundenen Key zugleich als Capabilities wirken: contacts:read, contacts:write, events:write, transactional:send, topics:write (auch auf eigenständigen Keys gültig) sowie die ausschließlich für Tier 2 gültigen campaigns:read, mail:read, knowledge:read, webhooks:manage, plugin-storage:read, plugin-storage:write.
Ein ausschließlich für Tier 2 gültiger Scope hat für einen eigenständigen Key keine Bedeutung. Die Key-Erstellung lehnt ihn ab, sofern der Key nicht an ein Plugin gebunden ist — er kann also nur innerhalb des Modells aus Manifest-Obergrenze plus Betreiber-Grant existieren. Alte Keys ohne scopes-Spalte sind bei der Durchsetzung deny-all.
Wann durchgesetzt wird
Jede gehostete Operation validiert die gesamte Kette unmittelbar bevor sie läuft — nicht einmalig beim Start:
- Singleton-Organisations-Scope,
- das Plugin ist registrierter Bestandteil der Build-Komposition,
- das Feature-Flag ist in der aufgelösten Flag-Map aktiv,
- das Manifest deklariert die Capability,
- die Betreiber-Grant für genau diese Capability existiert,
- jeder Eintrag aus
flag.requiredEnvVarsist vorhanden.
Jeder Fehlschlag löst einen PluginHostError aus (capability_not_declared, capability_not_granted, plugin_disabled, required_environment_missing, …), und der deklarierte sichere Fallback der Contribution greift. Eine Verweigerung ruft niemals Plugin-Code auf. Das Deaktivieren eines Plugins oder das Widerrufen einer Grant wirkt deshalb schon bei der nächsten Operation — kein Redeploy, kein Cache, den man abwarten müsste.
Services statt Framework-Kontexte
Gehosteter Plugin-Code erhält einen PluginContext, der ausschließlich aus host-vermittelten Services besteht:
| Service | Hinweise |
|---|---|
permissions | has(capability) / require(capability) |
storage | Namensraum-gebundener JSON-KV-Store. Methoden nehmen kein Organisations- oder Plugin-Argument entgegen |
llm | Attribuierter, budgetierter Dispatch |
logger | Strukturiertes Logging |
scheduler | Scheduling benannter Tasks |
Er legt niemals einen Convex-ctx, ein Datenbank-Handle, Umgebungswerte, rohe Modellobjekte oder beliebige Scheduler-Funktionsreferenzen offen.
Storage-Isolation und Limits
Der Host bindet den Storage-Service an die authentifizierte Organisation und das validierte Plugin und prüft dann bei jeder Operation erneut das Flag und die exakte plugin-storage:read / plugin-storage:write-Grant. Einträge sind nach Organisation, Plugin und Key indiziert; List-Cursor sind authentifiziert verschlüsselt und scope-gebunden.
| Limit | Wert |
|---|---|
| Key | 256 Bytes |
| Wert | 64 KiB |
| Einträge pro (Org, Plugin) | 1 000 |
| Gesamtbytes pro (Org, Plugin) | 10 MiB |
| Seitengröße einer Liste | 100 |
| JSON-Tiefe / -Knoten / -Array-Elemente / -Objektfelder | 32 / 4 096 / 1 024 / 1 024 |
Quota-Zähler werden in derselben Convex-Transaktion wie der jeweilige Schreibvorgang aktualisiert, die Obergrenze lässt sich also nicht durch eine Race Condition umgehen.
LLM-Budgets
llm:invoke erfordert ein explizites Flag und ein validiertes llmBudget.dailyUsd. Der Host autorisiert, bevor er die Provider-Konfiguration des Mandanten auflöst, und prüft Registrierung, Flag, Deklaration, Grant und Budget in derselben Transaktion erneut, in der er unmittelbar vor dem Dispatch die Ausgabe reserviert.
Anfragen sind auf 64 KiB UTF-8-Eingabe über höchstens 32 Nachrichten mit je 32 KiB sowie auf 2 048 Output-Tokens begrenzt. Akzeptiert werden nur exakte Modellidentitäten aus einem providerspezifischen Zulassungskatalog, und Preisangaben gelten nur für den eingebauten Endpunkt eines Providers als vertrauenswürdig — eine explizite Base-URL, ein Azure-Deployment, ein kompatibler/lokaler Server oder ein unbekannter Provider schlagen fail-closed fehl.
Die Abrechnung nutzt ganzzahlige Mikro-USD. Eine Reservierung deckt jeden Retry ab, den die gemeinsame Dispatch-Policy zulässt; bei Erfolg wird ungenutzter Spielraum freigegeben, für jeden fehlgeschlagenen Versuch bleibt jedoch ein Maximum einbehalten. Fehlende oder fehlerhafte Verbrauchsdaten, ein Provider-Fehler, ein Absturz der Action, ein Abrechnungsfehler oder ein Provider, der ein anderes als das zugelassene Modell meldet, führen alle dazu, dass die konservative Reservierung für diesen UTC-Tag einbehalten wird. Dieser Kompromiss zulasten der Verfügbarkeit gibt niemals Budget wieder frei, das der Provider möglicherweise berechnet hat.
Nicht vertrauenswürdiger Text
Von Plugins erzeugter Text ist an jeder Grenze nicht vertrauenswürdig. An den ausgelieferten Convex-seitigen Grenzen — Begründungen von Automation-Schritten und Begründungen von Autonomy-Gates — beschneidet der Host den Text auf ein Code-Point-Maximum und führt eine Injection-Bereinigung durch, bevor er einen Prompt oder ein gespeichertes Feld erreichen kann. Es gibt bewusst keine permissive Standard-Policy: Jeder Host-Adapter benennt die Policy für seine Laufzeit, und ein Nicht-String-Wert, ein Scrubber, der wirft, oder ein Scrubber, der keinen String zurückgibt, werden rundweg abgelehnt (untrusted_output_rejected).
Diese beiden sind die gesamte Liste. Der Assistent fehlt bewusst: Heute kann kein Plugin ein Assistenten-Tool beitragen — assistantTools ist ein reservierter Bucket (siehe Contribution-Referenz) —, und die Ausgabe der eingebauten Tools wird vom scrubForInjection des Assistenten selbst injection-bereinigt, einer Kernkontrolle ohne Bezug zu dieser Policy.
Browser-gebundener Plugin-Text ist eine andere, engere Kontrolle: Navigations- und Einstellungslabels werden auf 64 UTF-16-Code-Units beschnitten (ein astrales Zeichen zählt doppelt — dieselbe Einheit, mit der der Manifest-Validator den Namen begrenzt), wobei Steuer- und Bidi-Formatzeichen beim Ableiten des Eintrags entfernt werden. Das ist Spoofing-Abwehr — gegen Markup schützt Vues HTML-Escaping — und die Injection-Bereinigungs-Policy wird auf diesem Pfad nicht angewendet.
Gehostete Mail-Projektionen werden nach Unicode-Code-Points gekürzt: from 512, to 2 048, subject 1 024, jeder entschlüsselte Body oder Entwurf 65 536 und die Klassifikation, die ein Autonomy-Gate sieht, 128.
Audit-Attribution
Jede erfolgreiche Host-Service-Operation schreibt eine mandantenbezogene Audit-Zeile, die sowohl organizationId als auch pluginId trägt. Audit-Metadaten sind ein Allowlist-basierter Skalar-Snapshot: Storage-Keys/-Werte/-Präfixe/-Cursor, LLM-Prompts/-Nachrichten/-Ergebnisse, Provider-Fehler, Credentials, Mail-Inhalte und aufruferdefinierte Metadaten gelangen nie über die Audit-Grenze. Eine erfolgreiche gehostete Mutation schreibt ihre Audit-Zeile in derselben Transaktion wie die Zustandsänderung — ein Rollback verwirft also beides.
Die drei Vertrauensstufen
Tier 1 — gebündelte Plugins
Vom Betreiber installierte Pakete, die zur Build-Zeit komponiert werden. Sie dürfen Backend- und Frontend-Module beitragen und eine isolierte Convex-Komponente unter einem injektiven plugin_<id>-Namensraum ausliefern. Laufzeit-Flags deaktivieren sie sofort; das Installieren oder Entfernen erfordert einen Rebuild.
Vertrauen: dasselbe wie beim eigenen deployten Code des Betreibers. Capabilities, Grants, Flags und host-vermittelte Services reduzieren die Autorität und machen Aktionen attribuierbar. Sie sind keine Sandbox.
Tier 2 — Connected Apps
Externe HTTPS-Dienste, die über gescopte API-Keys und Webhooks erreicht werden, mit einem spezifizierten Protokoll für signierte synchrone Hooks. Sie führen niemals Code innerhalb von Convex oder Nuxt aus. Die API-Key- und die Webhook-Hälfte sind aktiv; der Hook-Laufzeitadapter wird nicht ausgeliefert. Siehe Connected Apps.
Vertrauen: nicht vertrauenswürdiger Netzwerk-Peer. Antworten werden strikt strukturvalidiert, bereinigt, beschnitten, größenbegrenzt, deadline-begrenzt und gegen Replay geschützt.
Tier 3 — gesandboxte Worker-Jobs
Das workerseitige Protokoll für nicht vertrauenswürdige oder ressourcenintensive Berechnungen bleibt hinter der OS-Grenze des code-worker, aber es wird kein Host-Enqueue-Adapter ausgeliefert. Siehe Gesandboxte Jobs.
Vertrauen: nicht vertrauenswürdiger Prozess. Keine ambienten Credentials, harte Ressourcen- und Zeitobergrenzen, host-kontrollierter Befehl, host-autoritative Abbrüche und Retries.
Dasselbe Capability-Vokabular und dieselben Host-Service-Prinzipien gelten für alle drei Tiers.
Invarianten, die ein Plugin niemals aufweichen kann
Diese gelten für jedes Tier und jeden Contribution-Punkt. Bräche eine Änderung eine davon, ist sie kein Plugin-Feature — sie ist ein Bug.
- Capabilities werden bei der Codegen und zur Laufzeit geprüft. Eine Contribution, deren Capability nicht deklariert ist, wird nie komponiert; eine Operation, deren Capability nicht gewährt ist, läuft nie.
- Betreiber-Grants können das Manifest nur einschränken. Nichts weitet sie aus.
- Ein Plugin darf Arbeit oder Vorsicht hinzufügen, nie entfernen. Es kann den Sicherheitsscan nicht umgehen, kein Sicherheits-Gate entfernen, keine Freigabe erzwingen und keinen Versand erzwingen. Die Ergebnistypen von Gates und Hooks sind strukturell nicht in der Lage, eine Freigabe auszudrücken.
- Gate-Fehler und -Timeouts schlagen fail-closed fehl. Fehlende, deaktivierte, widerrufene, veraltete, abgelaufene, fehlgeschlagene oder fehlerhafte Gates leiten in die menschliche Prüfung.
- Von Plugins erzeugter Text ist nicht vertrauenswürdig — an jeder Grenze beschnitten und an jeder Convex-seitigen Grenze injection-bereinigt.
- Der LLM-Zugriff von Plugins läuft über den Dispatch, mit Plugin-Attribution und einem harten Tagesbudget; Unsicherheit in der Abrechnung schlägt fail-closed fehl.
- Mandanten- und Plugin-Storage sind isoliert, wobei der Scope vom Host gebunden und pro Operation erneut geprüft wird.
- Es wird kein beliebiges Laufzeit-JavaScript in Convex oder den Browser geladen. Sofortige Installation gibt es nur für Connected Apps und gesandboxte Jobs.