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

LimitWert
Payload≤ 64 KiB (zu groß ⇒ beim Enqueue abgelehnt)
Gespeichertes Ergebnis≤ 64 KiB (zu groß ⇒ vor dem Persistieren abgeschnitten)
Versucheauf 1 … 5 begrenzt
Wall Clock einer einzelnen Ausführungauf 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 als failed endet, statt endlos zu kreisen.
  • Fehlergründe sind die festen Codes worker_failed und worker_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>/environ zu lesen oder den Orchestrator zu ptracen;
  • argv mit shell: false verwendet — keine Shell-Interpolation;
  • detached lä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_TOKEN oder 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:

KontrolleDefault
mem_limit2 g (CODE_WORKER_MEM_LIMIT)
cpus1.0 (CODE_WORKER_CPUS)
pids_limit512 (CODE_WORKER_PIDS_LIMIT)
Capabilitiescap_drop: ALL, danach nur SETUID, SETGID, CHOWN
Dateisystemread_only: true, Schreibzugriffe beschränkt auf das Volume code-workspace und ein 256 MiB großes /tmp-tmpfs
Privilegien-Eskalationno-new-privileges: true; das Standard-Seccomp-Profil von Docker bleibt aktiv
NetzwerkDie 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
Legen Sie einen Plugin-Job nicht ins Default-Netzwerk

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 IdKommando
selftestEine 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-testDer 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.

Nur Protokoll: kein Enqueue-Adapter

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.