Sandboxed Plugin-Jobs
Tier 3: nicht vertrauenswürdige oder rechenintensive Plugin-Berechnungen hinter einer OS-Prozessgrenze und ohne Ambient-Credentials.
Tier 3 spezifiziert Berechnungen, die nicht innerhalb von Convex laufen sollten: das Parsen nicht vertrauenswürdiger Eingaben, aufwendige Analysen, alles, was hängen bleiben oder explodieren könnte. Das Polling-, Lease- und Outcome-Protokoll auf Worker-Seite ist weiterhin implementiert, aber es wird weder ein Convex-Enqueue-Adapter noch eine Operator-UI für Jobs ausgeliefert, sodass Plugins heute keine Jobs starten können.
Derselbe Container bedient aktiv die Queue des Coding-Agents. Sein ruhender Plugin-Poller nutzt denselben gehärteten Executor, statt eine parallele Sicherheitsgrenze zu schaffen.
Was ein Plugin darf und was nicht
worker:enqueue ist reserviert. Ein künftiger Host-Adapter darf ausschließlich Enqueue gewähren; Claiming, Cancelling, Reclaiming und Lesen müssen Host- und Operator-Operationen bleiben.
Eine Job-Art hat die Form plugin.<pluginId>.<localId>. Der Host prüft die Zugehörigkeit der Art direkt am String selbst, sodass ein Plugin, das plugin.other-plugin.job einreihen will, abgewiesen wird. Das Plugin wählt, welche eingebaute Job-Art ausgeführt wird; das Kommando ist host-kontrolliert und wird niemals aus der Payload zusammengebaut. Die nicht vertrauenswürdige Payload wird immer als eigenständiges argv-Element übergeben, niemals in einen Shell-String interpoliert.
Enqueue-Limits
| Limit | Wert |
|---|---|
| Payload | ≤ 64 KiB (zu groß ⇒ beim Enqueue abgelehnt) |
| Gespeichertes Ergebnis | ≤ 64 KiB (zu groß ⇒ vor dem Persistieren abgeschnitten) |
| Versuche | auf 1 … 5 begrenzt |
| Wall Clock einer einzelnen Ausführung | auf 1 s … 15 min begrenzt |
| In-Flight-Jobs pro (Organisation, Plugin) | 100 queued + running |
Ein künftiger Enqueue-Adapter muss fail-closed abbrechen bei einem deaktivierten, nicht gewährten oder nicht deklarierten Plugin, einer plugin-fremden Job-Art, einer zu großen Payload oder einem erschöpften In-Flight-Budget. Terminale Jobs zählen nicht gegen die In-Flight-Obergrenze.
Job-Lebenszyklus
queued ──claim──▶ running ──complete──▶ succeeded
│ │
│ ├──fail (attempts < max)──▶ queued
│ ├──fail (attempts = max)──▶ failed
│ └──cancel requested──────▶ cancelled
└──cancel requested, then claim───────────▶ cancelled (never runs)
- Heartbeats belegen Lebendigkeit und sind der Weg, über den ein Worker von einem Operator-Cancel erfährt. Der nächste Heartbeat eines abgebrochenen laufenden Jobs weist den Worker an, die gesamte Prozessgruppe zu töten.
- Ein Abbruch lässt sich nicht umgehen, und ein abgebrochener Job wird niemals erneut versucht.
- Lease Reclaim holt Jobs zurück, die ein abgestürzter oder neu gestarteter Worker zurückgelassen hat. Das Lease-Fenster liegt großzügig über dem maximalen Ausführungsbudget, sodass ein langsamer, aber lebendiger Job, der weiter Heartbeats sendet, nie unter sich selbst weggezogen wird; jeder Sweep ist begrenzt.
- Retries sind begrenzt durch das host-geklemmte
maxAttempts, sodass ein Poison-Job alsfailedendet, statt endlos zu kreisen. - Fehlergründe sind die festen Codes
worker_failedundworker_timeout; jede Fehlermeldung wird gekürzt.
Enqueue und jedes terminale Ergebnis schreiben eine dem pluginId zugeordnete Audit-Zeile.
Die Sandbox
Der Worker-Orchestrator hält den Deployment-Admin-Key (und, für die Coding-Agent-Queue, GITHUB_TOKEN sowie den LLM-Key) und läuft als eingeschränkter Root, ausschließlich um Privilegien abgeben zu können. Jeder nicht vertrauenswürdige Kindprozess wird über eine einzige Naht gestartet, die immer:
- auf die unprivilegierte
sandbox-uid/gid (10001) herunterschaltet — eine uid-übergreifende Kernel-Grenze, die das Kind nicht überschreiten kann, um/proc/<orchestrator>/environzu lesen oder den Orchestrator zu ptracen; - argv mit
shell: falseverwendet — keine Shell-Interpolation; detachedläuft, sodass das Kind seine eigene Prozessgruppe anführt und ein Timeout oder Abbruch die gesamte Gruppe abräumt, nicht nur das direkte Kind;- mit jedem Ambient-Credential aus der Job-Umgebung entfernt läuft. Ein Plugin-Job sieht niemals den Admin-Key,
GITHUB_TOKENoder den LLM-Key. Jede Capability, die Zugangsdaten erfordert, wird vom Host über Convex vermittelt, niemals durch Leaken von Env in die Sandbox; - an der host-geklemmten Wall-Clock-Deadline getötet wird (fail-closed).
Obergrenzen auf Container-Ebene, aus docker-compose.yml:
| Kontrolle | Default |
|---|---|
mem_limit | 2 g (CODE_WORKER_MEM_LIMIT) |
cpus | 1.0 (CODE_WORKER_CPUS) |
pids_limit | 512 (CODE_WORKER_PIDS_LIMIT) |
| Capabilities | cap_drop: ALL, danach nur SETUID, SETGID, CHOWN |
| Dateisystem | read_only: true, Schreibzugriffe beschränkt auf das Volume code-workspace und ein 256 MiB großes /tmp-tmpfs |
| Privilegien-Eskalation | no-new-privileges: true; das Standard-Seccomp-Profil von Docker bleibt aktiv |
| Netzwerk | Die isolierte code-worker-Bridge, ausschließlich mit Convex geteilt — keine Route zum MTA-Admin-Port, zu IMAP, Redis, mail-sync, clamd, dem Updater oder dem Docker-Socket-Proxy |
Der Worker führt von Angreifern beeinflussten Code aus. Seine Netzwerkisolation ist genau das, was laterale Bewegung in die Mail- und Infrastrukturschicht unterbindet. Ihn der gemeinsamen Bridge hinzuzufügen würde die gesamte Grenze zunichtemachen.
Eingebaute Job-Arten
Die Registry der Job-Arten liegt im Worker, ist host-kontrolliert und wird über die lokale Id adressiert:
| Lokale Id | Kommando |
|---|---|
selftest | Eine No-op, die mit 0 endet — belegt uid-Drop, Env-Stripping, Timeout und Cancel-Verdrahtung Ende zu Ende, ohne dass Plugin-Code im Image ist |
seed-test | Der Seed-List-Platzierungstest des Deliverability Lab; das kompilierte Shim ist das Kommando und die nicht vertrauenswürdige Payload trifft als eigenständiges argv-Element ein |
Eine neue Tier-3-Job-Art ist eine Änderung am Worker-Image und wird wie jeder andere Host-Code reviewt — nichts, was ein Plugin-Package im Alleingang einführen kann.
Betrieb des Workers
COMPOSE_PROFILES=plugin-tasks docker compose up -d code-worker
Der Worker kann sich mit dem Deployment-Admin-Key bei Convex authentifizieren und das ruhende Queue-Protokoll abfragen, aber der aktuelle Host-Code legt keine Zeilen an. Es gibt weder eine Query für kürzliche Jobs noch eine UI zum Abbrechen.
Das Aktivieren des Profils plugin-tasks startet einen Poller, aber kein ausgelieferter Host-Pfad kann Arbeit einreihen. Die Deklaration von worker:enqueue hat keine Laufzeitwirkung, bis ein Adapter mit konkretem Producer und Operator-Oberfläche landet.