47 KiB
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, anddocs/security/plugin-runtime-signoff.md. The shipped foundation runtime is production-gated; only reviewedlogandaudithost 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 dediziertenworker-plugin-Pool. Plugin-Invocations laufen niemals inworker-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-Tagsplugin.id,plugin.version,plugin.capabilitymü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
logandaudithost 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 keygender CLI ausaudit-docs.md). - Public Key wird mit
publisherKeyIdan Host-Operatoren übermittelt — Operator pflegt eine Publisher-Key-Registry (initial einfache JSON-DateiPLUGIN_REGISTRY_PUBLIC_KEYS_PATH, perspektivisch DB-backed). Marketplace-Layer ist out-of-scope. - Bei Plugin-Install: Host liest
signatureaus Manifest, holt Public Key viapublisherKeyIdaus Registry, verifiziert Signatur überbinary+ canonical-serialized Manifest-Subset ({id, version, capabilities, hooks, binary-hash}). Verify-Fail → Install-Abbruch. - Unsigned Plugins sind in Production verboten (
PLUGIN_REQUIRE_SIGNATURE=truedefault inprod, defaultfalsenur indev/testfür lokale Entwicklung).
Capability-System
- Jede Capability hat ein
name(statisch enumeriert inpackages/plugin-runtime/src/capabilities.ts) und ein optionalesscope-Object mit capability-spezifischer Granularität (z.B.allowedHostsfürhttp.outbound,keyPatternfürsecret.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, normalisiertscope. - 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 intenant_plugin_capability_grantsTabelle 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 proworker-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
MessageChannelfür Host-API-Calls; alle Calls sind Promise-based; Main-Thread macht die Capability-Checks und die Tenant-Scoping-Injection.
Implementation Steps
-
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
- Files: keine; Output ist eine
-
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
- Files:
-
Manifest-Schema (Zod) + Validator — Pflicht-Felder, Versionsbereich, Capability-Schema
- Files:
packages/plugin-manifest/src/schema.ts(Zod-Schema mitschemaVersion,idals reverse-DNS-Pattern,versionSemVer,capabilitiesDiskriminator-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
- Files:
-
Capability-Registry + Validator — Single Source of Truth für Capability-Namen
- Files:
packages/plugin-runtime/src/capabilities.ts(enumCAPABILITY_NAMES, Schema pro Capability fürscope),packages/plugin-runtime/src/capability-validator.ts(validateCapability(name, scope)undisCapabilityGranted(grants, requested)). - Tests:
packages/plugin-runtime/src/capability-validator.test.ts— Exhaustiveness: jede Capability ausCAPABILITY_NAMESist im Manifest-Schema verzeichnet;isCapabilityGrantedcheckt korrekt Scope-Subsumption (z.B.http.outboundmitallowedHosts: [a.com]ist kein Superset von[a.com, b.com]). - Commit:
06-plugin-system/wasm-sandbox: add capability registry and validator
- Files:
-
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
- Files:
-
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-EintragPLUGIN_REGISTRY_PUBLIC_KEYS_PATH=./config/plugin-keys.json.- Beispiel-Datei
config/plugin-keys.example.jsonmit 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
- Files:
-
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)→PluginInstancemit.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
- Files:
-
Worker-Thread-Pool — Pool für Plugin-Calls
- Files:
packages/plugin-runtime/src/pool.ts—createPluginWorkerPool({ size, workerPath }), methodsacquire(),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, ruftengine.tsauf, 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
- Files:
-
Host-Bindings (Main-Thread-Seite) — Capability-Checks + Tenant-Scoping
- Files:
packages/plugin-runtime/src/host-bindings/(eigenes Sub-Module)http.ts—httpFetch(req, ctx)checkthttp.outbound-Grant + Host-Allowlist + Body-Size; nutzt globalerundici-Dispatcher mit Timeout.db.ts—dbQuery(sql, params, ctx)injizierttenantId-Constraint, hängtLIMIT 1000an, läuft viaprismaRead.$queryRawUnsafe;dbExecute(sql, params, ctx)checktplugin_<id>_*-Prefix für Tabellen, läuft viaprismaWrite.storage.ts— KV via SharedStore mit Prefixplugin:<id>:tenant:<tid>:; TTL default 24h, override via Manifest.log.ts— Strukturiertes Logging mitplugin.id,plugin.version,tenant.idTags.audit.ts— Insert inaudit_logmitactor.type=plugin,actor.id=<pluginId>.secret.ts— Read viatenant_plugin_secretsTabelle, 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 limitsusw.) — 7 Commits.
- Files:
-
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
- Files:
-
PluginStore — Filesystem-Cache für Plugin-Binaries + Manifests
- Files:
packages/plugin-runtime/src/store.ts—getPluginBinary(pluginId, version)returnt Buffer; lädt ausvar/plugins/<id>/<version>/plugin.wasm; cached in-Memory (lru-cachemit Größenlimit ausPLUGIN_BINARY_CACHE_MB). - Initialer Load aus DB-Tabelle
installed_plugins(Schema kommt inhooks-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
- Files:
-
SDK-Package: AssemblyScript-Bindings — was Plugin-Autoren
importwerden- Files:
packages/plugin-sdk/assembly/index.ts— re-exportierthttp,db,storage,log,audit,secret,tenantModule.packages/plugin-sdk/assembly/http.ts—export function fetch(req: HttpRequest): HttpResponse(compiled-to-WASM, ruft Host-Import auf).packages/plugin-sdk/assembly/db.tsetc.packages/plugin-sdk/assembly/hooks.ts— Decorators / Helper damit Pluginexport function handleTenantSignup(input: TenantSignupInput): voideinfach schreiben kann.
packages/plugin-sdk/tsconfig.jsonmit AssemblyScript-Compiler-Config.- Tests: Compile-Smoke-Test eines Hello-World-Plugins (
packages/plugin-sdk/test/fixtures/hello/), verifiziert das resultierende.wasminstanzierbar im Runtime ist. - Commit:
06-plugin-system/wasm-sandbox: add assemblyscript plugin sdk bindings
- Files:
-
SDK-Package: Rust-Crate (optional in Phase 06, später nachreichbar) — wenn Zeit
- Files:
packages/plugin-sdk-rust/Cargo.toml,src/lib.rsmit#[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)
- Files:
-
Worker-Plugin-Service — neuer Worker-Group
plugin(passt zuworker-queues.mdaus Phase 03)- Files:
packages/queue/src/groups.ts— addplugingroup + zugehörige Queueplugin-invoke.apps/worker/src/runtime.ts— Bootstrap forplugin-Group.apps/worker/src/plugin-handlers.ts— Registriert BullMQ-Handler fürplugin-invoke-Job: nimmt Payload, ruftinvokePluginauspackages/plugin-runtimeauf.docker-compose.yml—worker-plugin-Service mitWORKER_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
- Files:
-
Sentry/OTel-Tagging — pro Plugin-Span Tags setzen
- Files:
packages/plugin-runtime/src/observability.ts— Wrapper der OTel-Span erstellt mit Attributesplugin.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.tsund allehost-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
- Files:
-
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 aufhooks-lifecycle.md-Doku.
- Commit:
06-plugin-system/wasm-sandbox: add operations docs for plugin runtime
- Files:
-
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
- Files:
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.
- Endlosschleife (
packages/plugin-runtime/test/integration/capability-enforcement.integration.test.ts:- Plugin ohne
http.outbound-Grant rufthttp.fetch→ Reject mitcapability_denied. - Plugin mit
http.outboundaber andere Host als Allowlist → Reject mithost_not_allowed. - Plugin ohne
db.writeruftdb.execute→ Reject. - Plugin mit
db.writeschreibt auf Tabelle ohneplugin_<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.
- Plugin ohne
packages/plugin-runtime/test/integration/sandbox-escape.integration.test.ts— Sandbox-Escape-Tests (kritisch für Security-Review):- WASM mit
wasi:filesystemImport-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.fetchRequest mitfile://oderlocalhostURL → Reject (Allowlist hat keine Loopbacks/Local-Scheme). - Plugin versucht via
secret.getmit 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).
- WASM mit
-
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ürpackages/plugin-manifest/src/, ≥ 80% fürpackages/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 wieworker-queues.mdPattern- 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 testgründocs/operations/plugin-runtime.md+docs/features/plugins.md(Stub) angelegt.env.exampleergä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-pluginlä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-pluginentfernt den Service. Job-Queueplugin-invokelä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_ENABLEDunset/falseist. Für einen Notfall-Stoppworker-pluginin Coolify stoppen oder auf 0 skalieren,toolctrl.plugin-invokepausieren/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 = $tenantzu erzwingen. Storage-Capabilities prefix-en Keys mitplugin:<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/ed25519ist audited. Public-Key-Registry ist OP-controlled, nicht Tenant-controlled. Key-Rotation: Publisher rotieren Keys regelmäßig (siehe Compatibility-Matrix inaudit-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 (indocs/operations/plugin-runtime.mddokumentieren). -
Plugin-Binary-Storage: Binaries werden vor Disk-Schreibe nochmal verifiziert.
var/plugins/Verzeichnis ist read-only fürworker-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. Inaudit-docs.mdweiter ausgebaut. -
Sandbox-Escape-Detection: Bei jedem
Trapoder unexpected Worker-Crash → automatische Alert an Sentry mitseverity=critical+plugin.id-Tag. Operator-On-Call-Workflow indocs/operations/plugin-runtime.mddokumentiert. -
No Eval, No Dynamic Code: WASM ist statisch — Plugin kann kein neues WASM-Modul zur Laufzeit instantiieren (kein
WebAssembly.instantiateaus 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."
- 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
- 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-reviewernach jedem Step-Batch (nicht erst am Ende — bei 17 Steps und Security-kritischem Code lohnt sich häufige Review).
Staging-Deployment-Steps
- Branch
agent/06-wasm-sandboxpushen. - Coolify-Auto-Deploy auf Staging triggert.
- Vor erstem Deploy: In Coolify-Compose den neuen
worker-plugin-Service aktivieren (Steps sieheworker-queues.mdPattern). Volume-Mount fürvar/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: - 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 - 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" } ] } - 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/ - Smoke-Test via Test-Endpoint (temporärer
/api/internal/plugin-test/invokeEndpoint 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!" } } - 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 - Capability-Enforcement-Smoke-Test mit
unauthorized-http-plugin-Fixture (Plugin ohnehttp.outbound-Capability versuchtfetch):curl -X POST … '{"pluginId":"com.toolctrl.unauth-http","version":"1.0.0","hookName":"call","input":{}}' # erwartet: { "error": "capability_denied", "capability": "http.outbound" } - Coolify-Health-Check für
worker-pluginaktivieren (/readyzals Readiness-Probe auf Port 9105). - Sentry-Dashboard nach 30 Min prüfen: Plugin-Spans sichtbar mit Tags
plugin.id=com.toolctrl.hello,plugin.hook=sayHello. - Logs prüfen:
worker-plugin-Container logged jede Plugin-Invocation strukturiert. - Vor Production: Test-Endpoint
/api/internal/plugin-test/invokemuss aus Production-Build ausgeschlossen sein (env-gatedif (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 V8WebAssemblyglobal behind aPluginSandboxinterface 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_SIZEtuning yet. Pooling remains an upgrade path alongside stronger per-instruction fuel. - Manifest signed-by is a hex Ed25519 public key, not a
publisherKeyIdlookup. 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 indocs/plugins/cli-reference.md:scaffold,build,sign,lint, andpublishare available for artifact preparation, whilelist,enable,disable, anduninstallare operator escape hatches.buildprints the detected toolchain command instead of executing it;publishcreates a local tarball only and does not upload to a marketplace. - No hot-reload. Operators restart the
worker-pluginpod 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.)