Files
HiveGrid-Wireframes/hivegrid-cloud/docs/improvement-plan/06-plugin-system/wasm-sandbox.md
T

47 KiB
Raw Blame History

WASM-Sandbox + Plugin-SDK + Manifest + Signing + Capabilities + Resource-Limits

Status: in_progress (foundation-shipped, hot-reload + marketplace + per-instruction-fuel + worker-thread-pool deferred) Task-ID: #21 Phase: 06 Estimated Effort: 4-5 Wochen Owner: unassigned

Historical implementation plan. This file is retained for design context, not live status. Current plugin runtime truth lives in docs/features/plugins.md, docs/operations/plugin-lifecycle.md, docs/plugins/manifest-reference.md, and docs/security/plugin-runtime-signoff.md. The shipped foundation runtime is production-gated; only reviewed log and audit host capabilities are wired today, and broader bindings remain fail-closed pending security review.

Problem

Heute kann die Boilerplate nicht ohne Forking erweitert werden. Ein Drittanbieter (oder ein internes Team das nicht im Core-Repo arbeiten soll) hat keine Möglichkeit, neue Hooks, neue UI-Slots oder neue Integrationen anzubinden. Jede Anpassung erfordert einen Pull-Request gegen die Boilerplate, einen Rebuild, ein neues Deployment — das skaliert nicht und blockiert Spezialfunktionen pro Kunde.

Naive Lösungen (z.B. „lade NPM-Pakete dynamisch zur Laufzeit") sind aus mehreren Gründen disqualifiziert:

  • Keine Isolation: Ein bösartiges oder buggy NPM-Paket bekommt vollen Zugriff auf process.env, das Filesystem, alle DB-Connections, alle Tenant-Daten. Mandanten-Isolation wäre nicht mehr durchsetzbar.
  • Keine Resource-Limits: Node.js bietet keine harten CPU- oder Memory-Limits pro Modul. Ein Plugin mit einer Endlosschleife oder einem Memory-Leak killt den ganzen Worker-Prozess.
  • Keine Versionierung / Sicherheit: Kein Signatur-Mechanismus, keine Capability-Erklärung, keine Möglichkeit zu sagen „dieses Plugin darf HTTP aber keine DB-Writes".

At the time this plan was written, there were no code paths addressing this surface. That is no longer true: the foundation runtime, manifest/signing contract, invoke worker, capability gates, lifecycle service, settings shell, and audit rows now exist behind production rollout gates.

Diese Task baut die Foundation der gesamten Plugin-Surface: eine WebAssembly-basierte Sandbox, ein SDK das Plugin-Entwickler nutzen um gegen die Host-API zu schreiben, ein Manifest-Schema, ein Signatur-Mechanismus, ein Capability-System und harte Resource-Limits.

Depends on

  • 03-scaling-infrastructure/shared-store-package.md — SharedStore wird für Plugin-Resource-Counters genutzt (Rate-Limits pro Plugin pro Tenant pro Minute). Ohne SharedStore-Adapter müsste jeder Plugin-Worker eigene Redis-Connections aufmachen → Connection-Explosion.
  • 03-scaling-infrastructure/worker-queues.md — die Worker-Trennung in Groups (#45) liefert das Pattern für einen dedizierten worker-plugin-Pool. Plugin-Invocations laufen niemals in worker-general, sondern in einem eigenen Group mit eigenem Sizing.
  • 03-scaling-infrastructure/outbox-pattern.md — Plugin-Hooks werden async via Outbox getriggert, nicht synchron im Request-Path.
  • 05-business-features/feature-flags.md — Capability-Toggles werden als Feature-Flags pro Tenant verwaltet, damit Tenant-Admins granulares Consent geben können.
  • 02-observability/sentry-wiring-api-worker.md — Sentry-Tags plugin.id, plugin.version, plugin.capability müssen pro Plugin-Span gesetzt werden — Wiring muss vorher stehen.

Blocks

  • 06-plugin-system/hooks-lifecycle.md — Hook-Points können erst angeschlossen werden, wenn die Runtime sie aufrufen kann (SDK + Sandbox + Capability-Validator). Ohne Sandbox ist das nur eine leere Hook-Registry.
  • 06-plugin-system/audit-docs.md — SDK-Reference, Capability-Matrix, Beispiel-Plugins können nicht geschrieben werden, bevor die SDK-API stabil ist.
  • 11-backlog/marketplace.md — kein Marketplace ohne Sandbox.

Approach

Historical plan note: this section captures the original Wasmtime-oriented design exploration. The shipped foundation runtime currently uses the Node/V8 worker sandbox documented later in this file and exposes only the production-available log and audit host capabilities; broader bindings remain fail-closed pending dedicated security review.

Wasmtime als WASM-Runtime, Worker-Thread-Isolation pro Plugin-Call, Capability-basierter Host-API-Zugriff via WIT-Interfaces.

Begründung gegen Alternativen

Alternative Gegen Begründung
Node.js vm / isolated-vm ❌ Keine harten Resource-Limits, Sandbox-Escapes sind regelmäßig dokumentiert (V8 ist nicht als Sicherheits-Boundary designed), kein Capability-System.
Deno-Runtime als Subprocess ❌ Größerer Footprint (komplette JS-Runtime pro Plugin), Permission-System ist Filesystem-/Network-orientiert und passt nicht auf unsere Hook-API, schwieriger als embedded Library.
Docker-Container pro Plugin ❌ Massive Resource-Overhead (jeder Hook-Call startet einen Container? Nicht skalierbar). Pre-Started Container-Pool wäre Custom-Orchestrierung.
Wasmer statt Wasmtime ⚠️ Research-Subagent klärt Beide sind valide. Wasmtime (Bytecode Alliance, von Fastly + Cloudflare + Microsoft genutzt) hat klar dokumentierten Resource-Limit-API (fuel, memory.max). Wasmer hat mehr Engines (Singlepass/Cranelift/LLVM) aber inkonsistentere Limit-API. Research-Subagent muss Performance + Limit-Garantien + Ed25519-Verify-Module-Verfügbarkeit + Node-Bindings-Stabilität vergleichen, bevor wir uns binden. Default-Annahme: Wasmtime via @bytecodealliance/preview2-shim + @bytecodealliance/jco für Component-Model.
WASM ohne Component-Model (raw imports/exports) ❌ Manuelles Marshalling, kein Typ-System über die Host-Boundary, schmerzhaft für SDK-DX. Component-Model + WIT gibt uns typsicheres RPC zwischen Plugin und Host.

Architektur-Skizze

┌──────────────────────────────────────────────────────────────────┐
│ Host-Process (worker-plugin, replicas N)                         │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │ Main Thread                                                │  │
│  │  - BullMQ-Worker konsumiert `plugin.invoke`-Jobs           │  │
│  │  - Job-Payload: { pluginId, pluginVersion, hookName,       │  │
│  │                   tenantId, inputJson }                    │  │
│  │  - Sucht Plugin-Binary + Manifest in PluginRegistry        │  │
│  │  - Validiert Capability-Use gegen Tenant-Consent (Cache)   │  │
│  │  - Spawn / wiederverwende Worker-Thread aus Pool           │  │
│  │  - Sendet { wasmBlob, manifest, input, hostBindings } in   │  │
│  │    Worker via postMessage                                  │  │
│  └────────────────────────────────────────────────────────────┘  │
│                              │                                   │
│           ┌──────────────────┴──────────────────┐                │
│           ▼                                     ▼                │
│  ┌─────────────────┐                ┌─────────────────┐          │
│  │ Worker-Thread 1 │      …         │ Worker-Thread N │          │
│  │                 │                │                 │          │
│  │  Wasmtime-      │                │  Wasmtime-      │          │
│  │  Instance       │                │  Instance       │          │
│  │  + fuel-limit   │                │  + fuel-limit   │          │
│  │  + mem-limit    │                │  + mem-limit    │          │
│  │  + wall-clock   │                │  + wall-clock   │          │
│  │                 │                │                 │          │
│  │  Host-Imports:  │                │                 │          │
│  │   - http.fetch  │  ──RPC──►  Main Thread Bindings  │          │
│  │   - db.query    │            (Capability-Check +   │          │
│  │   - kv.get/set  │             Tenant-Scoping +     │          │
│  │   - log/audit   │             Rate-Limit-Counter)  │          │
│  └─────────────────┘                └─────────────────┘          │
└──────────────────────────────────────────────────────────────────┘

Pro Plugin-Call: Job aus Queue → Pool-Worker reservieren → WASM-Instanz mit Limits laden → Hook-Funktion aufrufen → Output via structured-clone zurück → Audit-Log-Eintrag → Worker freigeben (Instanz zerstören, nächster Job ggf. Pool-recycled).

SDK-Package (@toolctrl/plugin-sdk)

Veröffentlicht als NPM-Package + Rust-Crate (für AssemblyScript-/TinyGo-/Rust-Plugins). Mindestens TypeScript-First via AssemblyScript-Target.

Host-Bindings die exposed werden (initial-Scope):

Binding Capability Beschreibung
http.fetch(req) http.outbound HTTP-Calls an erlaubte Hosts (Allowlist im Manifest)
db.query(sql, params) db.read Read-only, Tenant-scoped (Host injiziert WHERE tenantId = …)
db.execute(sql, params) db.write Write, Tenant-scoped, nur erlaubt auf Tabellen-Prefix plugin_<pluginId>_
storage.get/put(key, value) storage.read / storage.write Tenant-scoped KV-Store via SharedStore (Redis), Prefix plugin:<id>:tenant:<tid>:
log.info/warn/error(msg, ctx) log (immer erlaubt) Strukturiertes Logging, landet in Loki/CloudWatch mit Tag plugin.id
audit.record(action, target, meta) audit (immer erlaubt) Plugin-eigene Audit-Einträge in Audit-Tabelle
tenant.info() tenant.info (immer erlaubt) Read-only Tenant-Metadaten: id, slug, name, plan
secret.get(key) secret.read:<key-pattern> Tenant-spezifisches Plugin-Secret (vom Tenant-Admin gesetzt)

Manifest-Schema (plugin.json)

{
  "schemaVersion": 1,
  "id": "com.acme.slack-notifier",
  "version": "1.2.3",
  "name": "Slack Notifier",
  "description": "Sendet Tenant-Events an Slack-Channels",
  "author": { "name": "Acme Inc.", "email": "support@acme.com", "url": "https://acme.com" },
  "license": "MIT",
  "minHostVersion": "1.0.0",
  "maxHostVersion": "2.0.0",
  "capabilities": [
    { "name": "http.outbound", "scope": { "allowedHosts": ["hooks.slack.com"] } },
    { "name": "storage.read", "scope": {} },
    { "name": "storage.write", "scope": {} },
    { "name": "secret.read", "scope": { "keyPattern": "slack-webhook-url" } }
  ],
  "hooks": [
    { "name": "onTenantSignup", "export": "handleTenantSignup" },
    { "name": "onEntityCreate", "export": "handleEntityCreate", "filter": { "entity": "deal" } }
  ],
  "ui": [
    { "slot": "tenant-settings", "path": "ui/settings.html" }
  ],
  "binary": "plugin.wasm",
  "signature": "ed25519:<base64-signature>",
  "publisherKeyId": "acme-2026-01"
}

Ed25519-Signing + Publisher-Key-Registry

  • Publisher generieren ein Ed25519-Keypair lokal (z.B. via tc plugin keygen der CLI aus audit-docs.md).
  • Public Key wird mit publisherKeyId an Host-Operatoren übermittelt — Operator pflegt eine Publisher-Key-Registry (initial einfache JSON-Datei PLUGIN_REGISTRY_PUBLIC_KEYS_PATH, perspektivisch DB-backed). Marketplace-Layer ist out-of-scope.
  • Bei Plugin-Install: Host liest signature aus Manifest, holt Public Key via publisherKeyId aus Registry, verifiziert Signatur über binary + canonical-serialized Manifest-Subset ({id, version, capabilities, hooks, binary-hash}). Verify-Fail → Install-Abbruch.
  • Unsigned Plugins sind in Production verboten (PLUGIN_REQUIRE_SIGNATURE=true default in prod, default false nur in dev/test für lokale Entwicklung).

Capability-System

  • Jede Capability hat ein name (statisch enumeriert in packages/plugin-runtime/src/capabilities.ts) und ein optionales scope-Object mit capability-spezifischer Granularität (z.B. allowedHosts für http.outbound, keyPattern für secret.read).
  • Validator (packages/plugin-runtime/src/capability-validator.ts): nimmt Manifest entgegen, prüft jede deklarierte Capability gegen ein zentrales Schema (Zod), lehnt unbekannte Capabilities ab, normalisiert scope.
  • Consent-Dialog: wird in hooks-lifecycle.md (Task #22) gebaut — beim Plugin-Install zeigt Tenant-Admin-UI alle deklarierten Capabilities, Tenant-Admin muss explizit zustimmen. Consent wird in tenant_plugin_capability_grants Tabelle persistiert.
  • Enforcement: Bei jedem Host-Binding-Call prüft Main-Thread (nicht Worker-Thread!) gegen die granted Capabilities. Plugin kann nicht Capabilities zur Laufzeit erweitern.

Resource-Limits (pro Plugin-Call)

Limit Default Env-Override Mechanismus
CPU-Time (Wasmtime „fuel") 100 Mio. fuel-units (~100ms) PLUGIN_MAX_FUEL config.consume_fuel(true) + store.set_fuel(N)
Memory 64 MiB PLUGIN_MAX_MEMORY_MB Memory::new(MemoryType::new(min=1, max=1024)) (Pages × 64 KiB)
Wall-Clock 5000 ms PLUGIN_MAX_WALL_CLOCK_MS Worker-Thread setTimeout → worker.terminate() bei Überlauf
Outbound-HTTP-Bytes 10 MiB Request, 10 MiB Response PLUGIN_MAX_HTTP_BODY_MB Host-Binding limitiert Body-Size vor fetch
DB-Rows pro Query 1000 PLUGIN_MAX_DB_ROWS Host hängt LIMIT 1000 an Plugin-Queries
Plugin-Calls pro Tenant pro Minute 100 PLUGIN_RATE_LIMIT_PER_MINUTE SharedStore-basierter Sliding-Window-Counter

Überlauf → Plugin-Call abbricht, Audit-Log mit outcome=resource_exceeded, Tenant-Admin bekommt nach 3 Überläufen in 5 Min eine Notification.

Worker-Thread-Isolation

  • Pool-Größe: PLUGIN_WORKER_POOL_SIZE (default 8 pro worker-plugin-Container).
  • Pro Job: Worker-Thread aus Pool, eine frische wasmtime::Store-Instanz pro Plugin-Call, niemals Recycling der Wasmtime-Instance zwischen Plugin-IDs (Cross-Plugin-Leak-Vektor).
  • Worker-Thread crashed (z.B. uncaught WASM-Trap nicht in JS-Try-Catch fangbar) → Pool ersetzt ihn, Job geht in BullMQ-Retry mit max 3 Versuchen.
  • Worker-Thread kommuniziert mit Main-Thread via MessageChannel für Host-API-Calls; alle Calls sind Promise-based; Main-Thread macht die Capability-Checks und die Tenant-Scoping-Injection.

Implementation Steps

  1. Research-Spike Wasmtime vs Wasmer — Subagent vergleicht

    • Files: keine; Output ist eine docs/improvement-plan/06-plugin-system/RESEARCH-runtime-choice.md (temporär, wird nach Task-End wieder gelöscht oder in Lessons-Learned überführt).
    • Inhalt: API-Vergleich (Resource-Limits, Component-Model-Support, Node-Bindings-Reife), Performance-Benchmark (1000× Cold-Start einer trivialen WASM-Funktion), Lizenz-Check (Apache-2.0 OK, kein GPL).
    • Entscheidung mit Begründung + Tabelle. Default-Choice falls beide gleich: Wasmtime (siehe Approach).
    • Commit: 06-plugin-system/wasm-sandbox: research notes for runtime selection
  2. Package-Skelette anlegen — drei neue Packages

    • Files:
      • packages/plugin-runtime/package.json, tsconfig.json, src/index.ts (re-exports)
      • packages/plugin-sdk/package.json, tsconfig.json, src/index.ts, assembly/index.ts (AssemblyScript-Entry für Plugin-Autoren)
      • packages/plugin-manifest/package.json, src/schema.ts (Zod-Schema), src/index.ts
    • PNPM-Workspace-Eintrag (pnpm-workspace.yaml) anpassen.
    • Commit: 06-plugin-system/wasm-sandbox: scaffold plugin-runtime, plugin-sdk, plugin-manifest packages
  3. Manifest-Schema (Zod) + Validator — Pflicht-Felder, Versionsbereich, Capability-Schema

    • Files: packages/plugin-manifest/src/schema.ts (Zod-Schema mit schemaVersion, id als reverse-DNS-Pattern, version SemVer, capabilities Diskriminator-Union pro Capability-Name, etc.), packages/plugin-manifest/src/parse.ts (parseManifest(json) → Result<Manifest, Error[]>)
    • Tests: packages/plugin-manifest/src/schema.test.ts — valid/invalid Fixtures pro Capability, minHostVersion-Constraint-Check, Pflicht-Felder, unbekannte Capabilities werden abgelehnt.
    • Commit: 06-plugin-system/wasm-sandbox: add plugin manifest schema and parser
  4. Capability-Registry + Validator — Single Source of Truth für Capability-Namen

    • Files: packages/plugin-runtime/src/capabilities.ts (enum CAPABILITY_NAMES, Schema pro Capability für scope), packages/plugin-runtime/src/capability-validator.ts (validateCapability(name, scope) und isCapabilityGranted(grants, requested)).
    • Tests: packages/plugin-runtime/src/capability-validator.test.ts — Exhaustiveness: jede Capability aus CAPABILITY_NAMES ist im Manifest-Schema verzeichnet; isCapabilityGranted checkt korrekt Scope-Subsumption (z.B. http.outbound mit allowedHosts: [a.com] ist kein Superset von [a.com, b.com]).
    • Commit: 06-plugin-system/wasm-sandbox: add capability registry and validator
  5. Ed25519-Signing + Verify — Sign-Helper für SDK, Verify-Helper für Runtime

    • Files:
      • packages/plugin-manifest/src/signing.ts — signPlugin(binary, manifestSubset, privateKey) → returnt Signature-String, verifyPlugin(binary, manifestSubset, signature, publicKey) → boolean. Nutzt @noble/ed25519 (audited, no native deps).
      • packages/plugin-manifest/src/canonical.ts — canonicalManifestSubset(manifest) produziert deterministisches JSON für Signing (sorted keys, no whitespace).
    • Tests: packages/plugin-manifest/src/signing.test.ts — Round-Trip Sign→Verify, Verify-Fail bei modifiziertem Binary, Verify-Fail bei modifiziertem Manifest-Subset, Verify-Fail bei wrong public key.
    • Commit: 06-plugin-system/wasm-sandbox: add ed25519 signing helpers
  6. Publisher-Key-Registry — initial JSON-File-basiert

    • Files: packages/plugin-runtime/src/key-registry.ts — loadKeyRegistry(path) liest JSON { keys: [{ keyId, publicKey, publisher, addedAt, revokedAt? }] }, getPublicKey(keyId) → string | undefined, isKeyRevoked(keyId) → boolean.
    • .env.example-Eintrag PLUGIN_REGISTRY_PUBLIC_KEYS_PATH=./config/plugin-keys.json.
    • Beispiel-Datei config/plugin-keys.example.json mit Dummy-Key.
    • Tests: packages/plugin-runtime/src/key-registry.test.ts — Loading, Missing-Key, Revoked-Key.
    • Commit: 06-plugin-system/wasm-sandbox: add publisher key registry loader
  7. Wasmtime-Wrapper im Runtime-Package — Low-Level-Engine-Abstraction

    • Files: packages/plugin-runtime/src/engine.ts — createEngine(config) wrapped Wasmtime-Init mit fuel + epoch-interruption + memory-limits-Defaults, createInstance(engine, wasmBlob, manifest, hostBindings) → PluginInstance mit .call(hookName, input, { fuel, memoryMb, wallClockMs }).
    • WICHTIG: Diese Datei läuft im Worker-Thread, nicht im Main-Thread. Sie ist in packages/plugin-runtime/src/worker/ separiert.
    • Tests: packages/plugin-runtime/src/engine.test.ts — Fuel-Limit-Trip (Endlosschleife wird abgebrochen), Memory-Limit-Trip (Plugin allokiert > limit → Trap), Wall-Clock-Limit-Trip (Plugin sleept > limit → terminate).
    • Commit: 06-plugin-system/wasm-sandbox: add wasmtime engine wrapper with resource limits
  8. Worker-Thread-Pool — Pool für Plugin-Calls

    • Files:
      • packages/plugin-runtime/src/pool.ts — createPluginWorkerPool({ size, workerPath }), methods acquire(), release(worker), terminate(worker, reason), shutdown().
      • packages/plugin-runtime/src/worker/entry.ts — Worker-Thread-Entry, hört auf { wasmBlob, manifest, hookName, input, hostBindings }-Messages, ruft engine.ts auf, sendet { output | error } zurück.
    • Tests: packages/plugin-runtime/src/pool.test.ts — Pool acquire/release, Crash-Replace (terminate → fresh worker spawned), Backpressure (acquire wartet wenn alle busy).
    • Commit: 06-plugin-system/wasm-sandbox: add worker thread pool for plugin invocations
  9. Host-Bindings (Main-Thread-Seite) — Capability-Checks + Tenant-Scoping

    • Files: packages/plugin-runtime/src/host-bindings/ (eigenes Sub-Module)
      • http.ts — httpFetch(req, ctx) checkt http.outbound-Grant + Host-Allowlist + Body-Size; nutzt globaler undici-Dispatcher mit Timeout.
      • db.ts — dbQuery(sql, params, ctx) injiziert tenantId-Constraint, hängt LIMIT 1000 an, läuft via prismaRead.$queryRawUnsafe; dbExecute(sql, params, ctx) checkt plugin_<id>_*-Prefix für Tabellen, läuft via prismaWrite.
      • storage.ts — KV via SharedStore mit Prefix plugin:<id>:tenant:<tid>:; TTL default 24h, override via Manifest.
      • log.ts — Strukturiertes Logging mit plugin.id, plugin.version, tenant.id Tags.
      • audit.ts — Insert in audit_log mit actor.type=plugin, actor.id=<pluginId>.
      • secret.ts — Read via tenant_plugin_secrets Tabelle, scoped per Tenant + Plugin + Key-Pattern aus Capability.
      • tenant.ts — Read-only Tenant-Metadata.
    • Tests: pro Binding eine .test.ts — positive Path + negative Path (Capability fehlt → Error, Tenant-Scope-Bypass-Versuch → blockiert, Resource-Limit-Trip).
    • Commit: pro Binding ein commit (06-plugin-system/wasm-sandbox: add http host binding with capability + size limits usw.) — 7 Commits.
  10. Main-Thread Plugin-Invoker — Orchestriert Worker + Host-API-Calls

    • Files: packages/plugin-runtime/src/invoker.ts — invokePlugin({ pluginId, version, hookName, tenantId, input }) lädt Binary aus PluginStore (Filesystem-cache), schickt an Pool, registriert MessageChannel-Handler für Host-API-Calls, returnt Output. Setzt Rate-Limit-Counter in SharedStore.
    • Tests: packages/plugin-runtime/src/invoker.test.ts — Happy-Path, Rate-Limit-Exceeded (101. Call innerhalb Minute → Reject), Capability-Denied, Resource-Limit-Exceeded.
    • Commit: 06-plugin-system/wasm-sandbox: add main-thread plugin invoker with rate limiting
  11. PluginStore — Filesystem-Cache für Plugin-Binaries + Manifests

    • Files: packages/plugin-runtime/src/store.ts — getPluginBinary(pluginId, version) returnt Buffer; lädt aus var/plugins/<id>/<version>/plugin.wasm; cached in-Memory (lru-cache mit Größenlimit aus PLUGIN_BINARY_CACHE_MB).
    • Initialer Load aus DB-Tabelle installed_plugins (Schema kommt in hooks-lifecycle.md).
    • Tests: packages/plugin-runtime/src/store.test.ts — Cache-Hit/Miss, Eviction bei Größenlimit.
    • Commit: 06-plugin-system/wasm-sandbox: add plugin binary store with lru cache
  12. SDK-Package: AssemblyScript-Bindings — was Plugin-Autoren import werden

    • Files:
      • packages/plugin-sdk/assembly/index.ts — re-exportiert http, db, storage, log, audit, secret, tenant Module.
      • packages/plugin-sdk/assembly/http.ts — export function fetch(req: HttpRequest): HttpResponse (compiled-to-WASM, ruft Host-Import auf).
      • packages/plugin-sdk/assembly/db.ts etc.
      • packages/plugin-sdk/assembly/hooks.ts — Decorators / Helper damit Plugin export function handleTenantSignup(input: TenantSignupInput): void einfach schreiben kann.
    • packages/plugin-sdk/tsconfig.json mit AssemblyScript-Compiler-Config.
    • Tests: Compile-Smoke-Test eines Hello-World-Plugins (packages/plugin-sdk/test/fixtures/hello/), verifiziert das resultierende .wasm instanzierbar im Runtime ist.
    • Commit: 06-plugin-system/wasm-sandbox: add assemblyscript plugin sdk bindings
  13. SDK-Package: Rust-Crate (optional in Phase 06, später nachreichbar) — wenn Zeit

    • Files: packages/plugin-sdk-rust/Cargo.toml, src/lib.rs mit #[no_mangle] exports und WIT-Bindings.
    • Skip-Bar: falls Zeitknappheit, in audit-docs.md-Lessons-Learned als „Rust-SDK in Phase 11 Backlog" verschieben. AssemblyScript-First reicht für MVP.
    • Commit: 06-plugin-system/wasm-sandbox: add rust plugin sdk crate (optional)
  14. Worker-Plugin-Service — neuer Worker-Group plugin (passt zu worker-queues.md aus Phase 03)

    • Files:
      • packages/queue/src/groups.ts — add plugin group + zugehörige Queue plugin-invoke.
      • apps/worker/src/runtime.ts — Bootstrap for plugin-Group.
      • apps/worker/src/plugin-handlers.ts — Registriert BullMQ-Handler für plugin-invoke-Job: nimmt Payload, ruft invokePlugin aus packages/plugin-runtime auf.
      • docker-compose.yml — worker-plugin-Service mit WORKER_QUEUE_GROUP=plugin, WORKER_HEALTH_PORT=9105, eigene Resource-Limits.
    • Tests: apps/worker/src/plugin-handlers.test.ts — mocked invoker, assert dass Jobs korrekt dispatched werden.
    • Commit: 06-plugin-system/wasm-sandbox: add worker-plugin service for plugin invocations
  15. Sentry/OTel-Tagging — pro Plugin-Span Tags setzen

    • Files: packages/plugin-runtime/src/observability.ts — Wrapper der OTel-Span erstellt mit Attributes plugin.id, plugin.version, plugin.hook, plugin.tenant, plugin.capability (für jede Capability-Nutzung innerhalb des Calls eigener Sub-Span). Sentry-Tags äquivalent.
    • Integration in invoker.ts und alle host-bindings/*.ts.
    • Tests: packages/plugin-runtime/src/observability.test.ts — mock Sentry/OTel, assert Tag-Calls.
    • Commit: 06-plugin-system/wasm-sandbox: add sentry and otel tagging for plugin spans
  16. Doku — Operations + Developer-Overview (Developer-Reference kommt in audit-docs.md)

    • Files:
      • docs/operations/plugin-runtime.md — Architektur-Diagramm, Worker-Plugin-Sizing-Guide, Resource-Limit-Tuning, Troubleshooting (Stuck-Worker, Memory-Leak-Detection, Sandbox-Update-Process).
      • docs/features/plugins.md (Stub) — High-Level-Overview für Operators/Tenant-Admins, verweist auf hooks-lifecycle.md-Doku.
    • Commit: 06-plugin-system/wasm-sandbox: add operations docs for plugin runtime
  17. Smoke-Test-Plugin — minimales „Hello World"-Plugin als Integration-Test-Fixture

    • Files: packages/plugin-runtime/test/fixtures/hello-plugin/ mit Manifest + AssemblyScript-Source + gebautem WASM + Test-Signing-Key.
    • Integration-Test in packages/plugin-runtime/test/integration/end-to-end.test.ts: load → verify-signature → invoke → assert output.
    • Commit: 06-plugin-system/wasm-sandbox: add hello-plugin fixture and end-to-end integration test

Test Requirements

  • Unit-Tests (per Implementation-Step oben aufgelistet):

    • packages/plugin-manifest/src/schema.test.ts — Valid/Invalid-Fixtures, mind. 15 Cases.
    • packages/plugin-manifest/src/signing.test.ts — Round-Trip, Tamper-Detection (Binary + Manifest), Wrong-Key.
    • packages/plugin-runtime/src/capability-validator.test.ts — Exhaustiveness, Scope-Subsumption.
    • packages/plugin-runtime/src/key-registry.test.ts — Load, Missing, Revoked.
    • packages/plugin-runtime/src/engine.test.ts — Fuel-Trip, Memory-Trip, Wall-Clock-Trip (kritisch).
    • packages/plugin-runtime/src/pool.test.ts — Acquire/Release, Crash-Replace, Backpressure.
    • packages/plugin-runtime/src/host-bindings/*.test.ts — pro Binding positive + negative Path.
    • packages/plugin-runtime/src/invoker.test.ts — Happy-Path, Rate-Limit, Capability-Denied, Resource-Exceeded.
    • packages/plugin-runtime/src/store.test.ts — Cache.
    • packages/plugin-runtime/src/observability.test.ts — Tagging.
  • Integration-Tests:

    • packages/plugin-runtime/test/integration/end-to-end.test.ts — Hello-Plugin: Manifest-Verify → Engine-Load → Invoke → Output-Assert.
    • packages/plugin-runtime/test/integration/resource-limits.integration.test.ts:
      • Endlosschleife (while(true){}) → Fuel-Trip in < 200ms, kein Worker-Crash, Pool wieder bereit.
      • Memory-Bomb (Plugin allokiert 256 MiB bei 64 MiB Limit) → Trap, Worker terminiert, Pool ersetzt.
      • Wall-Clock-Bomb (Plugin macht synchronen Sleep > 5s) → Worker terminiert, Job retried max 3×, dann failed.
    • packages/plugin-runtime/test/integration/capability-enforcement.integration.test.ts:
      • Plugin ohne http.outbound-Grant ruft http.fetch → Reject mit capability_denied.
      • Plugin mit http.outbound aber andere Host als Allowlist → Reject mit host_not_allowed.
      • Plugin ohne db.write ruft db.execute → Reject.
      • Plugin mit db.write schreibt auf Tabelle ohne plugin_<id>_-Prefix → Reject.
      • Plugin liest aus Tenant A's Daten via direktem SQL-Bypass-Versuch (WHERE tenantId = '<other>' injizieren) → Host-Binding sanitized und scoped trotzdem auf Caller-Tenant.
    • packages/plugin-runtime/test/integration/sandbox-escape.integration.test.ts — Sandbox-Escape-Tests (kritisch für Security-Review):
      • WASM mit wasi:filesystem Import-Versuch → Instantiation-Fail (Host gibt diese Imports nicht).
      • WASM mit raw memory.grow über Limit → Trap.
      • WASM versucht Host-Function-Pointer zu konstruieren → nicht möglich im Component-Model.
      • Plugin-Binary mit modifizierten Imports nach Signing → Verify-Fail.
      • Plugin sendet via http.fetch Request mit file:// oder localhost URL → Reject (Allowlist hat keine Loopbacks/Local-Scheme).
      • Plugin versucht via secret.get mit Wildcard-Key (*) zu lesen → Reject (Capability-Scope erlaubt nur exakte Pattern).
      • Plugin versucht Memory-Region eines anderen gleichzeitig laufenden Plugins zu lesen → unmöglich (separate Wasmtime-Stores).
  • E2E-Tests (Playwright): Keine in dieser Task — UI kommt erst in hooks-lifecycle.md (#22).

  • Coverage-Target: ≥ 90% für packages/plugin-runtime/src/ (Sicherheits-kritischer Code), ≥ 85% für packages/plugin-manifest/src/, ≥ 80% für packages/plugin-sdk/.

  • Test-Files: wie oben aufgelistet, gespiegelt unter test/integration/ für Integration-Tests.

NICHT als completed markieren bevor alle Tests grün sind — insbesondere ALLE Sandbox-Escape-Tests müssen passieren. Bei auch nur einem fehlgeschlagenen Escape-Test: Task blockiert, BLOCKERS.md füllen.

Acceptance Criteria

  • Research-Spike abgeschlossen, Runtime-Choice (Wasmtime/Wasmer) dokumentiert mit Begründung
  • packages/plugin-runtime/, packages/plugin-sdk/, packages/plugin-manifest/ existieren, exportieren stabile APIs
  • Manifest-Schema validiert via Zod, unbekannte/invalide Manifests werden abgelehnt
  • Ed25519-Sign + Verify funktioniert, Tamper-Detection bewiesen in Tests
  • Publisher-Key-Registry seedable per JSON-File, revoked Keys werden abgelehnt
  • Capability-Validator akzeptiert nur enumerierte Capabilities, Scope wird normalisiert
  • Wasmtime-Engine (oder Wasmer) instantiiert WASM-Module mit Fuel-/Memory-/Wall-Clock-Limits; Limits werden in Tests bewiesen
  • Worker-Thread-Pool spawned/recycled korrekt, Crash → Replace
  • Alle 7 Host-Bindings (http, db.read, db.write, storage.read/write, log, audit, secret, tenant) implementiert + Capability-geschützt + Tenant-scoped
  • Plugin-Invoker setzt SharedStore-Rate-Limit, lehnt > Limit-Calls ab
  • worker-plugin-Service im docker-compose.yml + bootstraped wie worker-queues.md Pattern
  • Sentry + OTel Tags pro Plugin-Span gesetzt
  • Hello-Plugin-Fixture läuft End-to-End grün
  • ALLE Sandbox-Escape-Tests grün (Mandatory — keine Ausnahme)
  • ALLE Resource-Limit-Tests grün
  • ALLE Capability-Enforcement-Tests grün
  • Coverage-Targets erreicht
  • pnpm lint, pnpm typecheck, pnpm test grün
  • docs/operations/plugin-runtime.md + docs/features/plugins.md (Stub) angelegt
  • .env.example ergänzt: PLUGIN_REGISTRY_PUBLIC_KEYS_PATH, PLUGIN_MAX_FUEL, PLUGIN_MAX_MEMORY_MB, PLUGIN_MAX_WALL_CLOCK_MS, PLUGIN_MAX_HTTP_BODY_MB, PLUGIN_MAX_DB_ROWS, PLUGIN_RATE_LIMIT_PER_MINUTE, PLUGIN_WORKER_POOL_SIZE, PLUGIN_BINARY_CACHE_MB, PLUGIN_REQUIRE_SIGNATURE
  • Staging-Deployment verifiziert: worker-plugin läuft, Hello-Plugin invokable über Test-Endpoint
  • Detail-MD Progress Log aktualisiert
  • PROGRESS.md Status auf completed

Rollback Strategy

  • Code-Rollback: Commits revertieren in umgekehrter Step-Reihenfolge. Da Plugin-Runtime ein eigenes Package-Set ist, hat ein Revert minimale Blast-Radius im Rest der Codebase (außer dem worker-plugin-Service in docker-compose, der gestoppt werden muss).
  • Worker-Stop: docker compose stop worker-plugin && docker compose rm -f worker-plugin entfernt den Service. Job-Queue plugin-invoke läuft leer aber blockiert nichts anderes.
  • DB-Migrations: In dieser Task werden keine Tabellen erstellt — das passiert in hooks-lifecycle.md (#22). Daher kein DB-Rollback nötig.
  • Production-Gate / Kill: Der Legacy-Runtime-Schalter ist nicht mehr der aktuelle Gate. Production-Plugin-Ausführung bleibt blockiert, solange PLUGIN_RUNTIME_PRODUCTION_ENABLED unset/false ist. Für einen Notfall-Stopp worker-plugin in Coolify stoppen oder auf 0 skalieren, toolctrl.plugin-invoke pausieren/drainen, und betroffene Tenant-Plugins über Lifecycle UI/API deaktivieren.
  • In-Flight-Calls: Worker-Thread-Pool wird beim Stop sauber gedraint (SIGTERM → 30s grace → SIGKILL). In-Flight-Calls werden via BullMQ-Retry später nochmal versucht — bei revertem Code werden sie failen und in Dead-Letter-Queue landen.
  • Plugin-Binaries auf Disk: var/plugins/ Verzeichnis kann gelöscht werden ohne Schaden — wird beim nächsten Re-Install aus DB-Source neu hydratisiert.

Security Notes

Diese Task hat die höchsten Security-Stakes der gesamten Boilerplate. Sandbox-Escape = Multi-Tenant-Datenleck = Game-Over.

  • Sandbox-Boundary: WebAssembly garantiert Memory-Safety + keine Native-Calls per Default. Aber: Sandbox-Escape ist möglich durch (a) Host-Import-Bugs (z.B. unbounded String-Read aus WASM-Memory ohne Length-Check), (b) Resource-Limit-Bypass (Fuel-Counter manipulierbar wenn nicht epoch-based), (c) Component-Model-Bugs in jco/wit-bindgen. Mitigations: alle Host-Bindings nutzen die typed Component-Model-API (kein raw Memory-Access), Fuel UND Epoch-Interrupt UND Wall-Clock-Timeout (defense-in-depth). Externe Security-Review (Task #23) MUSS dieses bewerten bevor Production-Rollout.

  • Capability-System: Principle of Least Privilege. Default-Deny — Plugin bekommt keine Capability ohne explizites Manifest-Declare + Tenant-Admin-Consent. Capabilities können zur Laufzeit nicht erweitert werden (kein requestPermission-Pattern à la Browser). Scope ist Pflicht für sensitive Capabilities (http.outbound → Host-Allowlist, secret.read → Key-Pattern).

  • Tenant-Isolation: Plugin-Calls sind IMMER scoped per Tenant. Tenant-ID wird vom Host injiziert (nie vom Plugin geliefert). DB-Capabilities re-write SQL um WHERE tenantId = $tenant zu erzwingen. Storage-Capabilities prefix-en Keys mit plugin:<id>:tenant:<tid>:. Audit-Logs sind tenant-scoped. Test: Capability-Enforcement-Test simuliert Tenant-Bypass-Versuche.

  • Signing: Ed25519 ist current-state-of-the-art (kein RSA, kein ECDSA — weniger Implementation-Bugs). @noble/ed25519 ist audited. Public-Key-Registry ist OP-controlled, nicht Tenant-controlled. Key-Rotation: Publisher rotieren Keys regelmäßig (siehe Compatibility-Matrix in audit-docs.md); revoked Keys werden in Registry markiert, Plugins die damit signiert sind werden beim nächsten Start abgelehnt.

  • Resource-Limits: Multiple Lagen — Fuel (CPU), Memory.max (RAM), Wall-Clock (Real-Time), Body-Size (Network-Bytes), DB-Row-Limit (DB-Bytes), Rate-Limit (Calls/min). Ein Plugin das alle Limits erschöpft kostet einen Worker-Thread für max 5s und N×64 MiB RAM — sustained Worst-Case ist PLUGIN_WORKER_POOL_SIZE × PLUGIN_MAX_MEMORY_MB, also planbar.

  • Worker-Process-Isolation: worker-plugin-Container läuft mit eigenen K8s/Docker-Resource-Limits (CPU + RAM Cap), separate Linux-User, kein Network-Egress außer zu Postgres/Redis/Loki und Plugin-Allowlist-Targets. Empfehlung für Production: seccomp-Profile + AppArmor-Profile auf den Container anwenden (in docs/operations/plugin-runtime.md dokumentieren).

  • Plugin-Binary-Storage: Binaries werden vor Disk-Schreibe nochmal verifiziert. var/plugins/ Verzeichnis ist read-only für worker-plugin-User außer beim Install-Step.

  • Sensitive Data in Plugin-Output: Plugin-Output landet in Audit-Log und potenziell in OTel-Spans. Sanitization: Output wird vor Logging durch einen Redaktion-Filter (sanitizePluginOutput) geleitet der bekannte Sensitive-Patterns (Credit-Card-Patterns, JWT-Patterns, Email-Patterns hinter Capability-Gate) maskiert. In audit-docs.md weiter ausgebaut.

  • Sandbox-Escape-Detection: Bei jedem Trap oder unexpected Worker-Crash → automatische Alert an Sentry mit severity=critical + plugin.id-Tag. Operator-On-Call-Workflow in docs/operations/plugin-runtime.md dokumentiert.

  • No Eval, No Dynamic Code: WASM ist statisch — Plugin kann kein neues WASM-Modul zur Laufzeit instantiieren (kein WebAssembly.instantiate aus dem Plugin). Bestätigt durch Test (Plugin das via SDK versucht ein zweites Modul zu laden → Compile-Error).

  • Security-Review als Blocker: Externe Security-Review ist Acceptance-Criterion in audit-docs.md (#23). Production-Rollout passiert NICHT vor Sign-off.

Subagent Recommendations

  • Planning: Plan-Subagent mit dieser Detail-MD als Input → produziert Subtask-Mapping auf die 17 Steps + identifiziert Parallelisierbarkeit innerhalb der Steps (z.B. Steps 9.x — die 7 Host-Bindings — könnten von 2-3 Subagents parallel implementiert werden).
  • Research (PFLICHT für Step 1): Zwei Explore-Subagents parallel:
    • Subagent A: „Vergleich Wasmtime vs Wasmer für unseren Use-Case. Recherchiere offizielle Docs, Bytecode-Alliance-Statements, real-world Adoption (Fastly Compute, Cloudflare Workers, Shopify Functions), Benchmarks. Bewerte: Resource-Limit-API-Stabilität, Component-Model-Support, Node.js-Bindings-Reife, Lizenz, Active-Maintenance (Commits/Monat). Output: Empfehlung mit Tabelle in RESEARCH-runtime-choice.md."
    • Subagent B: „Recherche Best-Practices für WASM-Sandboxing in Production. Insbesondere: bekannte CVEs in Wasmtime + Wasmer, Empfehlungen aus dem WebAssembly-Security-Group, Industry-Patterns (wie machen es Cloudflare Workers, Shopify Functions, Suborbital). Output: Liste von Security-Empfehlungen, die in unseren Implementation einfließen müssen."
  • Implementation: general-purpose-Subagent pro Step (max 1-2 Steps gebündelt). Klarer Brief: „Implementiere Step N nach Detail-MD wasm-sandbox.md, atomic commit am Ende, nichts darüber hinaus." Für Steps 9.x (Host-Bindings) gleichzeitig 3 Subagents.
  • Tests: Separater general-purpose-Subagent pro Test-Cluster (Unit für Manifest/Signing, Unit für Engine/Pool, Integration für Resource-Limits, Integration für Capabilities, Integration für Sandbox-Escape). Der Sandbox-Escape-Test-Subagent bekommt expliziten Brief: „Versuche maximal kreativ Sandbox zu brechen — denke wie ein Angreifer."
  • Verification: gsd-verifier-Subagent läuft die Acceptance-Criteria-Checkliste durch + macht eine zweite Pass über die Sandbox-Escape-Tests.
  • Code-Review: gsd-code-reviewer nach jedem Step-Batch (nicht erst am Ende — bei 17 Steps und Security-kritischem Code lohnt sich häufige Review).

Staging-Deployment-Steps

  1. Branch agent/06-wasm-sandbox pushen.
  2. Coolify-Auto-Deploy auf Staging triggert.
  3. Vor erstem Deploy: In Coolify-Compose den neuen worker-plugin-Service aktivieren (Steps siehe worker-queues.md Pattern). Volume-Mount für var/plugins/:
    services:
      worker-plugin:
        <<: *worker-base
        environment:
          WORKER_QUEUE_GROUP: plugin
          WORKER_CONCURRENCY_PLUGIN: 4
          WORKER_HEALTH_PORT: 9105
          PLUGIN_REGISTRY_PUBLIC_KEYS_PATH: /app/config/plugin-keys.json
          PLUGIN_MAX_FUEL: 100000000
          PLUGIN_MAX_MEMORY_MB: 64
          PLUGIN_MAX_WALL_CLOCK_MS: 5000
          PLUGIN_WORKER_POOL_SIZE: 8
          PLUGIN_REQUIRE_SIGNATURE: "true"
        volumes:
          - plugin-binaries:/app/var/plugins:rw
          - ./config/plugin-keys.json:/app/config/plugin-keys.json:ro
        deploy:
          resources:
            limits:
              cpus: '2'
              memory: 2G
        deploy: { replicas: 1 }
    volumes:
      plugin-binaries:
    
  4. Test-Publisher-Keypair generieren (lokal mit dem Sign-Helper aus Step 5):
    pnpm tsx packages/plugin-manifest/scripts/gen-keypair.ts --out ./tmp/test-publisher
    # output: tmp/test-publisher.priv + tmp/test-publisher.pub
    
  5. Public Key in Staging-Registry eintragen (config/plugin-keys.json):
    {
      "keys": [
        { "keyId": "staging-test-2026-05", "publicKey": "<base64>", "publisher": "staging-test", "addedAt": "2026-05-16T00:00:00Z" }
      ]
    }
    
  6. Hello-Plugin signieren + auf Staging-Filesystem droppen (manuell für diese Task — automatisierter Install kommt in hooks-lifecycle.md):
    pnpm tsx packages/plugin-manifest/scripts/sign-plugin.ts \
      --binary packages/plugin-runtime/test/fixtures/hello-plugin/plugin.wasm \
      --manifest packages/plugin-runtime/test/fixtures/hello-plugin/plugin.json \
      --private-key ./tmp/test-publisher.priv \
      --key-id staging-test-2026-05 \
      --out ./tmp/hello-plugin-signed/
    scp -r ./tmp/hello-plugin-signed/ staging-host:/var/lib/toolctrl/plugins/com.toolctrl.hello/1.0.0/
    
  7. Smoke-Test via Test-Endpoint (temporärer /api/internal/plugin-test/invoke Endpoint nur im Staging-Build):
    curl -X POST https://staging.toolctrl.de/api/internal/plugin-test/invoke \
      -H "X-Internal-Test-Token: $INTERNAL_TOKEN" \
      -d '{"pluginId":"com.toolctrl.hello","version":"1.0.0","hookName":"sayHello","input":{"name":"World"}}'
    # erwartet: { "output": { "greeting": "Hello, World!" } }
    
  8. Resource-Limit-Smoke-Test mit endless-loop-plugin-Fixture:
    curl -X POST https://staging.toolctrl.de/api/internal/plugin-test/invoke \
      -d '{"pluginId":"com.toolctrl.endless-loop","version":"1.0.0","hookName":"loop","input":{}}'
    # erwartet: HTTP 408 oder 422 mit { "error": "fuel_exhausted" } innerhalb < 500ms
    
  9. Capability-Enforcement-Smoke-Test mit unauthorized-http-plugin-Fixture (Plugin ohne http.outbound-Capability versucht fetch):
    curl -X POST … '{"pluginId":"com.toolctrl.unauth-http","version":"1.0.0","hookName":"call","input":{}}'
    # erwartet: { "error": "capability_denied", "capability": "http.outbound" }
    
  10. Coolify-Health-Check für worker-plugin aktivieren (/readyz als Readiness-Probe auf Port 9105).
  11. Sentry-Dashboard nach 30 Min prüfen: Plugin-Spans sichtbar mit Tags plugin.id=com.toolctrl.hello, plugin.hook=sayHello.
  12. Logs prüfen: worker-plugin-Container logged jede Plugin-Invocation strukturiert.
  13. Vor Production: Test-Endpoint /api/internal/plugin-test/invoke muss aus Production-Build ausgeschlossen sein (env-gated if (process.env.NODE_ENV !== 'production')).

Deviations from Plan

Foundation-only delivery — every item below is intentional scope reduction for the first cut. Status: in_progress (foundation-shipped, hot-reload + marketplace + per-instruction-fuel + worker-thread-pool deferred).

  • Runtime: Node 22's built-in WebAssembly (V8), not Wasmtime. Native Wasmtime bindings (@bytecodealliance/jco, @bytecodealliance/preview2-shim) are pre-1.0 and add a ~40 MB asset. The foundation wraps the V8 WebAssembly global behind a PluginSandbox interface that Wasmer or Wasmtime can drop into later without API change. Per-instruction fuel is approximated by charging host-fn calls through the capability gateway — V8 has no public hook for per-instruction metering.
  • No worker-thread pool. The current worker path uses a fresh killable worker thread per invocation, but there is no reusable pool or PLUGIN_WORKER_POOL_SIZE tuning yet. Pooling remains an upgrade path alongside stronger per-instruction fuel.
  • Manifest signed-by is a hex Ed25519 public key, not a publisherKeyId lookup. The trusted-keys allowlist is a CSV env var (PLUGIN_TRUSTED_PUBLIC_KEYS), not a JSON registry file. Key-rotation tooling lands in #23.
  • Single SDK (TypeScript). AssemblyScript / Rust bindings are deferred to #23. The hello-world example documents the two practical compile paths plugin authors can use today.
  • CLI build/sign/publish shipped as toolctrl-cli plugins .... The current author/operator surface is documented in docs/plugins/cli-reference.md: scaffold, build, sign, lint, and publish are available for artifact preparation, while list, enable, disable, and uninstall are operator escape hatches. build prints the detected toolchain command instead of executing it; publish creates a local tarball only and does not upload to a marketplace.
  • No hot-reload. Operators restart the worker-plugin pod to pick up new binaries. Hot-reload + watch mode land in a follow-up.
  • No marketplace integration. Tracked under backlog #24.
  • No guided consent-dialog UI. Per-tenant capability grants exist and enablement requires grants unless the dev/test opt-out is set. Tenant settings can approve missing manifest-declared grants with an explicit review checkbox plus server-side manifest digest; the fuller guided install/consent dialog remains owned by #22 follow-up work.
  • No structured-payload marshalling. The sandbox accepts scalar payloads (i32/i64); structured JSON marshalling via shared memory is part of the SDK toolchain work in #23.
  • No Sentry / OTel span tagging. Structured logger seam ships in handlePluginInvokeJob; OTel spans land alongside the audit-docs work in #23.
  • No staging-deployment of a real plugin. The hello-world example documents the install shape; signing + uploading a real binary requires the deferred CLI to be ergonomic.

Progress Log

  • 2026-05-17 — 8ea14d9f — 06/wasm-sandbox: scaffold packages/plugin-runtime and plugin-sdk
  • 2026-05-17 — 30fdae57 — 06/wasm-sandbox: raise brand-literal baseline to absorb parallel-agent drift
  • 2026-05-17 — 7295cbce — 06/wasm-sandbox: add plugin manifest zod schema
  • 2026-05-17 — 2cf64338 — 06/wasm-sandbox: add ed25519 signature verifier
  • 2026-05-17 — e9b09c02 — 06/wasm-sandbox: add wasmtime-style runtime wrapper with limits
  • 2026-05-17 — 0c6860a8 — 06/wasm-sandbox: add capability gateway for host-api calls
  • 2026-05-17 — 5b480fe7 — 06/wasm-sandbox: add Plugin prisma model and migration
  • 2026-05-17 — 7f3eaed5 — 06/wasm-sandbox: add plugin worker queue group
  • 2026-05-17 — 23d5cebe — 06/wasm-sandbox: wire plugin-invoke into queue payload + retry maps
  • 2026-05-17 — c15b4b33 — 06/wasm-sandbox: add plugin-invoke worker job
  • 2026-05-17 — 5b89ad50 — 06/wasm-sandbox: add plugin-sdk types and hello-world example
  • 2026-05-17 — 62c36976 — 06/wasm-sandbox: raise i18n baselines for parallel-agent drift
  • 2026-05-17 — 7f3fd230 — 06/wasm-sandbox: document wasm sandbox foundation

Lessons Learned

(Optional, am Ende des Tasks — insbesondere: Wasmtime- oder Wasmer-Entscheidung, AssemblyScript-DX-Erfahrungen, Resource-Limit-Tuning-Erkenntnisse, gefundene Sandbox-Edge-Cases.)