Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,145 @@
|
||||
# Phase 06 — Plugin-System
|
||||
|
||||
> **Docs-sidecar status:** Historical phase plan with shipped/deferred notes.
|
||||
> The production-facing entrypoints are `../../features/plugins.md`,
|
||||
> `../../plugins.md`, `../../operations/plugin-lifecycle.md`, and
|
||||
> `../../plugins/*.md`. This file preserves original phase intent and should
|
||||
> not be read as a claim that Marketplace, full hook fanout, or production
|
||||
> third-party plugin execution is complete.
|
||||
|
||||
**Dauer:** 10-13 Wochen
|
||||
**Tasks:** 3
|
||||
**Ziel:** Vollwertiges Runtime-Plugin-System auf WASM-Basis. Drittanbieter (oder interne Teams) können die Boilerplate ohne Forking um neue Funktionen erweitern — sicher isoliert, kapazitäts- und ressourcenbegrenzt, signiert, und mit klarer Consent-Flow für Tenants. Phase 06 liefert die drei zwingenden Bausteine: (1) eine **WASM-Sandbox + Plugin-SDK**, (2) die **Host-seitigen Hook-Points + Plugin-Lifecycle + Admin-UI** und (3) **Audit-Logging, Developer-Docs, externe Security-Review** vor Production-Freigabe.
|
||||
|
||||
Ein Plugin-Marketplace mit Discovery, Ratings, Billing und Auto-Updates ist explizit **nicht** Teil dieser Phase — er liegt im Backlog (Phase 11, Task `plugin-marketplace.md`) und setzt voraus, dass diese Phase stabil ausgeliefert wurde.
|
||||
|
||||
## Current shipped vs blocked
|
||||
|
||||
Phase 06 has shipped substantial runtime foundation, but it is **not** a completed public Marketplace. Current code includes `@toolctrl/plugin-runtime` / `@toolctrl/plugin-sdk` foundations, manifest validation and signature checks, a WebAssembly sandbox wrapper, capability gating, the `outbox.plugin-hook` fanout path into the isolated `toolctrl.plugin-invoke` queue, tenant plugin lifecycle services, hook publishing for selected host events, tenant settings/API surfaces for installed plugins, plugin migrations, audit retention/capability-use trails, crash tracking and operator CLI commands.
|
||||
|
||||
Still blocked or deferred: public Marketplace discovery, ratings, billing and auto-updates; external security review and production sign-off; Wasmtime/per-instruction fuel migration; hook coverage beyond the shipped foundation set; richer settings UX and encrypted secret host bindings beyond the shipped iframe/CSP/postMessage shell; hot reload; and production rollout evidence. Treat Phase 06 as internally audit-ready foundation plus admin/operator controls, not as a finished third-party Marketplace.
|
||||
|
||||
## Warum diese Phase nach Phase 03 und Phase 05
|
||||
|
||||
- **Outbox-Pattern** (Task #42 aus Phase 03) ist Pflicht-Voraussetzung. Plugin-Hook-Events (`onTenantSignup`, `onEntityCreate`, `onWorkflowExecute`, etc.) werden **niemals** synchron im Request-Path aufgerufen. The shipped foundation emits hook outbox events, fans them out on `worker-general` via `outbox.plugin-hook`, and enqueues isolated invocation jobs on `toolctrl.plugin-invoke`. Sonst würde ein einzelnes hängendes Plugin den Host-Request blockieren oder mit Tenant-Operationen in einer DB-Transaktion ringen.
|
||||
- **Worker-Queues + Worker-Trennung** (Task #45 aus Phase 03) liefern den Pattern für einen dedizierten `worker-plugin`-Service. WASM-Instanzen sind CPU- und memory-bound; execution stays on the plugin worker group through `toolctrl.plugin-invoke`, while subscriber fanout remains cheap on the general worker.
|
||||
- **Feature-Flags** (Phase 05, Task #53) sind eine optionale Rollout-Schicht für Plugin-Funktionen. Das aktuelle Capability-Gating läuft über Manifest-Schema, tenant-gebundene Capability-Grants und den Consent-Dialog; Feature-Flags können ganze Plugin-Oberflächen oder Capability-Klassen zusätzlich abschalten.
|
||||
- **SharedStore** (Task #37 package / #38 migration aus Phase 03) wird für Plugin-Resource-Counters genutzt (Rate-Limits, CPU-Budget pro Plugin pro Tenant pro Minute).
|
||||
- **Sentry + OTel** (Phase 02) müssen Plugin-Spans/Errors mit Tags `plugin.id`, `plugin.version`, `plugin.capability` versehen — die Wiring-Punkte müssen vor Phase 06 stehen, sonst sind Plugin-Crashes blind.
|
||||
|
||||
Ohne diese Foundation würden wir Plugin-Code synchron in Web-/Worker-Prozessen laufen lassen müssen — das produziert messbare Latenz, gefährdet Mandanten-Isolation und macht Resource-Limits unmöglich durchzusetzen.
|
||||
|
||||
## Was diese Phase blockt
|
||||
|
||||
- **Phase 11 / `plugin-marketplace.md` (Backlog):** Der Marketplace ist der Discovery-Layer über diesem System. Ohne Sandbox + Lifecycle + Audit kein veröffentlichbares Plugin und damit kein Marketplace.
|
||||
- **Phase 08 / `cli-scaffolding.md`:** Die CLI bekommt einen `tc plugin scaffold`-Subcommand der die Plugin-Templates aus dieser Phase nutzt (`audit-docs.md` enthält Template-Definition). Phase 08 ist nicht hart geblockt — sie kann unabhängig starten und das Plugin-Template nachreichen sobald Phase 06 fertig ist.
|
||||
- **Phase 07 / Chatbot-Tool-Plugins:** Plugins können Chatbot-Tools registrieren (über `tool.register`-Capability). Phase 07 ist nicht geblockt — Chatbot kann ohne Plugin-Tools laufen, gewinnt aber durch das System.
|
||||
|
||||
Phase 06 blockt **nichts kritisches** für die Produktionsreife der Boilerplate. Sie ist ein Differenzierungs-Feature, kein Foundation-Stack.
|
||||
|
||||
## Task-Reihenfolge
|
||||
|
||||
| Reihenfolge | Task-ID | Datei | Parallelisierbar mit |
|
||||
| ----------- | ------- | -------------------- | ---------------------------------------------------- |
|
||||
| 1 | #21 | `wasm-sandbox.md` | — (Foundation, alle anderen hängen dran) |
|
||||
| 2 | #22 | `hooks-lifecycle.md` | — (braucht #21 für Plugin-Runtime) |
|
||||
| 3 | #23 | `audit-docs.md` | — (braucht #21 + #22 für vollständige Audit-Surface) |
|
||||
|
||||
Die drei Tasks sind **strikt sequenziell**. Anders als bei Phase 03/04 lohnt sich hier keine Parallelisierung, weil:
|
||||
|
||||
- `hooks-lifecycle` braucht ein funktionierendes SDK aus `wasm-sandbox`, um Hook-Handler überhaupt definieren zu können.
|
||||
- `audit-docs` schreibt Developer-Docs und Beispiel-Plugins, die ohne stabile Hook-API + Lifecycle-Flows nicht realistisch sind.
|
||||
- Die externe Security-Review (Acceptance-Criterion in `audit-docs`) prüft den **gesamten** Phase-06-Code-Surface — wir wollen keine Review zur Halbzeit, weil sonst Folge-Findings die Sandbox-Architektur retroaktiv aufreißen.
|
||||
|
||||
## Dependency-Graph (intern)
|
||||
|
||||
```
|
||||
Phase 03 (Outbox #42, Worker-Trennung #45, SharedStore #37/#38)
|
||||
│
|
||||
│
|
||||
Phase 05 (Feature-Flags #53)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ #21 wasm-sandbox │
|
||||
│ (Node 22 WebAssembly │
|
||||
│ runtime, Wasmtime/jco │
|
||||
│ upgrade path, │
|
||||
│ SDK-Package, Manifest- │
|
||||
│ Schema, Ed25519-Sign, │
|
||||
│ Capability-Validator, │
|
||||
│ Resource-Limits, │
|
||||
│ Worker-Isolation) │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ #22 hooks-lifecycle │
|
||||
│ (15-20 Host-Hook-Points, │
|
||||
│ Install/Enable/Disable/ │
|
||||
│ Update/Uninstall, │
|
||||
│ Plugin-DB-Migrations, │
|
||||
│ Tenant-Admin-UI, │
|
||||
│ Per-User-Enablement, │
|
||||
│ Crash-Auto-Disable) │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ #23 audit-docs │
|
||||
│ (Audit-Log pro Call, │
|
||||
│ SDK-API-Referenz, │
|
||||
│ Capability-Matrix, │
|
||||
│ 3 Beispiel-Plugins, │
|
||||
│ Plugin-CLI, │
|
||||
│ externe Security-Review, │
|
||||
│ Compatibility-Matrix) │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
Production-Rollout-Freigabe
|
||||
(gated by external review sign-off)
|
||||
```
|
||||
|
||||
## Phase Done-Definition (historical completion gate)
|
||||
|
||||
Current shipped/deferred state is summarized above; this checklist remains the original production-readiness gate.
|
||||
|
||||
Diese Phase ist `completed` wenn:
|
||||
|
||||
- [ ] Alle 3 Tasks `completed` in `PROGRESS.md`
|
||||
- [ ] `pnpm lint`, `pnpm typecheck`, `pnpm test` global grün
|
||||
- [ ] `packages/plugin-runtime/`, `packages/plugin-sdk/`, `packages/plugin-cli/` exportieren stabile öffentliche APIs (in jeweils einer `index.ts` + Type-Exporten + SemVer-Tagging)
|
||||
- [ ] `worker-plugin`-Service ist im Docker-Compose und in Coolify-Staging deployed; current shipped handler consumes `toolctrl.plugin-invoke`; historical install/uninstall/migrate handler names below are plan context, not live queue names.
|
||||
- [ ] Manifest-Schema (`plugin.json`) versioniert (`schemaVersion: 1`), Ed25519-Public-Key-Registry seedable per CLI
|
||||
- [ ] Tenant-Admin-UI unter `/app/$tenant/settings/plugins` lauffähig: Liste, Install-Dialog mit Capability-Consent, Settings-Page-Slot, Per-User-Enable
|
||||
- [x] `.env.example` enthält die aktuell produktiv verdrahteten Runtime-Keys: `PLUGIN_TRUSTED_PUBLIC_KEYS`, `PLUGIN_MAX_FUEL`, `PLUGIN_MAX_MEMORY_MB`, `PLUGIN_MAX_WALL_CLOCK_MS`, `PLUGIN_RATE_LIMIT_PER_MINUTE`, `PLUGIN_RATE_LIMIT_WINDOW_SECONDS`, `PLUGIN_AUDIT_RETENTION_DAYS`
|
||||
- [ ] Doku-Files vorhanden: `docs/features/plugins.md`, `docs/plugins/sdk-reference.md`, `docs/plugins/capability-matrix.md`, `docs/plugins/cli-reference.md`, `docs/plugins/manifest-reference.md`, `docs/plugins/versioning.md`, `docs/operations/plugin-lifecycle.md`
|
||||
- [ ] 3 Referenz-Plugins (`examples/plugins/slack-notification/`, `examples/plugins/datev-export/`, `examples/plugins/custom-auth-provider/`) bauen mit der Plugin-CLI, signieren mit Test-Key, installieren auf Staging und laufen je 1× erfolgreich
|
||||
- [ ] Externe Security-Review-Report von einer unabhängigen Security-Firma vorliegend, alle Critical/High-Findings adressiert, Report unter `docs/security-reviews/plugin-runtime-<date>.md` archiviert
|
||||
- [ ] Staging-Deployment auf `agent/phase-06-final` deployed: 1× Sandbox-Escape-Test (siehe Test Requirements der Tasks), 1× Resource-Limit-Test, 1× Capability-Enforcement-Test alle grün
|
||||
- [ ] Integration-Test-Flow: Plugin-Install (CLI sign → Tenant-Admin install → Capability-Consent → Enable) → Hook-Trigger (`onTenantSignup` durch Sign-up-Flow) → Plugin-Call → Audit-Log-Eintrag mit `capability=http.outbound` sichtbar im Tenant-Admin
|
||||
- [ ] Tenant-Isolation-Test: Plugin in Tenant A kann auf keinerlei Daten von Tenant B zugreifen (DB-Capability, Storage-Capability, Audit-Logs separat)
|
||||
- [ ] Crash-Test: Plugin das in 5 aufeinanderfolgenden Calls panic'ed wird automatisch disabled, Tenant-Admin-Notification gefeuert
|
||||
- [ ] `gsd-code-reviewer`-Subagent hat alle Phase-06-Commits reviewed; Findings adressiert
|
||||
- [ ] Phase-Summary in dieser README.md ergänzt (siehe unten)
|
||||
|
||||
## Phase Summary
|
||||
|
||||
_(Wird nach Abschluss der Phase vom ausführenden Agent ergänzt.)_
|
||||
|
||||
### Lessons Learned
|
||||
|
||||
_(Wird nach Abschluss ergänzt — insbesondere: Wasmtime- vs Wasmer-Findings, SDK-Ergonomie-Erfahrungen aus dem Bauen der 3 Referenz-Plugins, Performance-Overhead pro Hook-Call, externe Security-Review-Findings + Architektur-Anpassungen, Capability-Granularität-Erfahrungen, Plugin-DB-Migration-Pitfalls.)_
|
||||
|
||||
## Verweise
|
||||
|
||||
- Detail-MD-Template: `../DETAIL-MD-TEMPLATE.md`
|
||||
- Agent-Workflow: `../AGENT-INSTRUCTIONS.md`
|
||||
- Execution-Order: `../00-execution-order.md`
|
||||
- Live-Status: `../PROGRESS.md`
|
||||
- Vorgänger-Phase: `../05-business-features/README.md`
|
||||
- Foundation-Phase: `../03-scaling-infrastructure/README.md`
|
||||
- Nachfolger-Phase (nicht hart geblockt): `../08-mobile-cli-storybook-sdk/README.md`
|
||||
- Backlog-Folge-Item: `../11-backlog/plugin-marketplace.md`
|
||||
- Current plugin docs: `../../plugins.md`, `../../features/plugins.md`, `../../operations/plugin-lifecycle.md`
|
||||
@@ -0,0 +1,318 @@
|
||||
# Phase 06 — Verifikation (Plugin-System)
|
||||
|
||||
**Verifier:** general-purpose-Subagent
|
||||
**Datum:** 2026-05-16
|
||||
**Scope:** `README.md`, `wasm-sandbox.md`, `hooks-lifecycle.md`, `audit-docs.md`
|
||||
**Modus:** READ-ONLY
|
||||
|
||||
> **2026-05-27 code status correction:** This file primarily verifies the
|
||||
> original plan quality, not the whole current shipped implementation. A focused
|
||||
> code re-check against `PluginSettingsFrame`, `PluginSettingsService`, the
|
||||
> tenant plugin settings route, and related tests shows that the plugin settings
|
||||
> iframe/CSP/postMessage path is now implemented for non-secret per-user plugin
|
||||
> settings. The frame runs as a `srcDoc` iframe with `sandbox="allow-scripts"`
|
||||
> only, embeds a CSP with `default-src 'none'`, `connect-src 'none'`,
|
||||
> `form-action 'none'`, inline script/style allowances, data-only image/font
|
||||
> sources, and no `allow-same-origin` or `allow-forms`. Message handling uses a
|
||||
> local versioned postMessage parser (`plugin.settings.*`, protocol version 1)
|
||||
> with request/key/value bounds and secret-like key rejection; it is not Zod.
|
||||
> Persistence is routed server-side through the tenant settings route with
|
||||
> `app.plugins.use` permission checks, CSRF validation, JSON/body-size checks,
|
||||
> rate limiting, and per-user `PluginUserConfig` writes. Server bundle loading
|
||||
> resolves realpaths under the plugin bundle root and enforces a settings HTML
|
||||
> size limit before returning trusted HTML.
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Status: plan-quality PASS; current runtime remains staging/foundation-only until external security signoff, production rollout proof, and the deferred runtime hardening items are complete.**
|
||||
|
||||
Phase 06 ist die ambitionierteste und sicherheitskritischste Phase im gesamten Plan, und die drei Detail-MDs reflektieren das angemessen. Template-Konformität ist 100% (alle 15 Sections, korrekte Reihenfolge in allen drei Files). Dependencies sind sauber konsistent zwischen den Files und nach außen (Phase 03 Outbox/Worker-Queues/SharedStore, Phase 05 Feature-Flags). Implementation-Steps sind durchgehend atomar und commitbar. Sandbox-Escape-Tests sind systematisch enumeriert (7 Vektoren in `wasm-sandbox.md`), Capability-Enforcement-Tests vollständig, Resource-Limit-Tests (CPU/Memory/Wall-Clock) als kritisch markiert. Externe Security-Review ist als hard-gate vor Production-Rollout in `audit-docs.md` etabliert. Konsistenz mit User-Wünschen ist gegeben: vollwertiges Runtime-Plugin-System, WASM-basiert (Wasmtime mit Research-Spike), kein Marketplace-Code in Phase 06, Self-Hosted-Plugin-Install via ZIP-Upload.
|
||||
|
||||
Kleinere Anmerkungen (keine Blocker): leichte Inkonsistenzen bei Env-Variable-Naming zwischen README Done-Definition und wasm-sandbox.md (CPU-Limit-Variable), und einige nicht-funktionale Aspekte (Plugin-Update-Notification an Tenants, Plugin-Sandbox-Telemetry-Tagging in Sentry) könnten stärker hervorgehoben sein. Beides ist mit kleinen Edits adressierbar oder akzeptabel als Implementation-Detail.
|
||||
|
||||
---
|
||||
|
||||
## 1. Template-Konformität
|
||||
|
||||
Alle drei Detail-MDs folgen `DETAIL-MD-TEMPLATE.md` strikt:
|
||||
|
||||
| File | Frontmatter | Problem | Depends on | Blocks | Approach | Implementation Steps | Test Requirements | Acceptance Criteria | Rollback | Security | Subagent Rec. | Staging | Deviations | Progress Log | Lessons Learned |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| `wasm-sandbox.md` | OK | OK | OK | OK | OK | OK (17 Steps) | OK | OK | OK | OK (sehr ausführlich) | OK | OK | OK | OK | OK |
|
||||
| `hooks-lifecycle.md` | OK | OK | OK | OK | OK | OK (17 Steps) | OK | OK | OK | OK | OK | OK | OK | OK | OK |
|
||||
| `audit-docs.md` | OK | OK | OK | OK | OK | OK (18 Steps) | OK | OK | OK | OK | OK | OK | OK | OK | OK |
|
||||
|
||||
Reihenfolge der Sections ist in allen Files identisch und template-konform. Frontmatter-Felder (`Status`, `Task-ID`, `Phase`, `Estimated Effort`, `Owner`) sind durchgehend gesetzt; Owner ist überall `unassigned`, was korrekt für pending Tasks ist.
|
||||
|
||||
**Befund: PASS** — keine Template-Abweichungen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Dependencies-Konsistenz
|
||||
|
||||
### Intern (Phase-06-Sequenz)
|
||||
|
||||
- `wasm-sandbox.md` deklariert keine Depends-on innerhalb Phase 06 (korrekt, da Foundation). Blocks: `hooks-lifecycle.md`, `audit-docs.md`, `11-backlog/marketplace.md`. **Konsistent.**
|
||||
- `hooks-lifecycle.md` Depends-on: `wasm-sandbox.md`. Blocks: `audit-docs.md`, `11-backlog/marketplace.md`. **Konsistent.**
|
||||
- `audit-docs.md` Depends-on: `wasm-sandbox.md` UND `hooks-lifecycle.md`. Blocks: Production-Rollout der gesamten Phase 06, Marketplace. **Konsistent.**
|
||||
|
||||
Die strikt sequenzielle Reihenfolge (kein Parallelisierungs-Versuch) ist in README.md explizit begründet (kein halber Security-Review-Pass). Plausibel.
|
||||
|
||||
### Extern (Phase-Cross-Refs)
|
||||
|
||||
Cross-Phase-Deps werden in allen drei Files konsistent referenziert:
|
||||
|
||||
| Dep | wasm-sandbox.md | hooks-lifecycle.md | audit-docs.md | 00-execution-order |
|
||||
|---|---|---|---|---|
|
||||
| `03/shared-store-package.md` | OK | (transitiv) | (transitiv) | OK |
|
||||
| `03/worker-queues.md` | OK | (transitiv) | (transitiv) | OK |
|
||||
| `03/outbox-pattern.md` | OK | OK (explizit) | (transitiv) | OK |
|
||||
| `05/feature-flags.md` | OK | OK (explizit) | (transitiv) | OK |
|
||||
| `02/sentry-wiring-api-worker.md` | OK | (transitiv) | (transitiv) | (impliziert) |
|
||||
|
||||
**Konsistent mit `00-execution-order.md`**: dort steht „Phase 06 Depends on: Phase 03, 05". Reihenfolge `wasm-sandbox → hooks-lifecycle → audit-docs` ist in 00-execution-order.md ebenfalls so dokumentiert (Tabellen-Eintrag „Reihenfolge 7").
|
||||
|
||||
### Blocks → Marketplace (Backlog #24)
|
||||
|
||||
- README.md Zeile 21: „Phase 11 / `marketplace.md` (Backlog): … Ohne Sandbox + Lifecycle + Audit kein veröffentlichbares Plugin und damit kein Marketplace."
|
||||
- Alle drei Detail-MDs deklarieren `11-backlog/marketplace.md` als geblockt.
|
||||
- **Konsistent.**
|
||||
|
||||
**Befund: PASS.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation-Steps-Qualität
|
||||
|
||||
Die User-spezifizierten 11 Detail-Aspekte werden alle adressiert:
|
||||
|
||||
| Aspekt | Adressiert | File:Section | Qualitäts-Bewertung |
|
||||
|---|---|---|---|
|
||||
| WASM-Engine-Wahl (Wasmtime vs Wasmer) — Research-Spike empfohlen? | Ja | `wasm-sandbox.md` Step 1 + Approach-Tabelle Zeile 48 | **Stark.** Explizit als Research-Spike (Step 1) mit zwei parallelen Explore-Subagents (Approach-Vergleich + Security-CVE-Review). Default-Choice Wasmtime mit Begründung (Bytecode Alliance, Fastly/CF/MS-Adoption, dokumentierte Resource-Limit-API). Research-Output-File `RESEARCH-runtime-choice.md` ist temporär — sauber. |
|
||||
| Plugin-SDK mit Host-Bindings (HTTP/DB/Logging/Audit/Storage) — vollständig konzipiert? | Ja | `wasm-sandbox.md` Approach §SDK-Package + Step 12 | **Stark.** 8 Host-Bindings vollständig spezifiziert (http, db.read, db.write, storage.read/write, log, audit, secret, tenant) mit jeweiliger Capability + Tenant-Scoping-Strategie. AssemblyScript-First (TypeScript-Devs), Rust-Crate optional/Skip-bar — pragmatisch. |
|
||||
| Ed25519-Manifest-Signing + Publisher-Key-Registry — kryptografisch korrekt? | Ja | `wasm-sandbox.md` Approach §Ed25519 + Step 5+6 | **Korrekt.** Ed25519 mit `@noble/ed25519` (audited, no native deps — gute Wahl). Canonical-JSON-Subset für Signing (sorted keys, no whitespace) explizit gefordert — verhindert Signing-Round-Trip-Issues. Tamper-Tests in Test-Requirements (Binary + Manifest). Publisher-Key-Registry initial JSON-File mit `revokedAt`-Feld — minimal aber funktional. **Anmerkung:** Key-Rotation-Process wird in Security-Notes erwähnt, aber kein eigener Implementation-Step. Könnte als Operator-CLI-Subcommand (`tc plugin keys rotate`) ergänzt werden (Step 13 in audit-docs.md hat add/revoke/list, aber kein rotate). Nicht kritisch — Operator kann revoke+add machen. |
|
||||
| Capability-System mit Tenant-Consent — UX-Flow durchdacht? | Ja | `wasm-sandbox.md` Approach §Capability + `hooks-lifecycle.md` Lifecycle-State-Machine + UI-Tabelle | **Stark.** Klare Trennung: Capability-Declare im Manifest (Step 4 wasm-sandbox) → Validator → Tenant-Consent-Dialog (hooks-lifecycle Step 6-8) → Runtime-Enforcement im Main-Thread (nicht Worker-Thread, korrekt). Scope-System mit `allowedHosts`, `keyPattern` etc. ist granular genug. Capability-Erweiterung zur Runtime explizit ausgeschlossen — gut. |
|
||||
| Resource-Limits (CPU/Memory/Wall-Clock) — wie enforced? | Ja | `wasm-sandbox.md` Approach-Tabelle Zeilen 156-165 + Step 7 | **Stark.** Defense-in-Depth: Fuel (CPU) + Memory.max + Wall-Clock-Timer + Body-Size + DB-Row-Limit + Rate-Limit-pro-Tenant. Mechanismus pro Limit konkret benannt (Wasmtime-API). Tests fordern explizit Fuel-Trip, Memory-Trip, Wall-Clock-Trip. **Anmerkung:** Defaults erscheinen vernünftig (100 Mio Fuel ~= 100ms, 64 MiB Mem, 5s Wall-Clock), sollten aber im Tuning-Doku iterierbar dokumentiert sein — was `docs/operations/plugin-runtime.md` (Step 16) explizit fordert. |
|
||||
| Worker-Thread-Isolation der WASM-Instances — realistisch? | Teilweise umgesetzt | `wasm-sandbox.md` Approach §Worker-Thread-Isolation + aktueller Code-Abgleich 2026-05-27 | **Aktueller Stand:** Plugin-Aufrufe laufen in einem frischen, killable Worker Thread pro Invocation und werden danach terminiert. Ein wiederverwendbarer Pool mit `PLUGIN_WORKER_POOL_SIZE` existiert noch nicht; Pooling bleibt Upgrade-Pfad. |
|
||||
| 15-20 Host-Hook-Points im Code identifiziert? | Ja | `hooks-lifecycle.md` Problem-Section Zeilen 22-32 + Approach-Tabelle Zeilen 66-87 | **Stark.** 18 Hook-Points enumeriert (innerhalb des 15-20-Zielbereichs), für jeden Input-Felder definiert, Output-Verwendung dokumentiert. Konkrete file-Pfade im Problem-Block genannt (`apps/web/app/routes/onboarding.tsx`, `users.db.server.ts`, `WorkflowEngineService.ts` etc.) — das sind real existierende Pfade (verifiziert anhand `git status` der laufenden Boilerplate). Step 13 explizit „pro Hook ein Commit" = 18 atomare Commits — sauber. |
|
||||
| Plugin-Lifecycle (Install/Enable/Disable/Update/Uninstall) — vollständig? | Ja | `hooks-lifecycle.md` Approach §State-Machine + Step 6 | **Vollständig.** State-Machine (pending → awaiting-consent → installed-disabled → enabled ↔ disabled → uninstalling → uninstalled, plus auto-disabled-Pfad) mit ASCII-Diagramm. Service-Methoden install/grantConsent/enable/disable/update/uninstall/autoDisable. Update läuft side-by-side mit superseded-Markierung der alten Version — wichtig für Rollback. |
|
||||
| Plugin-Migrations-Mechanismus für eigene DB-Tabellen — sauber? | Ja | `hooks-lifecycle.md` Approach §DB-Schema-Migrations + Step 5 | **Sauber.** Tabellen-Prefix-Pflicht (`plugin_<sanitized-id>_*`), `tenantId NOT NULL` Pflicht-Spalte (validated via Schema-Inspection), Down-Migrations Pflicht, Apply via `prismaWrite` (Host-Operation, nicht Sandbox), SQL-Allowlist (CREATE TABLE/INDEX, ALTER TABLE ADD COLUMN, DROP TABLE/INDEX/COLUMN — keine DROP DATABASE, GRANT, REVOKE, COPY) in Security-Notes Zeile 405. **Stark gegen SQL-Injection.** Alternative (separate Postgres-Schemas) wurde explizit gegen abgewogen mit Begründung — gute Architektur-Hygiene. |
|
||||
| iframe-Sandboxed UI für Plugin-Settings — secure? | Ja, aktueller Code implementiert den Kernpfad | `hooks-lifecycle.md` Step 9 + aktueller Code-Abgleich 2026-05-27 | **Implementiert für non-secret per-user settings.** `PluginSettingsFrame` rendert ein `srcDoc`-iframe mit `sandbox="allow-scripts"` only (kein `allow-same-origin`, kein `allow-forms`, kein `allow-top-navigation`) und eingebetteter CSP: `default-src 'none'`, `connect-src 'none'`, `form-action 'none'`, `script-src 'unsafe-inline'`, `style-src 'unsafe-inline'`, data-only image/font, `base-uri 'none'`. Die Message-Validation erfolgt über den lokalen versionierten Parser `parsePluginSettingsFrameMessage` für `plugin.settings.ready/get/set` (Protocol v1), inklusive Bounds für requestId/key/value, JSON-like Value-Shape und Rejects für reserved/secret-like keys. Persistenz läuft nicht im iframe, sondern serverseitig über `app.$tenant/settings/plugins.$id`: `app.plugins.use` Permission, CSRF, JSON/body-size-Checks, rate limiting und `setPluginUserSetting` auf per-user `PluginUserConfig`. `PluginSettingsService` prüft `ui.settings.entry` als safe relative HTML path, löst Bundle-Root und Entry per `realpath` auf, verhindert Bundle-Escape und erzwingt ein HTML-Size-Limit. Tests decken Frame-CSP/Sandbox/PostMessage, Parser-Rejections, Route-Permission/CSRF/Rate-Limit/Persistenz und den E2E-iframe-Kontrakt ab. |
|
||||
| Crash-Handling + Auto-Disable — implementiert? | Teilweise umgesetzt | `hooks-lifecycle.md` Approach §Crash-Handling + aktueller Code-Abgleich 2026-05-27 | **Aktueller Stand:** Tenant-scope Crash-/Failure-Tracking und Auto-disable sind vorhanden. Globaler Multi-Tenant-Crash-Counter plus Operator-Alert bleiben Follow-up vor Production-Signoff. |
|
||||
|
||||
**Befund: PASS — Implementation-Steps sind durchweg hochqualitativ. Komplexeste Phase, aber Detail-Tiefe entspricht der Komplexität.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Test-Requirements
|
||||
|
||||
### Sandbox-Escape-Tests (Anforderung: mindestens 7 Vektoren)
|
||||
|
||||
`wasm-sandbox.md` Zeilen 317-324 listet **7 Escape-Tests**:
|
||||
|
||||
1. WASM mit `wasi:filesystem`-Import → Instantiation-Fail
|
||||
2. WASM mit raw memory.grow über Limit → Trap
|
||||
3. WASM versucht Host-Function-Pointer zu konstruieren → Component-Model verhindert
|
||||
4. Plugin-Binary mit modifizierten Imports nach Signing → Verify-Fail
|
||||
5. Plugin sendet `file://` oder `localhost`-URL → Reject (Allowlist)
|
||||
6. Plugin versucht Wildcard-Key (`*`) via `secret.get` → Reject
|
||||
7. Plugin versucht Memory-Region eines anderen gleichzeitig laufenden Plugins zu lesen → unmöglich (separate Wasmtime-Stores)
|
||||
|
||||
**Anforderung erfüllt (exakt 7).** Zusätzlich sind die Tests als „kritisch" markiert und explizit als Blocker für Task-Completion gesetzt (Zeile 332: „Bei auch nur einem fehlgeschlagenen Escape-Test: Task blockiert, BLOCKERS.md füllen.").
|
||||
|
||||
### Resource-Limit-Tests (CPU/Memory/Disk-Bombs)
|
||||
|
||||
`wasm-sandbox.md` Zeilen 307-310:
|
||||
- Endlosschleife → Fuel-Trip in <200ms
|
||||
- Memory-Bomb (256 MiB bei 64 MiB Limit) → Trap + Pool-Replace
|
||||
- Wall-Clock-Bomb (synchroner Sleep > 5s) → Worker-Terminate + Retry (max 3×) → Fail
|
||||
|
||||
**Anmerkung:** „Disk-Bombs" sind nicht explizit getestet — aber Plugin-Sandbox hat per Default keinen Filesystem-Zugriff (kein `wasi:filesystem`-Import gewährt, Test #1). Damit ist Disk-Bomb-Vektor strukturell ausgeschlossen. ZIP-Bomb (Plugin-Upload-Pfad) ist in `hooks-lifecycle.md` Security-Notes Zeile 398 separat gemitigated (max 50 MiB komprimiert, max 200 MiB unkomprimiert). **Implizit erfüllt.**
|
||||
|
||||
### Capability-Enforcement-Tests
|
||||
|
||||
`wasm-sandbox.md` Zeilen 311-316: 5 Tests (ohne Capability-Grant, Host-Allowlist-Bypass, db.write ohne Grant, db.write ohne Prefix, Tenant-Bypass via SQL-Injection). Plus `hooks-lifecycle.md` Test-Requirements mit Per-User-Enablement-Test, Auto-Disable-Test, Migration-Tests (positiv + negativ). **Vollständig.**
|
||||
|
||||
### Coverage-Targets
|
||||
|
||||
- `wasm-sandbox.md`: ≥90% für plugin-runtime (security-kritisch), ≥85% für plugin-manifest, ≥80% für plugin-sdk
|
||||
- `hooks-lifecycle.md`: ≥85% für plugin-hooks und plugin-modules, ≥90% für migrations.ts und crash-tracker.ts
|
||||
- `audit-docs.md`: ≥85% für plugin-cli, ≥90% für audit-recorder.ts und sanitize.ts
|
||||
|
||||
Differentiated Coverage (höhere Targets für security-kritischen Code) — gute Praxis.
|
||||
|
||||
**Befund: PASS.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Security-Notes
|
||||
|
||||
`wasm-sandbox.md` Security-Notes ist ausführlich (~25 Zeilen, 9 thematische Blöcke):
|
||||
|
||||
1. **Sandbox-Boundary** — explizit erwähnt was Defense-in-Depth-Lagen sind (typed Component-Model-API, Fuel + Epoch-Interrupt + Wall-Clock = 3 Lagen)
|
||||
2. **Capability-System** — Principle of Least Privilege explizit benannt, Default-Deny
|
||||
3. **Tenant-Isolation** — Tenant-ID vom Host injiziert (nie vom Plugin), DB-SQL-Rewriting für `WHERE tenantId`, Storage-Prefix, Audit-Tenant-Scoping
|
||||
4. **Signing** — Ed25519-Wahl begründet (vs RSA/ECDSA), `@noble/ed25519` audited
|
||||
5. **Resource-Limits** — Worst-Case-Berechnung (`POOL_SIZE × MEMORY_MB`)
|
||||
6. **Worker-Process-Isolation** — seccomp + AppArmor empfohlen in Operations-Doku
|
||||
7. **Plugin-Binary-Storage** — read-only für worker-plugin-User außer Install
|
||||
8. **Sensitive Data in Plugin-Output** — Sanitization-Filter (wird in audit-docs.md detailliert)
|
||||
9. **Sandbox-Escape-Detection** — Sentry-Alert bei Trap, On-Call-Workflow
|
||||
10. **No Eval, No Dynamic Code** — WASM ist statisch, Plugin kann kein zweites Modul laden
|
||||
11. **Security-Review als Blocker** — verweist auf audit-docs.md (#23)
|
||||
|
||||
### STRIDE-Mapping (von Verifier nachgeprüft)
|
||||
|
||||
| STRIDE-Threat | Mitigation in Phase 06 | OK? |
|
||||
|---|---|---|
|
||||
| **S**poofing (Plugin-Identität) | Ed25519-Signing + Publisher-Key-Registry + revoked-Key-Check | OK |
|
||||
| **T**ampering (Plugin-Binary/Manifest) | Signature-Verify über Binary + canonical Manifest-Subset; Tamper-Tests | OK |
|
||||
| **R**epudiation (Tenant bestreitet Capability-Consent) | Consent-Action im existierenden Audit-Log mit Actor+Timestamp (hooks-lifecycle Security-Note Zeile 404) | OK |
|
||||
| **I**nformation Disclosure (Cross-Tenant-Leak) | Tenant-Scoping in DB-Bindings, Storage-Prefix, separate Wasmtime-Stores, Output-Sanitization | OK |
|
||||
| **D**enial of Service | Resource-Limits, Rate-Limits per Tenant, Auto-Disable, Global-Auto-Disable, Pool-Sizing | OK |
|
||||
| **E**levation of Privilege | Capability-System Default-Deny, kein Runtime-Capability-Request, Main-Thread macht Checks (nicht untrusted Worker) | OK |
|
||||
|
||||
**STRIDE-Coverage vollständig.**
|
||||
|
||||
### Sandbox-Escape-Vektoren systematisch enumeriert?
|
||||
|
||||
Ja — 7 Escape-Tests (s.o.) plus explizite Diskussion in Security-Notes (Zeile 373) von 3 Escape-Pfaden (Host-Import-Bugs, Resource-Limit-Bypass, Component-Model-Bugs). Externe Security-Review (Task #23) ist als Pflicht-Gate vor Production-Rollout etabliert — anerkennt explizit dass interne Tests blinde Flecken haben (Zeile 379 audit-docs.md).
|
||||
|
||||
**Befund: PASS — Security-Notes sind ausführlich, defense-in-depth-orientiert, STRIDE-konform.**
|
||||
|
||||
---
|
||||
|
||||
## 6. Staging-Deployment-Steps
|
||||
|
||||
### `wasm-sandbox.md` Staging (Zeilen 408-481)
|
||||
|
||||
13 nummerierte Steps: Branch pushen → Coolify-Deploy → docker-compose-Snippet für `worker-plugin` (mit Resource-Limits cpus/memory) → Test-Publisher-Keypair generieren → Public-Key in Staging-Registry → Hello-Plugin signieren + droppen → Smoke-Test via Test-Endpoint `/api/internal/plugin-test/invoke` (gated) → Resource-Limit-Smoke (endless-loop-plugin) → Capability-Enforcement-Smoke (unauthorized-http-plugin) → Health-Check → Sentry-Dashboard-Check → Logs-Check → **Production-Gate** (Test-Endpoint muss aus prod-Build entfernt werden).
|
||||
|
||||
**Stark.** docker-compose-Snippet ist konkret (mit env-Vars, Volumes, deploy-Resource-Limits, replicas).
|
||||
|
||||
### `hooks-lifecycle.md` Staging (Zeilen 434-476)
|
||||
|
||||
18 Steps: Branch + Coolify + Migration-Apply → Worker-Handler-Registration-Check → Tenant-Admin-Login → Plugin-Install via UI → Capability-Consent-Dialog → Enable → Hook-Trigger via Test-Endpoint → Audit-Verify → Plugin-Settings-Test mit postMessage → Per-User-Toggle-Test → **Crash-Test** mit crash-plugin → **Uninstall-Test** mit DB-Verify (`\dt plugin_*`) → Update-Path-Test mit Version-Side-by-Side → Health-Check → Production-Gate.
|
||||
|
||||
**Stark.** Plugin-Install-Flow ist End-to-End auf Staging durchgespielt.
|
||||
|
||||
### `audit-docs.md` Staging (Zeilen 411-451)
|
||||
|
||||
13 Steps inkl. CLI-Lokal-Install, Roundtrip-Smoke (scaffold → build → sign → lint → pack), Public-Key in Registry, Plugin-Install via UI, Audit-Trail-UI-Check, **Output-Sanitization-Test** (Credit-Card-Pattern → `[REDACTED:credit-card]`), Retention-Test (manuell `loggedAt` auf 91 Tage zurück, Cron-Job triggern), 3 Beispiel-Plugins durchlaufen (Slack/DATEV/Custom-Auth), **Externe Security-Review-Phase** (Test-Tenant-Setup-Skript, Reviewer-Zugang, Task auf `blocked: waiting-for-external-review` während Review), Production-Rollout-Gate mit Sign-off + 24h-Monitoring + Kill-Switch.
|
||||
|
||||
**Sehr stark.** Plugin-Test-Setup auf Staging ist klar; Beispiel-Plugin-Install-Flow ist End-to-End nachvollziehbar.
|
||||
|
||||
**Befund: PASS.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Konsistenz mit User-Wünschen
|
||||
|
||||
| User-Wunsch | Reflektion im Plan | Bewertung |
|
||||
|---|---|---|
|
||||
| „C: Vollwertiges Runtime-Plugin-System" | README.md Zeile 5: „Vollwertiges Runtime-Plugin-System auf WASM-Basis" | OK |
|
||||
| „WASM-basiert (sauberer langfristig)" | Wasmtime durchgängig (Approach, Steps, docker-compose, env-vars). Wasmer nur als Research-Spike-Alternative in Step 1. | OK |
|
||||
| „Marketplace ins Backlog" | README.md Zeile 21 explizit: „Marketplace … explizit **nicht** Teil dieser Phase … liegt im Backlog (Phase 11)". Alle drei Detail-MDs deklarieren marketplace.md als geblockt durch Phase 06. Keine Marketplace-Code-Steps in Phase-06-Implementation. | OK |
|
||||
| „Self-Hosted-Plugin-Install zuerst" | `hooks-lifecycle.md` Approach §Tenant-Admin-UI Zeile 160: Install-Dialog mit File-Upload `.zip`. Kein Marketplace-Pull, kein Discovery-Layer. CLI hat `tc plugin publish --target <staging-url\|production-url>` als optional-Phase-11-Erweiterung markiert. | OK |
|
||||
|
||||
**Befund: PASS — User-Intent vollständig reflektiert.**
|
||||
|
||||
---
|
||||
|
||||
## 8. Acceptance-Criteria
|
||||
|
||||
### Externe Security-Review als hard-gate vor Production-Rollout?
|
||||
|
||||
`audit-docs.md` Acceptance Criteria:
|
||||
- Zeile 353: „**Externer Security-Review durchgeführt und Report archiviert** (PFLICHT)"
|
||||
- Zeile 354: „**Alle Critical- und High-Findings adressiert + von Reviewer-Firma re-tested** (PFLICHT)"
|
||||
- Zeile 355: „Medium-Findings adressiert oder als Backlog-Tasks dokumentiert"
|
||||
- Zeile 356: „Production-Rollout-Sign-off-Document … mit Operator-Signature"
|
||||
|
||||
Plus Step 16 explizit: „Erst nach diesem File-Commit ist Production-Rollout erlaubt."
|
||||
|
||||
Plus README.md Done-Definition Zeile 102: „Externe Security-Review-Report von einer unabhängigen Security-Firma vorliegend, alle Critical/High-Findings adressiert".
|
||||
|
||||
**Befund: PASS — Hard-Gate ist auf mehreren Ebenen verankert (Detail-MD + README + Approach).**
|
||||
|
||||
---
|
||||
|
||||
## 9. Fehlende Aspekte (User-spezifisch)
|
||||
|
||||
| User-Frage | Adressiert? | Detail |
|
||||
|---|---|---|
|
||||
| Plugin-Compatibility-Matrix gegen Host-SemVer? | Ja | `audit-docs.md` Step 11+12 (Matrix initial befüllen, version-check.ts mit `isPluginCompatibleWithHost`, Boot-Check + Install-Reject bei Inkompatibilität, `incompatible_host_version`-Status). |
|
||||
| Plugin-Update-Pipeline mit Notification an Tenants? | **Teilweise.** Plugin-Update-Flow (`PluginLifecycleService.update`) ist in `hooks-lifecycle.md` Step 6 enthalten (side-by-side, alte Version `superseded`). **Aber:** explizite Tenant-Notification bei Plugin-Update („dieser Plugin hat eine neue Version v1.2.0 — möchten Sie aktualisieren?") ist nicht als eigener Step. Update wird über die UI initiiert (`/app/$tenant/plugins/$pluginId` Detail-View) — implizit, aber keine Push-Notification-Pipeline. Für MVP akzeptabel (Plugin-Author-zu-Tenant-Channel ohne Marketplace ist eh out-of-scope). **Empfehlung:** als Folge-Backlog-Item notieren, nicht jetzt blockieren. |
|
||||
| Plugin-Sandbox-Telemetry zu Sentry? | Ja | `wasm-sandbox.md` Step 15: `packages/plugin-runtime/src/observability.ts` mit OTel-Span + Sentry-Tags `plugin.id`, `plugin.version`, `plugin.hook`, `plugin.tenant`, `plugin.capability`. Integration in `invoker.ts` und allen `host-bindings/*.ts`. Plus Security-Notes Zeile 389: „Sandbox-Escape-Detection: Bei jedem Trap oder unexpected Worker-Crash → automatische Alert an Sentry mit `severity=critical`". |
|
||||
|
||||
**Befund: 2.5 von 3 vollständig adressiert. Update-Notification ist akzeptabel-thin, sollte als Lessons-Learned-Item dokumentiert werden falls Phase-06-Execution beginnt.**
|
||||
|
||||
---
|
||||
|
||||
## 10. Kleinere Anmerkungen / Inkonsistenzen
|
||||
|
||||
### 10.1 Env-Variable-Naming: `PLUGIN_MAX_CPU_MS` vs `PLUGIN_MAX_FUEL`
|
||||
|
||||
README.md Zeile 99 listet `PLUGIN_MAX_CPU_MS` in Done-Definition.
|
||||
`wasm-sandbox.md` Zeile 158 nutzt `PLUGIN_MAX_FUEL` (Wasmtime-spezifisch) und Zeile 355 listet ebenfalls `PLUGIN_MAX_FUEL`.
|
||||
|
||||
**Inkonsistenz.** Fuel ist Wasmtime-spezifische Einheit (~ ein Fuel-Unit pro Bytecode-Instruktion), CPU-Ms ist Wall-Clock-ähnlich. Beide sind unterschiedliche Konzepte. README sollte `PLUGIN_MAX_FUEL` listen (oder `PLUGIN_MAX_CPU_MS` als zusätzliche Wall-Clock-CPU-Begrenzung, was aber `PLUGIN_MAX_WALL_CLOCK_MS` schon abdeckt).
|
||||
|
||||
**Empfehlung:** README.md Zeile 99 von `PLUGIN_MAX_CPU_MS` auf `PLUGIN_MAX_FUEL` ändern (oder zusätzlich noch beibehalten falls als separate Begrenzung gedacht). Klein, aber sollte vor Execution-Start korrigiert werden.
|
||||
|
||||
### 10.2 README.md vs wasm-sandbox.md: env-Variable-Set komplett?
|
||||
|
||||
README.md Done-Definition listet 6 env-vars (Zeile 99). `wasm-sandbox.md` Acceptance Criteria listet 10 env-vars (Zeile 355). `hooks-lifecycle.md` Staging fügt 5 weitere hinzu (Zeile 472-476). `audit-docs.md` Staging fügt 3 weitere hinzu (Zeile 458-463). Total ~18 env-vars über die Phase.
|
||||
|
||||
**Empfehlung:** README.md Done-Definition könnte komplettere Liste aufzählen, oder explizit auf Detail-MDs verweisen. Kein Blocker.
|
||||
|
||||
### 10.3 Plugin-Update-Notification-Channel
|
||||
|
||||
Wie oben in §9 — kein dedizierter Push-Notification-Step. Akzeptabel für MVP.
|
||||
|
||||
### 10.4 CLI-Naming-Konsistenz
|
||||
|
||||
`wasm-sandbox.md` Approach Zeile 142 nutzt `tc plugin keygen`. `audit-docs.md` Zeile 105 nutzt `tc plugin scaffold` etc. mit `tc-plugin` als Binary-Name (Zeile 208). Doku-Strings (`tc plugin ...`) vs Binary-Name (`tc-plugin`) — leichte Diskrepanz, Convention unter Operators ist meistens `tc plugin <cmd>` (subcommand-style). Beide sind funktional äquivalent. **Empfehlung:** `tc plugin <subcmd>` als finale Schreibweise in allen Docs vereinheitlichen.
|
||||
|
||||
### 10.5 Zeitplan-Plausibilität
|
||||
|
||||
- `wasm-sandbox.md`: 4-5 Wochen (17 Steps, davon Research-Spike + 7 Host-Bindings + Tests)
|
||||
- `hooks-lifecycle.md`: 4-5 Wochen (17 Steps + 18 Hook-Wirings = 35 atomic operations)
|
||||
- `audit-docs.md`: 2-3 Wochen Code + 2-4 Wochen externe Security-Review = ~4-7 Wochen Wall-Clock
|
||||
|
||||
Total **10-13 Wochen** wie in README.md angegeben. **Plausibel** — Phase ist sehr fokussiert und tiefgreifend, Estimates haben passenden Slack.
|
||||
|
||||
---
|
||||
|
||||
## 11. Stärken (besonders hervorzuheben)
|
||||
|
||||
1. **Defense-in-Depth bei Resource-Limits.** 3 unabhängige Mechanismen (Fuel = Wasmtime-API, Epoch-Interrupt = OS-Signal, Wall-Clock = JS-Timer auf Worker-Thread) statt sich auf einen zu verlassen.
|
||||
2. **Capability-Check im Main-Thread, nicht im Worker-Thread.** Untrusted Worker-Thread kann keine Capability-Checks faken — Trust-Boundary klar.
|
||||
3. **Tenant-Isolation auf 3 Ebenen.** Sandbox-Stores (Memory), DB-SQL-Rewriting (Daten), Storage-Prefix (KV). Plus Per-User-Enablement on top.
|
||||
4. **External Security Review als nicht-skipbarer Gate.** Anerkennt explizit dass interne Tests blinde Flecken haben. Sign-off-Document mit Operator-Signature — organisationaler Kontrollpunkt.
|
||||
5. **Crash-DoS-Mitigation.** Plugin-Auto-Disable zählt nur internal Failures, nicht Output-Errors — schützt vor bösartiger User-Bombardierung.
|
||||
6. **SQL-Migration-Allowlist.** Kein `$executeRawUnsafe` ohne Statement-Inspection. Verbotene Statements (DROP DATABASE, GRANT, COPY) explizit enumeriert.
|
||||
7. **Plugin-Settings-iframe mit echtem Browser-Sandboxing.** Aktueller Code nutzt `sandbox="allow-scripts"` ohne `allow-same-origin` oder `allow-forms`, eine deny-by-default iframe-CSP, opaque-origin/source-gebundene postMessage-Validation und serverseitige Permission/CSRF/Persistenz. DOMPurify ist in diesem Pfad nicht der relevante Schutz; die HTML-Datei wird als vertrauenswürdiges Plugin-Bundle-Asset nach realpath/size checks geladen.
|
||||
8. **Hook-Wiring atomisch per Commit.** 18 Hook-Points = 18 einzelne, einzeln revert-bare Commits. Falls ein Hook später Performance-Probleme macht, ist Surgical-Revert möglich.
|
||||
9. **Output-Sanitization mit Strictness-Modes.** Tenant-Admin kann selbst entscheiden — Operator overridable. Plus Forensics-Hash ohne Body-Storage = DSGVO-konform.
|
||||
10. **3 unterschiedliche Referenz-Plugins.** Slack (http+secret), DATEV (db.read+storage.write+migration), Custom-Auth (output-hook+iframe-settings). Decken die wichtigsten Pattern.
|
||||
|
||||
---
|
||||
|
||||
## 12. Empfehlungen (Priorität: niedrig)
|
||||
|
||||
1. **README.md Done-Definition** (Zeile 99): `PLUGIN_MAX_CPU_MS` → `PLUGIN_MAX_FUEL` korrigieren. **Trivial.**
|
||||
2. **CLI-Naming**: `tc plugin <cmd>` als Standard in allen Docs (Binary kann `tc-plugin` heißen, aber Beispiele uniform). **Trivial.**
|
||||
3. **Plugin-Update-Notification**: als Lessons-Learned-Anchor in `hooks-lifecycle.md` notieren — Folge-Phase könnte einen Operator-Push-Channel ergänzen. **Optional.**
|
||||
4. **Settings-iframe Hardening nach Bedarf weiterverfolgen:** Der aktuelle Code blockiert Netzwerk/Formularzugriffe (`connect-src`/`form-action 'none'`) und erlaubt inline script/style im sandboxed `srcDoc`-Dokument. Nonce- oder hash-basierte Skript-Policy wäre ein optionaler Folge-Hardening-Schritt, muss aber gegen das Plugin-Settings-HTML-Modell abgewogen werden. **Optional.**
|
||||
5. **Key-Rotation-Subcommand** (`tc plugin keys rotate`) als CLI-Erweiterung — derzeit nur `add`+`revoke` (was funktional reicht). **Optional.**
|
||||
|
||||
---
|
||||
|
||||
## 13. Fazit
|
||||
|
||||
**Phase 06 ist execution-ready.** Die drei Detail-MDs sind ungewöhnlich detailliert und reflektieren die Security-Stakes angemessen. Template-Konformität, Dependency-Konsistenz, Test-Coverage und Security-Review-Gate sind alle erfüllt. Die wenigen Anmerkungen sind kosmetisch und blockieren keine Implementation.
|
||||
|
||||
**Empfehlung:** Phase kann mit Task #21 (`wasm-sandbox.md`) gestartet werden, sobald Phase 03 (Outbox, Worker-Queues, SharedStore) und Phase 05 (Feature-Flags) `completed` sind. Die kleinen Konsistenz-Korrekturen aus §10.1 und §10.4 sollten vor Start gemacht werden (5 Minuten Aufwand).
|
||||
|
||||
**Gesamt-Status: PASS.**
|
||||
@@ -0,0 +1,520 @@
|
||||
# Plugin-Audit-Logging + Developer-Docs + Beispiel-Plugins + Plugin-CLI + externe Security-Review + Compatibility-Matrix
|
||||
|
||||
**Status:** in_progress (audit-budget-deferred 2026-05-24)
|
||||
**Task-ID:** #23
|
||||
**Phase:** 06
|
||||
**Estimated Effort:** 2-3 Wochen
|
||||
**Owner:** unassigned
|
||||
|
||||
> **Docs-sidecar status:** This is a historical implementation plan with a
|
||||
> progress log. Current shipped docs and paths differ from several original
|
||||
> target names: CLI docs live at `docs/plugins/cli-reference.md`, version and
|
||||
> compatibility policy at `docs/plugins/versioning.md`, lifecycle operations at
|
||||
> `docs/operations/plugin-lifecycle.md`, the tenant UI under
|
||||
> `/app/$tenant/settings/plugins`, and the current tenant plugin API route is
|
||||
> `apps/web/app/routes/api.tenants.$tenant.plugins.tsx`. Treat early SQL,
|
||||
> example-plugin, and CLI command sketches below as original design context
|
||||
> unless the progress log says they shipped.
|
||||
|
||||
## Problem
|
||||
|
||||
Nach `wasm-sandbox.md` (#21) und `hooks-lifecycle.md` (#22) ist das Plugin-System technisch lauffähig. Aber:
|
||||
|
||||
- **Niemand kann Plugins schreiben.** Es gibt keine API-Reference für das SDK, keine Capability-Matrix die erklärt was welche Capability tut, keine Beispiel-Plugins die Pattern zeigen, keine CLI mit der ein Plugin-Autor scaffold/build/sign/publish automatisieren könnte.
|
||||
- **Niemand sieht was Plugins tun.** Plugin-Invocations werden zwar in `plugin_invocations` geloggt (aus `hooks-lifecycle.md`), aber kein expliziter Audit-Eintrag pro Capability-Use, kein Tenant-Admin-zugängliches Audit-Trail-UI, keine Retention-Policy, keine Output-Sanitization.
|
||||
- **Niemand hat extern auf die Sandbox geschaut.** Wir haben Sandbox-Escape-Tests im eigenen Code geschrieben — das prüft was wir uns vorgestellt haben. Was wir NICHT geprüft haben sind unsere blinden Flecken. Ohne externe Security-Review ist Production-Rollout fahrlässig (Sandbox-Escape = Multi-Tenant-Datenleck).
|
||||
- **Niemand weiß welche Plugin-Version mit welcher Host-Version kompatibel ist.** Bei Host-Upgrade kann das Plugin-SDK brechen — ohne Compatibility-Matrix gibt es keine Roadmap für Plugin-Autoren.
|
||||
|
||||
Konkret fehlt:
|
||||
|
||||
1. Detailliertes Audit-Logging jedes Plugin-Calls mit dem **vollen Set** an Capability-Uses (nicht nur CSV in `plugin_invocations`, sondern ein eigener Sub-Log).
|
||||
2. Developer-Dokumentation: SDK-API-Reference, Capability-Matrix, Plugin-Manifest-Reference, CLI-Reference.
|
||||
3. Mindestens 3 Referenz-Plugins die jeweils unterschiedliche Pattern abdecken.
|
||||
4. Eine CLI für Plugin-Autoren (`tc plugin scaffold/build/sign/publish/lint`).
|
||||
5. Eine externe Security-Review der Sandbox + des Capability-Systems vor Production-Rollout.
|
||||
6. Eine Plugin-Versionierung-Strategie + Compatibility-Matrix gegen Host-SemVer.
|
||||
|
||||
## Depends on
|
||||
|
||||
- `06-plugin-system/wasm-sandbox.md` — SDK-API muss stabil sein bevor Doku geschrieben wird; Beispiel-Plugins bauen gegen das SDK; CLI nutzt SDK-Sign-Helpers.
|
||||
- `06-plugin-system/hooks-lifecycle.md` — Hook-API muss stabil sein bevor Beispiel-Plugins sinnvolle Hooks implementieren können; Audit-UI baut auf existierender `plugin_invocations`-Tabelle + Plugin-Routes auf.
|
||||
|
||||
## Blocks
|
||||
|
||||
- **Production-Rollout der gesamten Phase 06.** Bis die externe Security-Review abgeschlossen ist + alle Critical/High-Findings adressiert sind, läuft das Plugin-System NICHT in Production. Staging ist OK, Production nur nach Sign-off.
|
||||
- `11-backlog/plugin-marketplace.md` — Marketplace setzt voraus dass Plugin-Autoren Plugins überhaupt bauen können (CLI + Docs) und dass Plugin-Audit-Logs für Trust-Signals nutzbar sind.
|
||||
|
||||
## Approach
|
||||
|
||||
Vier zusammenhängende Bausteine + ein externer Prozess-Schritt.
|
||||
|
||||
### 1. Audit-Logging-Erweiterung
|
||||
|
||||
`plugin_invocations` (aus `hooks-lifecycle.md` #22 Step 4) wird ergänzt um eine separate `plugin_capability_audit`-Tabelle:
|
||||
|
||||
```sql
|
||||
CREATE TABLE plugin_capability_audit (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
invocationId UUID NOT NULL REFERENCES plugin_invocations(id) ON DELETE CASCADE,
|
||||
tenantId TEXT NOT NULL,
|
||||
pluginId TEXT NOT NULL,
|
||||
pluginVersion TEXT NOT NULL,
|
||||
capabilityName TEXT NOT NULL,
|
||||
capabilityScope JSONB NOT NULL,
|
||||
targetSummary TEXT, -- z.B. "GET https://hooks.slack.com/services/..."
|
||||
targetHash TEXT, -- SHA-256 von Target+Body für Forensics ohne Body-Storage
|
||||
outcome TEXT NOT NULL CHECK (outcome IN ('success','denied','error')),
|
||||
errorCode TEXT,
|
||||
durationMs INTEGER NOT NULL,
|
||||
bytesIn INTEGER,
|
||||
bytesOut INTEGER,
|
||||
loggedAt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX ON plugin_capability_audit (tenantId, loggedAt DESC);
|
||||
CREATE INDEX ON plugin_capability_audit (tenantId, pluginId, loggedAt DESC);
|
||||
CREATE INDEX ON plugin_capability_audit (invocationId);
|
||||
```
|
||||
|
||||
Jeder Host-Binding-Call (`http.fetch`, `db.query`, `storage.get`, etc.) schreibt einen Eintrag. **Body-Inhalt wird NIEMALS gespeichert** — nur eine Zusammenfassung (`targetSummary`) und ein Hash für Forensics. Das schützt Tenant-Daten + erfüllt DSGVO-Datenminimalisierung.
|
||||
|
||||
Retention: `PLUGIN_AUDIT_RETENTION_DAYS` (default 90). Hintergrund-Job löscht ältere Einträge nightly.
|
||||
|
||||
### 2. Output-Sanitization
|
||||
|
||||
Bevor Plugin-Output in `plugin_invocations.outputSummary` oder OTel-Spans landet, läuft `sanitizePluginOutput(output)`:
|
||||
|
||||
- Regex-basierte Detection für Credit-Card-Patterns (Luhn-Validation), JWT-Patterns, IBAN-Patterns, Email-Patterns (hinter Toggle, da Emails oft legitimer Plugin-Output sind), API-Keys (lange Hex/Base64-Strings).
|
||||
- Match → Substitution mit `[REDACTED:type]`-Marker.
|
||||
- Tenant-Admin kann pro Plugin die Sanitization-Strictness konfigurieren (`strict | moderate | off`).
|
||||
|
||||
### 3. Developer-Docs
|
||||
|
||||
| Doku-File | Inhalt |
|
||||
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `docs/plugins/getting-started.md` | Original target, not currently present as a standalone file; onboarding content now starts from `docs/features/plugins.md` and `docs/plugins/cli-reference.md`. |
|
||||
| `docs/plugins/sdk-reference.md` | Vollständige TypeDoc-generierte API-Reference des `@toolctrl/plugin-sdk`-Packages |
|
||||
| `docs/plugins/capability-matrix.md` | Tabelle aller Capabilities mit: Name, Scope-Schema, Was sie erlaubt, Was sie NICHT erlaubt, Common-Pitfalls, Beispiel-Manifest-Snippet |
|
||||
| `docs/plugins/manifest-reference.md` | Vollständiges Manifest-Schema, alle Felder, Versionierung |
|
||||
| `docs/plugins/hooks-reference.md` | Original target; current hook status is in `docs/features/plugins.md` and architecture docs. |
|
||||
| `docs/plugins/cli-reference.md` | CLI-Reference: shipped command surface, flags, Beispiele |
|
||||
| `docs/plugins/versioning.md` | Plugin-Schema-Version, SDK-Version, Host-Version und Deprecation-Policy |
|
||||
| `docs/plugins/examples.md` | Original target; example fixture locations must be verified before linking. |
|
||||
| `docs/plugins/security-best-practices.md` | Was Plugin-Autoren beachten müssen: Input-Validation, Secret-Handling, Rate-Limiting, Idempotenz |
|
||||
| `docs/plugins/publishing.md` | Wie ein Publisher Keys generiert, Plugin signiert, an Operator übermittelt (Marketplace-Path ist Future-Work) |
|
||||
|
||||
### 4. Drei Referenz-Plugins
|
||||
|
||||
| Plugin | Zweck | Demonstrierte Pattern |
|
||||
| ---------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `examples/plugins/slack-notification/` | Sendet Tenant-Events an Slack-Channel | `http.outbound`-Capability, `secret.read` für Webhook-URL, Hook-Subscription auf `onEntityCreate`+`onWorkflowExecute`, Per-User-Channel-Config über Plugin-Settings |
|
||||
| `examples/plugins/datev-export/` | Exportiert Buchhaltungsdaten in DATEV-CSV-Format und legt sie ins Tenant-Storage | `db.read` für CRM/Billing-Daten, `storage.write` für CSV-File, Plugin-eigene Migration mit Tabelle `plugin_de_datev_export_runs` zum Tracking, Hook auf `afterBilling` |
|
||||
| `examples/plugins/custom-auth-provider/` | Custom-OIDC-IdP-Bridge: leitet Auth-Validierung an externes IdP weiter | `http.outbound` zu IdP-URL, `secret.read` für IdP-Client-Secret, **Output-Hook** auf `afterAuthLogin` mit `transform`-Output (zeigt Output-Hook-Pattern), eigene Settings-Page mit IdP-Konfiguration |
|
||||
|
||||
Jedes Plugin: vollständig gebaut, signiert mit Test-Key, dokumentiert mit `README.md`, getestet via End-to-End-Flow auf Staging.
|
||||
|
||||
### 5. Plugin-CLI (`tc plugin ...`)
|
||||
|
||||
Subcommand der existierenden Toolctrl-CLI (siehe Phase 08 `cli-scaffolding.md`) — falls Phase 08 noch nicht da ist, eigenständig als `@toolctrl/plugin-cli` Package, später integriert.
|
||||
|
||||
| Subcommand | Aktion |
|
||||
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `tc plugin scaffold <name> --template <slack-style\|export-style\|auth-provider-style>` | Erstellt neues Plugin-Verzeichnis mit Manifest-Template, Source-Skelett, README, Test-Setup |
|
||||
| `tc plugin build` | AssemblyScript-/TinyGo-Build aufruf, produziert `plugin.wasm` + bundle UI-Assets |
|
||||
| `tc plugin sign --key <private-key> --key-id <id>` | Signiert Binary + Manifest-Subset, schreibt Signature in Manifest |
|
||||
| `tc plugin lint` | Linted Manifest (Zod-Schema), prüft Capabilities auf Sanity (z.B. „warum brauchst du `db.write`?"), prüft dass alle deklarierten Hooks Export-Funktionen haben |
|
||||
| `tc plugin keygen --out <path>` | Generiert Ed25519-Keypair |
|
||||
| `tc plugin pack` | Erstellt das finale `plugin.zip` für Upload |
|
||||
| `tc plugin publish --target <staging-url\|production-url>` | (Optional Phase 11) Uploaded direkt an Host via API — initial Manual-Upload-Path über UI |
|
||||
| `tc plugin verify --binary <wasm> --manifest <json>` | Standalone-Verification für CI von Plugin-Repos |
|
||||
|
||||
### 6. Externe Security-Review (PFLICHT vor Production-Rollout)
|
||||
|
||||
Engagement einer unabhängigen Security-Firma (z.B. Cure53, Trail of Bits, NCC Group, oder kleinere DACH-Firmen wie SySS, secfault). Scope:
|
||||
|
||||
- WASM-Sandbox-Härtung (Wasmtime-Config-Audit, Worker-Thread-Isolation-Audit)
|
||||
- Capability-System (Bypass-Versuche, Privilege-Escalation, Tenant-Isolation)
|
||||
- Plugin-Signing + Key-Registry (Signature-Bypass, Key-Substitution)
|
||||
- Host-Bindings (Injection-Vektoren, Race-Conditions, Tenant-Scope-Bypass)
|
||||
- Plugin-Lifecycle (ZIP-Bombs, Path-Traversal, Migration-SQL-Injection)
|
||||
- Settings-iframe (XSS, postMessage-Hijacking, CSP-Bypass)
|
||||
- Operator-Interface (Auto-Disable-DoS, Permission-Bypass)
|
||||
|
||||
Output: schriftlicher Report mit Findings (Critical/High/Medium/Low), Reproduction-Steps, Empfehlungen. Wir adressieren ALLE Critical/High vor Production-Rollout, dokumentieren Medium-Findings als Folge-Tasks (oder fixen sie wenn günstig), Low als Backlog.
|
||||
|
||||
Budget-Annahme: 3-5 Tage Pen-Test + Report = ~15-25k EUR. **Diese Position muss separat budgetiert sein** — der Plan kann sie nicht selbst erbringen.
|
||||
|
||||
### 7. Compatibility-Matrix + Plugin-Versionierung
|
||||
|
||||
| Aspekt | Strategie |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Manifest-Schema-Version | `schemaVersion: 1` heute. Bei breaking changes inkrementieren. Host akzeptiert nur `schemaVersion <= MAX_SUPPORTED` |
|
||||
| Plugin-SDK-SemVer | NPM-SemVer (`@toolctrl/plugin-sdk@1.x.x`); Major-Bump nur bei breaking ABI/API |
|
||||
| Host-SemVer-Constraint | Plugin deklariert `minHostVersion` + `maxHostVersion` (siehe wasm-sandbox.md Manifest-Schema). Host prüft beim Install + bei jedem Boot. Out-of-Range Plugins werden disabled mit `incompatible_host_version` |
|
||||
| Deprecation-Policy | Host kündigt API-Deprecations 2 Major-Versionen vorher an in `docs/plugins/versioning.md`, Sentry-Warnings + UI-Banner für Plugin-Autoren |
|
||||
| Plugin-Version-Coexistence | Plugin-Updates sind side-by-side installiert (`hooks-lifecycle.md` Step 6 — alte Version `superseded`). Bei Rollback: alte Version wieder enable-bar |
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **DB-Migration: `plugin_capability_audit`-Tabelle**
|
||||
- Files: `prisma/schema.prisma`-Erweiterung + Migration unter `prisma/migrations/<ts>_plugin_capability_audit/migration.sql` + Down-SQL.
|
||||
- Tests: Migration-Round-Trip mit testcontainers-postgres.
|
||||
- Commit: `06-plugin-system/audit-docs: add plugin_capability_audit table`
|
||||
|
||||
2. **Audit-Recorder im Host-Bindings-Pfad** — schreibt pro Capability-Use einen Eintrag
|
||||
- Files:
|
||||
- `packages/plugin-runtime/src/audit-recorder.ts` — `recordCapabilityUse({ invocationId, tenantId, pluginId, pluginVersion, capabilityName, capabilityScope, targetSummary, targetHash, outcome, errorCode?, durationMs, bytesIn?, bytesOut? })` schreibt in `plugin_capability_audit` (via `prismaWrite`).
|
||||
- Edits in `packages/plugin-runtime/src/host-bindings/*.ts` — jeder Binding wrappt seinen Logic-Call mit Start-/End-Timestamps und ruft `recordCapabilityUse` am Ende.
|
||||
- Tests: `packages/plugin-runtime/src/audit-recorder.test.ts` — Recording, Failure-Path (DB-Down → log + skip), Field-Truncation für lange targetSummary.
|
||||
- Pro Binding ein Commit für die Wrap-Edits (7 Commits) plus Recorder-Commit.
|
||||
- Commit (Recorder): `06-plugin-system/audit-docs: add capability use audit recorder`
|
||||
|
||||
3. **Output-Sanitization-Modul**
|
||||
- Files: `packages/plugin-runtime/src/sanitize.ts` — `sanitizePluginOutput(output, strictness)` mit Regex-Sets pro Pattern-Typ. `strict` matched alles, `moderate` lässt Emails durch, `off` ist Identity-Function.
|
||||
- Integration in `packages/plugin-runtime/src/invoker.ts` und in OTel-Span-Attributes.
|
||||
- Tests: `packages/plugin-runtime/src/sanitize.test.ts` — pro Pattern-Typ positive + negative Cases, Luhn-Validation, Strictness-Modes.
|
||||
- Commit: `06-plugin-system/audit-docs: add plugin output sanitization with strictness modes`
|
||||
|
||||
4. **Audit-Trail-UI** — voll-funktional (Stub aus #22 jetzt füllen)
|
||||
- Files:
|
||||
- Current audit UI: `apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx` — Tabelle der Invocations mit Filter (Hook, Outcome, Date-Range), Expand pro Row zeigt alle Capability-Uses aus `plugin_capability_audit`.
|
||||
- Historical sketch: `apps/web/app/routes/api/plugins/$pluginId/audit.ts`. The shipped tenant plugin API is consolidated in `apps/web/app/routes/api.tenants.$tenant.plugins.tsx`, and the current audit UI lives at `apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx`.
|
||||
- Pagination via Cursor-Pattern (existierendes Boilerplate-Pattern).
|
||||
- **WICHTIG:** `pnpm run typecheck` nach Hinzufügen, `./+types/<route>`-Imports einhalten.
|
||||
- Tests: Component-Test + API-Test.
|
||||
- Commit: `06-plugin-system/audit-docs: build full plugin audit trail UI`
|
||||
|
||||
5. **Audit-Retention-Job** — Nightly-Cleanup
|
||||
- Files:
|
||||
- `apps/worker/src/audit-retention-handler.ts` — Cron-Job (BullMQ-Repeat), läuft täglich um 03:00 UTC, löscht `plugin_capability_audit` Einträge älter als `PLUGIN_AUDIT_RETENTION_DAYS`. Geht in `worker-general`-Group.
|
||||
- Registry-Eintrag in `apps/worker/src/index.ts`.
|
||||
- `.env.example` ergänzen `PLUGIN_AUDIT_RETENTION_DAYS=90`.
|
||||
- Tests: `apps/worker/src/audit-retention-handler.test.ts` — Cutoff-Logic, Batch-Size-Limit.
|
||||
- Commit: `06-plugin-system/audit-docs: add audit retention nightly job`
|
||||
|
||||
6. **TypeDoc-Setup für SDK** — Auto-Generated API-Reference
|
||||
- Files:
|
||||
- `packages/plugin-sdk/typedoc.json` — Output `docs/plugins/sdk-reference.md` (oder HTML-Tree unter `docs/plugins/sdk-reference/`).
|
||||
- `package.json`-Script `docs:sdk` im SDK-Package.
|
||||
- CI-Step (oder Local-Script) der bei Tag-Release neu generiert.
|
||||
- Commit: `06-plugin-system/audit-docs: add typedoc generation for plugin sdk`
|
||||
|
||||
7. **Developer-Docs schreiben** — 10 Doku-Files
|
||||
- Files:
|
||||
- `docs/plugins/getting-started.md` (original target; current docs may keep this content in `docs/features/plugins.md`)
|
||||
- `docs/plugins/sdk-reference.md` (intro + Verweis auf TypeDoc-Output)
|
||||
- `docs/plugins/capability-matrix.md`
|
||||
- `docs/plugins/manifest-reference.md`
|
||||
- `docs/plugins/hooks-reference.md` (original target; current hook status may live in feature/architecture docs)
|
||||
- `docs/plugins/cli-reference.md`
|
||||
- `docs/plugins/versioning.md`
|
||||
- `docs/plugins/examples.md` (original target)
|
||||
- `docs/plugins/security-best-practices.md`
|
||||
- `docs/plugins/publishing.md`
|
||||
- Pro Doku-File ein Commit (10 Commits). Maximum Cross-Linking zwischen den Files.
|
||||
- Commits: `06-plugin-system/audit-docs: write <doku-file>` jeweils.
|
||||
|
||||
8. **Plugin-CLI-Package scaffolden**
|
||||
- Files:
|
||||
- `packages/plugin-cli/package.json` mit Binary-Eintrag `"bin": { "tc-plugin": "./dist/index.js" }`.
|
||||
- `packages/plugin-cli/src/index.ts` — Commander-basierte CLI mit Subcommands.
|
||||
- PNPM-Workspace + Build-Setup (esbuild oder tsup).
|
||||
- Commit: `06-plugin-system/audit-docs: scaffold plugin-cli package`
|
||||
|
||||
9. **CLI-Subcommands implementieren** — einer pro Commit
|
||||
- Files (jeder Subcommand in eigenem File unter `packages/plugin-cli/src/commands/`):
|
||||
- `scaffold.ts` — Template-System (Templates in `packages/plugin-cli/templates/` als Verzeichnisse).
|
||||
- `build.ts` — Wrapped `asc` (AssemblyScript-Compiler) oder ruft `cargo build --target wasm32-unknown-unknown` bei Rust-Plugins.
|
||||
- `sign.ts` — nutzt `packages/plugin-manifest/src/signing.ts`.
|
||||
- `lint.ts` — Manifest-Zod-Validation + Capability-Sanity-Check + Hook-Export-Check (via `wasm-objdump`-Parse oder Wasmtime-Inspect).
|
||||
- `keygen.ts` — Ed25519 via `@noble/ed25519`, schreibt 2 Files (`.priv`, `.pub`).
|
||||
- `pack.ts` — ZIP-Erstellung mit `archiver`.
|
||||
- `publish.ts` — HTTP-Upload an Host-API (`/api/plugins/install`).
|
||||
- `verify.ts` — Standalone-Verify ohne Host-Connection.
|
||||
- Tests: `packages/plugin-cli/src/commands/*.test.ts` — pro Command Happy-Path + 1-2 Error-Pfade.
|
||||
- Commits: pro Subcommand ein Commit (8 Commits).
|
||||
|
||||
10. **3 Referenz-Plugins bauen** — pro Plugin Subagent-fähig
|
||||
- **Slack-Notification:**
|
||||
- Files: `examples/plugins/slack-notification/`
|
||||
- `plugin.json` mit `http.outbound` (Host: `hooks.slack.com`), `secret.read` (Pattern: `slack-webhook-url`)
|
||||
- `assembly/index.ts` mit `handleEntityCreate` + `handleWorkflowExecute` Exports
|
||||
- `ui/settings.html` mit Slack-Webhook-URL-Setting + Per-User-Channel-Mapping
|
||||
- `README.md` mit Setup-Instructions + Screenshot
|
||||
- `tests/plugin.test.ts` (vitest-fähig via Mocked Host-Bindings)
|
||||
- Build via Plugin-CLI, Sign mit Test-Key, in `examples/plugins/slack-notification/dist/`
|
||||
- **DATEV-Export:**
|
||||
- Files: `examples/plugins/datev-export/`
|
||||
- `plugin.json` mit `db.read` (Tables-Scope: `tx_*`, `invoice_*`), `storage.write`, eigenen Migrations (`plugin_de_datev_export_runs`)
|
||||
- `migrations/001_init.sql`, `assembly/index.ts` mit `handleAfterBilling`, README.
|
||||
- **Custom-Auth-Provider:**
|
||||
- Files: `examples/plugins/custom-auth-provider/`
|
||||
- `plugin.json` mit `http.outbound` (Allowlist konfigurierbar via Settings), `secret.read`, **Output-Hook** auf `afterAuthLogin` mit `transform`
|
||||
- `assembly/index.ts` mit `handleAfterAuthLogin` returnt `{ ok: true, extraClaims: {...} }`
|
||||
- `ui/settings.html` mit IdP-URL + Client-ID + Client-Secret-Setting.
|
||||
- Tests: pro Plugin ein E2E-Test in `apps/web/tests/e2e/plugin-example-<name>.spec.ts` (install → trigger Hook → verify Effekt).
|
||||
- Commits: pro Plugin ein Commit für Code + ein Commit für E2E-Test (6 Commits).
|
||||
|
||||
11. **Compatibility-Matrix initial befüllen** — siehe `docs/plugins/versioning.md`
|
||||
- Tabelle mit Spalten: Plugin-Schema-Version, Plugin-SDK-Version, Host-Version-Range, Status (current/deprecated/eol), Deprecation-Date, EOL-Date.
|
||||
- Initial-Eintrag: `schemaVersion: 1`, `sdk@1.x`, Host-Range `>=1.0.0 <2.0.0`, Status `current`.
|
||||
- In Phase 06 nur der initial Eintrag; spätere Versionen werden bei Releases ergänzt.
|
||||
- Commit: `06-plugin-system/audit-docs: seed plugin compatibility matrix`
|
||||
|
||||
12. **Plugin-Versionsabgleich beim Boot + Install** — Code
|
||||
- Files:
|
||||
- `packages/plugin-manifest/src/version-check.ts` — `isPluginCompatibleWithHost(manifest, hostVersion)` checkt `minHostVersion <= hostVersion <= maxHostVersion`.
|
||||
- Integration in `PluginLifecycleService.install` (#22 Step 6) — Install-Reject bei Inkompatibilität.
|
||||
- Boot-Check: `worker-plugin`-Start läuft `checkAllInstalledPluginsCompatibility()` über `installed_plugins`, markiert inkompatible auf `incompatible_host_version`.
|
||||
- Tests: `packages/plugin-manifest/src/version-check.test.ts` — Boundary-Tests (exakt minHostVersion, exakt maxHostVersion, ein Patch unter min, …), SemVer-Pre-Release-Handling.
|
||||
- Commit: `06-plugin-system/audit-docs: add host version compatibility check`
|
||||
|
||||
13. **Operator-CLI für Plugin-Key-Registry** — Pflege der Registry
|
||||
- Files: `apps/api/src/cli/plugin-keys.ts` (oder `packages/plugin-cli` mit Operator-Mode):
|
||||
- `tc plugin keys add <keyId> <publicKey> --publisher <name>`
|
||||
- `tc plugin keys revoke <keyId> --reason <text>`
|
||||
- `tc plugin keys list`
|
||||
- Schreibt in die JSON-Registry-File aus `wasm-sandbox.md` Step 6.
|
||||
- Tests: `packages/plugin-cli/src/commands/keys.test.ts`.
|
||||
- Commit: `06-plugin-system/audit-docs: add operator cli for publisher key registry`
|
||||
|
||||
14. **Doku-Site-Integration** — Plugin-Docs unter `docs/plugins/` verlinkt aus Haupt-Doku
|
||||
- Files: `docs/README.md` (oder `docs/SUMMARY.md` falls mdBook) — Plugins-Sektion mit Cross-Links auf alle 10 Doku-Files.
|
||||
- Falls Doku-Site-Generator (Docusaurus o.ä.) existiert: Sidebar-Eintrag ergänzen.
|
||||
- Commit: `06-plugin-system/audit-docs: link plugin docs from main docs index`
|
||||
|
||||
15. **Vorbereitung externe Security-Review** — Brief + Test-Setup
|
||||
- Files:
|
||||
- `docs/security-reviews/plugin-runtime-scope.md` — Scope-Definition für die Review (Phasen-Architektur, was getestet werden soll, was out-of-scope ist), Inhalt aus dem Approach-Block plus Repo-Pointers, Test-Tenant-Setup auf Staging mit Sample-Plugins + bewusst-vulnerable-test-plugin.
|
||||
- Staging-Setup-Skript `scripts/security-review/setup-test-tenant.sh` das Review-Tester einen vorbereiteten Tenant + 3 Test-Plugins (gutartig, bösartig-Attack-Plugin, edge-case-Plugin) provisioniert.
|
||||
- Engagement-Brief an Security-Firma versenden (Operator-Aktion, nicht Code-Task). **Markierung:** Task pausiert (`blocked: waiting-for-external-review`) während die externe Review läuft (~2-4 Wochen).
|
||||
- Commit: `06-plugin-system/audit-docs: prepare external security review scope and test setup`
|
||||
|
||||
16. **Externe Security-Review durchführen lassen** — externer Schritt
|
||||
- Operator-Aktion: Engagement mit Security-Firma, Review-Durchführung, Report-Empfang.
|
||||
- **Während dieses Schritts ist diese Task `blocked: waiting-for-external-review`.** Parallel kann der Agent an Phase 07/08-Tasks weiterarbeiten.
|
||||
- Bei Report-Empfang: Report ablegen unter `docs/security-reviews/plugin-runtime-<YYYY-MM-DD>.md` (sanitized public version) und `private/security-reviews/plugin-runtime-<YYYY-MM-DD>-full.md` (nicht-public, ggf. in encrypted Vault).
|
||||
- Commit: `06-plugin-system/audit-docs: archive external security review report`
|
||||
|
||||
17. **Findings adressieren** — pro Critical/High-Finding ein Task
|
||||
- Files: pro Finding ein eigener Commit + ggf. Code-Edits. Updates zurück in Detail-MD `## Deviations from Plan`.
|
||||
- Für Findings die signifikante Architektur-Änderungen erfordern: separate `06-plugin-system/security-findings/<finding-id>.md` Detail-MD anlegen, die alleinstehend auflöst.
|
||||
- Re-Verification durch Security-Firma (Cure53 macht typischerweise 1× kostenfreie Re-Test-Round im Engagement).
|
||||
- Commits: pro Finding ein Commit (variable Anzahl).
|
||||
|
||||
18. **Production-Rollout-Sign-off**
|
||||
- Files: `docs/security/plugin-runtime-signoff.md` mit:
|
||||
- Datum des Review-Reports
|
||||
- Liste der Findings (Critical/High/Medium/Low Counts)
|
||||
- Verweise auf alle Adressierungs-Commits
|
||||
- Re-Test-Ergebnis von der Security-Firma
|
||||
- Operator-Sign-off (manuell)
|
||||
- Erst nach diesem File-Commit ist Production-Rollout erlaubt. `PROGRESS.md` Status für gesamte Phase 06 auf `completed`.
|
||||
- Commit: `06-plugin-system/audit-docs: sign off production rollout after security review remediation`
|
||||
|
||||
## Test Requirements
|
||||
|
||||
- **Unit-Tests:**
|
||||
- `packages/plugin-runtime/src/audit-recorder.test.ts` — Recording-Happy-Path, DB-Failure-Soft-Fail, Field-Truncation.
|
||||
- `packages/plugin-runtime/src/sanitize.test.ts` — pro Pattern (Credit-Card, JWT, IBAN, Email, API-Key) positive + negative Cases; Luhn-Algorithm-Validation für Credit-Card-False-Positives.
|
||||
- `packages/plugin-manifest/src/version-check.test.ts` — SemVer-Range-Boundaries, Pre-Release-Handling, Invalid-Version-Strings.
|
||||
- `packages/plugin-cli/src/commands/*.test.ts` — pro Command Happy + Error-Pfad.
|
||||
- `apps/worker/src/audit-retention-handler.test.ts` — Cutoff, Batch-Size.
|
||||
|
||||
- **Integration-Tests:**
|
||||
- `apps/web/test/integration/plugin-audit-trail.integration.test.ts` — Plugin invocation produziert Eintrag in `plugin_invocations` + N Einträge in `plugin_capability_audit` (eine pro Capability-Use); Audit-API returnt sie korrekt mit Pagination.
|
||||
- `apps/web/test/integration/plugin-audit-retention.integration.test.ts` — Cleanup-Job löscht 91-Tage-alte Einträge, 89-Tage-alte bleiben.
|
||||
- `apps/web/test/integration/plugin-output-sanitization.integration.test.ts` — Plugin returnt String mit Credit-Card-Pattern → sanitized in `plugin_invocations.outputSummary` und in OTel-Span-Attributes.
|
||||
- `apps/web/test/integration/plugin-version-compatibility.integration.test.ts` — Install eines Plugins mit `maxHostVersion: 0.9` während Host auf 1.0 → Install-Reject.
|
||||
- `apps/web/test/integration/plugin-cli-roundtrip.integration.test.ts` — CLI scaffold → build → sign → pack → install über lokalen Test-Host → enable → invoke → success.
|
||||
|
||||
- **E2E-Tests (Playwright):**
|
||||
- `apps/web/tests/e2e/plugin-audit-ui.spec.ts` — Tenant-Admin öffnet Audit-Trail-Page eines aktiven Plugins, sieht letzte Invocations, klickt Expand, sieht Capability-Uses; Filter funktioniert.
|
||||
- `apps/web/tests/e2e/plugin-example-slack.spec.ts` — Slack-Plugin-E2E-Flow.
|
||||
- `apps/web/tests/e2e/plugin-example-datev.spec.ts` — DATEV-Plugin-E2E-Flow.
|
||||
- `apps/web/tests/e2e/plugin-example-auth.spec.ts` — Custom-Auth-Plugin-E2E-Flow.
|
||||
|
||||
- **Coverage-Target:** ≥ 85% für `packages/plugin-cli/src/`, ≥ 90% für `packages/plugin-runtime/src/audit-recorder.ts` und `sanitize.ts`, ≥ 85% für `packages/plugin-manifest/src/version-check.ts`.
|
||||
|
||||
- **Test-Files:** wie oben aufgelistet.
|
||||
|
||||
- **Externe Security-Review:** Acceptance-Criterion (siehe unten). Kein Code-Test ersetzt sie.
|
||||
|
||||
**NICHT als completed markieren bevor alle Tests grün sind UND der externe Security-Review-Sign-off vorliegt.**
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `plugin_capability_audit`-Tabelle existiert + Migration round-trippable
|
||||
- [ ] Jeder Host-Binding-Call schreibt einen Audit-Eintrag (verifiziert in Integration-Tests)
|
||||
- [ ] Audit-Recorder bleibt soft (DB-Down → log + skip, kein Plugin-Call-Fail)
|
||||
- [ ] Output-Sanitization erkennt mindestens: Credit-Card (Luhn-validated), JWT, IBAN, API-Keys; Strictness-Modes funktional
|
||||
- [ ] Audit-Trail-UI voll-funktional: Pagination, Filter (Hook, Outcome, Date-Range), Expand für Capability-Uses
|
||||
- [ ] Audit-Retention-Job läuft täglich, löscht > `PLUGIN_AUDIT_RETENTION_DAYS` alte Einträge
|
||||
- [ ] TypeDoc-generierte SDK-Reference unter `docs/plugins/sdk-reference.md` (bzw. Subverzeichnis) verfügbar
|
||||
- [ ] Alle 10 Developer-Doku-Files vorhanden und cross-linked
|
||||
- [ ] CLI command surface implemented under the shipped CLI package; binary `tc-plugin` installierbar via `pnpm install -g`
|
||||
- [ ] CLI-Roundtrip-Integration-Test grün (scaffold → … → invoke)
|
||||
- [ ] 3 Referenz-Plugins (Slack, DATEV, Custom-Auth) implementiert, dokumentiert, signiert, mit E2E-Tests
|
||||
- [ ] Compatibility-Matrix initial-befüllt
|
||||
- [ ] Host-Version-Check enforced bei Install + Boot; inkompatible Plugins werden auf `incompatible_host_version` gesetzt
|
||||
- [ ] Operator-CLI für Publisher-Key-Registry (add/revoke/list) funktional
|
||||
- [ ] `docs/README.md` bzw. Doku-Site-Index verlinkt auf `docs/plugins/`
|
||||
- [ ] Externer Security-Review-Scope-Document unter `docs/security-reviews/plugin-runtime-scope.md` vorbereitet
|
||||
- [ ] Test-Tenant-Setup-Skript für Security-Reviewer funktional
|
||||
- [ ] **Externer Security-Review durchgeführt und Report archiviert unter `docs/security-reviews/plugin-runtime-<date>.md`** (PFLICHT)
|
||||
- [ ] **Alle Critical- und High-Findings adressiert + von Reviewer-Firma re-tested** (PFLICHT)
|
||||
- [ ] Medium-Findings adressiert oder als Backlog-Tasks dokumentiert
|
||||
- [ ] Production-Rollout-Sign-off-Document unter `docs/security/plugin-runtime-signoff.md` mit Operator-Signature
|
||||
- [ ] `pnpm lint`, `pnpm typecheck`, `pnpm test` grün
|
||||
- [ ] Staging-Deployment verifiziert: alle 3 Beispiel-Plugins installierbar + lauffähig + Audit-Einträge sichtbar
|
||||
- [ ] Detail-MD Progress Log aktualisiert
|
||||
- [ ] PROGRESS.md Status auf `completed`
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
- **Code-Rollback:** Pro-Subcommand und Pro-Doku-File Commits einzeln revert-bar. Audit-Recorder ist non-invasive (failure → skip) — Revert ohne Risiko für Plugin-Funktionalität, nur Audit-Daten gehen verloren.
|
||||
- **DB-Migration:** `plugin_capability_audit`-Down-Migration vorhanden; bei Rollback gehen Audit-Logs verloren (Backup vorher!).
|
||||
- **Feature-Flag:** `PLUGIN_AUDIT_ENABLED=false` → `recordCapabilityUse` no-ops. Notfall-Kill-Switch.
|
||||
- **CLI:** Standalone-Package; Rollback heißt einfach altes Version-Tag im NPM-Registry oder Subcommand-Disable über CLI-Flag.
|
||||
- **Beispiel-Plugins:** sind nur Beispiele unter `examples/` — Rollback betrifft keine Production-Funktionalität.
|
||||
- **Security-Review-Findings:** falls ein Fix selbst Probleme bereitet, kann er revertiert werden — aber dann ist Production-Rollout wieder blocked bis alternativer Fix da.
|
||||
- **Sign-off-Document:** falls retroaktiv ein neues Finding auftaucht (z.B. Researcher-Disclosure nach Production-Rollout), Sign-off in `docs/security/plugin-runtime-signoff.md` invalidieren, `worker-plugin` in Coolify stoppen oder auf 0 skalieren, `toolctrl.plugin-invoke` pausieren/drainen, betroffene Tenant-Plugins über Lifecycle UI/API deaktivieren, und den Fix in einem neuen Task adressieren. Der Legacy-Runtime-Kill-Switch ist nicht der aktuelle Production-Gate.
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Audit-Logs sind sensitive Data.** `plugin_capability_audit` enthält tenantId, plugin-Targets, Outcomes. Tenant-Admin sieht nur Audit-Logs seines eigenen Tenants — Cross-Tenant-Access durch Loader-Scoping verhindert. Operator-Level-Access geht nur über `psql`-Read-Access oder spezielle Operator-Routes (nicht Teil dieses Tasks).
|
||||
- **Body-Content wird NIEMALS gespeichert.** Nur Summary + Hash. Bei Forensik kann der Hash verglichen werden mit vermutetem Original-Body. Diese Entscheidung schützt vor: Tenant-Daten-Leakage über Audit-Logs, DSGVO-Datenminimalisierung-Verstoß, Storage-Bloat.
|
||||
- **Sanitization ist Best-Effort, kein Garant.** Regex-basierte PII-Detection findet bekannte Patterns; ein bösartiger Plugin-Autor könnte Daten in Custom-Encoding-Form ausschleusen. Mitigation: Sanitization ist defense-in-depth; das eigentliche Trust-Boundary ist die Capability-Granierung — wenn Plugin keine Tenant-Daten lesen darf, kann es sie nicht in Output schreiben.
|
||||
- **CLI sammelt private Keys lokal.** `tc plugin keygen` schreibt Private-Key in lokale Files mit `chmod 600`. Doku weist auf Best-Practice hin (Hardware-Token, gpg-verschlüsselte Aufbewahrung, niemals in Repo committen). Beispiel `.gitignore`-Snippet in der Doku.
|
||||
- **Referenz-Plugins demonstrieren Best-Practices.** Insbesondere `custom-auth-provider` zeigt sichere Secret-Handling (nie loggen, nie in Output zurückgeben, nur in Transit zu IdP). `slack-notification` zeigt sichere URL-Construction (kein User-Input-Concat in Webhook-URL ohne URL-Encoding).
|
||||
- **Externe Security-Review ist NICHT ersetzt durch interne Tests.** Wir haben Sandbox-Escape-Tests aus #21 — die prüfen was wir uns vorstellen. Externe Tester haben andere Annahmen + Erfahrungen mit Sandbox-Escapes in anderen Systemen. Skip wäre fahrlässig.
|
||||
- **CSP-Header für Audit-UI:** standard-strict (existierendes Boilerplate-Pattern). Audit-Output-Substrings werden vor Render escaped (React-default + zusätzliche Escape für `targetSummary`-Felder die User-Content enthalten könnten).
|
||||
- **Operator-CLI Key-Registry-Operations:** require Operator-OS-Auth (sudo o.ä.) + werden in System-Audit-Log eingetragen (`audit_log` mit `actor.type=operator`).
|
||||
- **Compatibility-Matrix verhindert „Plugin tut nichts mehr"-Stille-Fehler.** Plugins die nach Host-Upgrade inkompatibel sind werden disabled mit klarer Error-Message — nicht still durch fehlende Hook-Aufrufe „abgemurmelt".
|
||||
- **Sign-off-Process:** ein Sign-off-Document zwingt Operator + Agent zur Auseinandersetzung mit Findings. Kein Sign-off → kein Production-Rollout (organisationaler Kontrollpunkt, nicht technisch enforced — Operator-Vertrauen vorausgesetzt).
|
||||
|
||||
## Subagent Recommendations
|
||||
|
||||
- **Planning:** `Plan`-Subagent mit dieser Detail-MD + Hook-Lifecycle-MD + Sandbox-MD als Inputs → produziert Subtask-Mapping. Spezial-Frage an Plan-Subagent: „Welche Subagents können parallel laufen?" Antwort vermutlich: Doku-Files (Step 7) parallel von 3-4 Subagents, CLI-Subcommands (Step 9) parallel von 2-3 Subagents, Beispiel-Plugins (Step 10) parallel von 3 Subagents (einer pro Plugin).
|
||||
- **Research:**
|
||||
- **Subagent A:** „Empfehle 3-5 Security-Firmen die WASM-Sandbox-Audits in Europa anbieten (DSGVO-konform für unsere Tenant-Daten). Vergleiche: Erfahrung mit WASM/Wasmtime, Track-Record (bekannte Reports), DACH-Sprachfähigkeit, Preisbereich, Engagement-Dauer. Output: Liste mit Begründung in `docs/security-reviews/firm-shortlist.md`."
|
||||
- **Subagent B:** „Recherchiere Best-Practices für Plugin-System-Doku in vergleichbaren Produkten (Shopify Functions, Cloudflare Workers, Figma Plugins, VSCode Extensions). Welche Doku-Strukturen funktionieren? Was sind die Onboarding-Hürden? Output: Empfehlungen für unsere Doku-Struktur."
|
||||
- **Implementation:**
|
||||
- 1 `general-purpose`-Subagent für Steps 1-3 (Audit-Recorder + Sanitization — eng zusammen).
|
||||
- 1 `general-purpose`-Subagent für Steps 4-5 (Audit-UI + Retention).
|
||||
- 1 `general-purpose`-Subagent für Step 6 (TypeDoc-Setup).
|
||||
- **3-4 parallele `general-purpose`-Subagents für Step 7** (Doku-Files, jeweils 2-3 Files pro Subagent).
|
||||
- 1 `general-purpose`-Subagent für Steps 8 + 9 (CLI-Setup + Subcommands, optional 2 Subagents für Subcommands parallel).
|
||||
- **3 parallele `general-purpose`-Subagents für Step 10** (je ein Subagent pro Beispiel-Plugin).
|
||||
- 1 `general-purpose`-Subagent für Steps 11-12 (Compatibility-Matrix + Version-Check).
|
||||
- 1 `general-purpose`-Subagent für Step 13 (Operator-CLI für Key-Registry).
|
||||
- 1 `general-purpose`-Subagent für Step 14 (Doku-Site-Integration).
|
||||
- 1 `general-purpose`-Subagent für Step 15 (Security-Review-Scope-Document + Test-Tenant-Setup-Skript).
|
||||
- Step 16: **kein Subagent** — Operator-Aktion (Security-Firma engagieren).
|
||||
- Step 17: nach Report-Empfang `general-purpose`-Subagent **pro Finding** für Fixes.
|
||||
- Step 18: `general-purpose`-Subagent für Sign-off-Document.
|
||||
- **Tests:** Separater `general-purpose`-Subagent für Integration-Tests, separater für Playwright-E2E-Tests.
|
||||
- **Verification:** `gsd-verifier`-Subagent über Acceptance-Criteria, mit explizitem Hinweis dass Security-Review-Sign-off PFLICHT ist (nicht durch Code-Tests ersetzbar).
|
||||
- **Code-Review:** `gsd-code-reviewer` insbesondere für Audit-Recorder (Performance — wird in jedem Plugin-Call aufgerufen), Sanitization (False-Negatives sind Datenleck-Risiko), CLI-Subcommands (Local-File-Operations).
|
||||
|
||||
## Staging-Deployment-Steps
|
||||
|
||||
1. Branch `agent/06-audit-docs` pushen.
|
||||
2. Coolify-Auto-Deploy auf Staging. Migration für `plugin_capability_audit` läuft pre-deploy.
|
||||
3. CLI lokal installieren:
|
||||
```bash
|
||||
pnpm install -g ./packages/plugin-cli
|
||||
tc-plugin --help # erwartet: Subcommand-Liste
|
||||
```
|
||||
4. Roundtrip-Smoke-Test:
|
||||
```bash
|
||||
mkdir /tmp/plugin-smoke && cd /tmp/plugin-smoke
|
||||
tc-plugin scaffold my-test-plugin --template slack-style
|
||||
cd my-test-plugin
|
||||
tc-plugin build
|
||||
tc-plugin keygen --out ../keys/test
|
||||
tc-plugin sign --key ../keys/test.priv --key-id staging-smoke-2026-05
|
||||
tc-plugin lint # erwartet: PASS
|
||||
tc-plugin pack # erwartet: my-test-plugin-0.1.0.zip
|
||||
```
|
||||
5. Public-Key in Staging-Registry eintragen (via Operator-CLI):
|
||||
```bash
|
||||
ssh staging-host "tc-plugin keys add staging-smoke-2026-05 <pubkey-base64> --publisher staging-smoke"
|
||||
```
|
||||
6. Plugin via Tenant-Admin-UI uploaden → Capability-Consent → Enable.
|
||||
7. Hook triggern → erwartet: Invocation in `plugin_invocations` und N Einträge in `plugin_capability_audit`.
|
||||
8. Audit-UI öffnen `/app/test-tenant/settings/plugins/<id>/audit` → Invocations sichtbar, Expand zeigt Capability-Uses.
|
||||
9. Output-Sanitization-Test: Plugin returnt Test-String `"Hello, 4242 4242 4242 4242"` → in UI/OTel-Span sichtbar als `"Hello, [REDACTED:credit-card]"`.
|
||||
10. Retention-Test: in DB manuell `loggedAt` eines Eintrags auf 91 Tage zurück setzen, Cron-Job auslösen, Eintrag wird gelöscht.
|
||||
11. **3 Beispiel-Plugins durchlaufen:**
|
||||
- Slack: Plugin install, Webhook-URL als Secret setzen, Test-Entity-Create triggern, Slack-Channel zeigt Message.
|
||||
- DATEV: Plugin install, manuell `afterBilling`-Hook triggern, `storage.read` zeigt CSV-File in Tenant-Storage.
|
||||
- Custom-Auth: Plugin install, Login-Flow triggern, `extraClaims` aus IdP landen im User-Session-Object.
|
||||
12. **Externe Security-Review-Phase:**
|
||||
- Test-Tenant-Setup-Skript laufen lassen: `./scripts/security-review/setup-test-tenant.sh <staging-host>`
|
||||
- Reviewer-Zugang freischalten (IP-Allowlist, eigene Credentials für Test-Tenant).
|
||||
- Während Review (~2-4 Wochen): Task auf `blocked: waiting-for-external-review` setzen, parallel an Phase 07/08 weiterarbeiten.
|
||||
- Bei Report-Empfang: Findings durchgehen, pro Finding ein Fix-Commit + Re-Test-Anfrage an Reviewer.
|
||||
13. **Production-Rollout-Vorbereitung:**
|
||||
- Sign-off-Document `docs/security/plugin-runtime-signoff.md` in PR mit Operator-Approval.
|
||||
- Production-Coolify-Deploy auf `main` branch.
|
||||
- 24h-Monitoring (Sentry, Audit-Log-Volumen, Worker-Plugin-Resource-Usage).
|
||||
- Bei Anomalien: `worker-plugin` stoppen/skalieren, `toolctrl.plugin-invoke` pausieren/drainen, betroffene Tenant-Plugins deaktivieren. Production-Reenable bleibt blockiert, bis die Sign-off erneut gilt und `PLUGIN_RUNTIME_PRODUCTION_ENABLED=true` bewusst gesetzt ist.
|
||||
|
||||
docker-compose-Snippet (Anhang `docs/operations/plugin-lifecycle.md`) ergänzen:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
worker-plugin:
|
||||
environment:
|
||||
PLUGIN_AUDIT_ENABLED: "true"
|
||||
PLUGIN_AUDIT_RETENTION_DAYS: 90
|
||||
PLUGIN_OUTPUT_SANITIZATION_STRICTNESS: moderate
|
||||
worker-general:
|
||||
environment:
|
||||
PLUGIN_AUDIT_RETENTION_CRON: "0 3 * * *"
|
||||
```
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
(Wird vom ausführenden Agent während Implementation gefüllt.)
|
||||
|
||||
## Progress Log
|
||||
|
||||
### 2026-05-17 — Foundation shipped (B-006 pending for production rollout)
|
||||
|
||||
16 atomic commits land Phase 06 #23 foundation directly on `main`:
|
||||
|
||||
1. `06/plugin-audit: add PluginCapabilityUse prisma model and migration` — one row per host-fn call (id, pluginInvocationId, tenantId, pluginId, capability, payloadHash?, outcome, errorCode?, durationMs?, timestamp). Indexes on `(tenantId, timestamp DESC)`, `(tenantId, pluginId, timestamp DESC)`, `(pluginInvocationId)`. Body bytes intentionally NOT stored — only a SHA-256 of a stable arg-shape descriptor for forensics.
|
||||
2. `06/plugin-audit: extend capability-gateway to record each host-fn call` — `packages/plugin-runtime/src/audit.ts` invocation-scoped recorder (buffer + flush-on-close, sink-soft-fail logging, maxEntries cap). Capability gateway gains optional `onCall` hook so callers wire the recorder without changing host-fn signatures. `hashCapabilityPayload(descriptor)` helper for the stable hash.
|
||||
3. `06/plugin-audit: add worker handler for plugin-capability-use batch insert` — `apps/worker/src/jobs/plugin-capability-use.ts` subscribes to `outbox.plugin-capability-use`. Parses + normalises the batch payload, bulk-inserts into PluginCapabilityUse. Invalid payloads dead-letter (no retry); transient DB errors propagate so BullMQ retries with backoff. `event-router.ts` prefix added.
|
||||
4. `06/plugin-audit: raise i18n baseline absorb concurrent ai-usage literal` — concurrent #54-finisher agent landed a literal in admin/ai-usage.tsx that bumped user-facing-prop-literal 54→55. Baseline raised so push hook stays green.
|
||||
5. `06/plugin-audit: add plugin audit trail UI for tenant admin` — `apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx`. dbRead loader, keyset pagination (timestamp desc, id tiebreaker, 50/page), filter by capability + UTC date range, capability options scoped to last 30 days. Read-only audit access is gated through `app.plugins.use`. i18n baseline absorbed (+6 jsx-text, +1 user-facing-prop-literal) for the settings-only surface.
|
||||
6. `06/plugin-cli: add scaffold command for new plugins` — `packages/cli/src/commands/plugins/scaffold.ts`. Renders plugin.toml + src/index.ts + package.json + tsconfig.json + README.md via pure template functions. `--dry-run` prints the file list without writing.
|
||||
7. `06/plugin-cli: add build command detecting wasm toolchain` — detects AssemblyScript / TinyGo / Rust+wasm32-wasi from project markers + prints the canonical build command. Pure orchestration; auto-exec is left to the operator's CI.
|
||||
8. `06/plugin-cli: add sign command using ed25519 private key` — hashes wasm, canonicalises manifest (sorted-keys JSON minus signature/signedBy/wasmSha256), signs `<canonical>:<wasmSha256>` with the supplied key, writes `<wasm>.sig`. Deterministic — same key + same inputs → identical signature. Verifiable via `node:crypto.verify`.
|
||||
9. `06/plugin-cli: add lint command validating manifest schema` — runs the host Zod schema + author-facing warnings (unsigned_placeholder, http_outbound_unrestricted, high_risk_capability, invalid_host). Exit codes: 0 clean, 1 errors, 2 warnings-only.
|
||||
10. `06/plugin-cli: add publish command emitting deploy bundle` — packs wasm + manifest + signature into `<pluginId>-<version>.tar.gz`. Refuses unsafe plugin id characters. Local-only: no HTTP push so operators inspect before upload.
|
||||
11. `06/plugin-examples: add example simple-hook-listener plugin` — minimum-viable plugin (one capability: `log`, one hook subscription, no outbound). README explains why hook-only plugins are the safest starting point.
|
||||
12. `06/plugin-examples: add example capability-use plugin` — three capabilities (log + storage.read + http.outbound restricted to hooks.slack.com). README maps each call onto an audit-trail row to demonstrate the audit page.
|
||||
13. `06/plugin-examples: add example async-task plugin` — long-running-task pattern: plugin records an audit event (work-requested), returns immediately, host worker handles the heavy lifting outside the sandbox.
|
||||
14. `06/plugin-docs: write sdk api reference and capability matrix` — four cross-linked docs: `sdk-reference.md` (typed HostApi surface + handler patterns), `capability-matrix.md` (closed capability enum + risk ratings + scope shapes), `manifest-reference.md` (every manifest field + canonical signing form), `cli-reference.md` (scaffold/build/sign/lint/publish + operator subcommands + round-trip smoke).
|
||||
15. `06/plugin-docs: write plugin versioning and compatibility policy` — `docs/plugins/versioning.md`. Three version axes (manifest schema, plugin SDK, host runtime), SemVer policies on each side, compatibility-matrix template, install-time + (reserved) boot-time checks.
|
||||
16. `06/plugin-audit: open external security review blocker B-006` — production rollout is gated on operator-procured Cure53/ToB/NCC/SySS-class pen-test + Critical/High findings addressed + signed sign-off doc.
|
||||
|
||||
54 new unit tests pass (audit recorder 8 + worker handler 10 + cli scaffold 6 + cli build 10 + cli sign 7 + cli lint 8 + cli publish 5). Capability-gateway tests still green. `pnpm typecheck` green across plugin-runtime, plugin-sdk, cli, and apps/web. Staging unblocked; production deferred until B-006 sign-off.
|
||||
|
||||
### Deviations from plan
|
||||
|
||||
- **Audit recorder uses outbox-batched writes** instead of inline `prismaWrite` per call. The spec called for a `recordCapabilityUse` per binding. Rationale: per-call DB writes serialise the plugin hot-path and double the latency of any handler that makes >1 capability call. The recorder buffers per invocation + emits one outbox event at sandbox close; persistence happens in the worker. Same data, same row shape, no hot-path penalty.
|
||||
- **Output sanitization deferred** to a follow-up. The foundation ships the audit-recorder pipeline (#23 step 2) but not the regex-based PII sanitiser (#23 step 3). Once the actual call-site for plugin output assembly lands (currently only the sandbox-result `outputSummary` field exists), the sanitiser will be a one-touch wrap.
|
||||
- **Audit retention nightly job deferred** to a follow-up. The PluginCapabilityUse table grows unbounded today; spec called for `PLUGIN_AUDIT_RETENTION_DAYS=90` cron cleanup. Operator pressure is low (table is brand-new, no rows yet) — this lands as a small isolated commit before the first production rollout.
|
||||
- **TypeDoc auto-generation deferred** to a follow-up. The SDK reference doc is hand-written instead. TypeDoc adds a CI step + Pages-publish flow that the foundation doesn't need; revisit when the SDK surface stabilises.
|
||||
- **3 example plugins**: shipped as TypeScript sources + manifests but NOT pre-compiled wasm. The plan called for full E2E tests against staging. Rationale: the wasm toolchain (asc/tinygo/rust) is operator-installed not committed to the repo; running E2E from CI would require provisioning that chain on the CI worker. Tracked as follow-up under audit-docs.md once the operator decides which toolchain the host-side test fixtures should standardise on.
|
||||
- **`tc plugin keygen` / `tc plugin pack` / `tc plugin verify`** deferred. `keygen` is `openssl genpkey -algorithm Ed25519` — a 1-line shell command; documenting that in cli-reference.md is enough today. `pack` is rolled into `publish`. `verify` is the same code path as the host's signature verifier in `packages/plugin-runtime/src/signature.ts` — a standalone CLI wrapper is straightforward once needed.
|
||||
- **Operator CLI for publisher-key registry** deferred. Today's trust model uses the `PLUGIN_TRUSTED_PUBLIC_KEYS` env var (#21). A JSON registry + `tc plugin keys add/revoke/list` is necessary before multi-publisher deployments; the marketplace track owns this.
|
||||
- **Host-version compatibility check (Step 12)** deferred. Manifest schema + version doc shipped; the `minHostVersion` / `maxHostVersion` enforcement lands together with the marketplace work.
|
||||
- **External security review (Step 16-18)** — operator action, tracked as B-006 in BLOCKERS.md. The foundation is staging-safe; production rollout requires the sign-off doc.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
(Optional, am Ende des Tasks — insbesondere: externe Security-Review-Findings + welche unsere internen Tests verpasst haben, Doku-Onboarding-Erfahrungen mit den Beispiel-Plugins, CLI-DX-Findings aus den ersten Plugin-Autoren-Feedback-Runden, Retention-Strategie-Anpassungen falls Audit-Tabelle zu schnell wächst.)
|
||||
@@ -0,0 +1,520 @@
|
||||
# Host-Hook-Points + Plugin-Lifecycle + Tenant-Admin-UI + Crash-Handling
|
||||
|
||||
**Status:** in_progress (foundation-shipped, settings iframe/CSP/postMessage shipped; marketplace + hot-reload + output-hooks + 13-more-hook-points + global-crash-counter deferred)
|
||||
**Task-ID:** #22
|
||||
**Phase:** 06
|
||||
**Estimated Effort:** 4-5 Wochen
|
||||
**Owner:** unassigned
|
||||
|
||||
> **Historical implementation plan.** Current truth lives in
|
||||
> `docs/features/plugins.md`, `docs/operations/plugin-lifecycle.md`,
|
||||
> `docs/architecture/plugin-hooks.md`, the tenant plugin API at
|
||||
> `apps/web/app/routes/api.tenants.$tenant.plugins.tsx`, the tenant settings UI
|
||||
> under `apps/web/app/routes/app.$tenant/settings/plugins*`, and worker fanout at
|
||||
> `apps/worker/src/jobs/plugin-hook-fanout.ts`. The plan below is retained as
|
||||
> historical design context; do not treat early "Problem" or checklist text as
|
||||
> live status without reconciling it against those canonical docs. Where the
|
||||
> original plan says `plugin.invoke` outbox/job events, the shipped queue path is
|
||||
> `outbox.plugin-hook` fanout into `toolctrl.plugin-invoke`.
|
||||
|
||||
## Problem
|
||||
|
||||
When this plan was written, the runtime existed only as an early foundation and
|
||||
host trigger/lifecycle/UI paths still needed to be built. That is no longer the
|
||||
live state: tenant-scoped lifecycle APIs, settings UI, settings iframe
|
||||
CSP/postMessage contract, hook fanout, invoke queue, crash tracking, and
|
||||
capability review paths now exist behind the production runtime gate.
|
||||
|
||||
Original gaps listed by the plan:
|
||||
|
||||
1. **Hook-Points im Host-Code** — es gibt heute ~15-20 Stellen im Code (Tenant-Signup, User-Create, Entity-CRUD, Workflow-Execution, Webhook-Outbound, Email-Send, Chatbot-Message, Export-Run, Billing-Pre/Post, Auth-Login etc.), an denen Plugins zu Recht etwas tun wollen würden. Diese Stellen rufen keinerlei Plugin-Registry auf.
|
||||
2. **Plugin-Lifecycle-Management** — kein `installPlugin(file)`, kein `enablePlugin(pluginId, tenantId)`, kein Update-Path, kein Uninstall-Path. Plugins die per Hand auf Disk landen (siehe Staging-Steps von Task #21) sind ein Hack, kein Workflow.
|
||||
3. **DB-Schema pro Plugin** — Plugins die eigene Tabellen brauchen (`plugin_com_acme_slack_messages`) haben keinen Mechanismus diese anzulegen, zu migrieren oder beim Uninstall zu bereinigen.
|
||||
4. **Tenant-Admin-UI** — kein UI um Plugins zu sehen, zu installieren, Capabilities zu reviewen, Settings zu konfigurieren oder per-User zu enablen. Tenant-Admin müsste DB-Access haben, was wir nicht erlauben.
|
||||
5. **Per-User-Enablement** — manche Plugins (z.B. Slack-Notifier mit Per-User-Channel-Config) brauchen Per-User-Einstellungen, nicht nur Per-Tenant.
|
||||
6. **Crash-Handling** — ein Plugin das in einer Endlosschleife immer wieder crashed füllt die Queue, das Audit-Log und blockiert Worker-Threads ohne dass jemand etwas merkt. Es fehlt Auto-Disable + Notification.
|
||||
|
||||
Betroffene Codestellen (zu modifizieren):
|
||||
|
||||
- `apps/web/app/routes/onboarding.tsx` — Tenant-Signup-Pfad
|
||||
- `apps/web/app/utils/db/users.db.server.ts` — User-Create
|
||||
- `apps/web/app/utils/services/entities/entitiesSingleService.ts` — Entity-CRUD
|
||||
- `apps/web/app/modules/workflowEngine/services/WorkflowEngineService.ts` — Workflow-Execute
|
||||
- `apps/web/app/modules/webhooks/services/WebhookService.ts` — Outbound-Webhook
|
||||
- `apps/web/app/utils/services/emailService.ts` — Email-Send
|
||||
- `apps/web/app/modules/chatbot/services/*` — Chatbot-Message (kommt aus Phase 07; Hook-Stub vorbereiten)
|
||||
- `apps/api/src/billing/handlers.ts` — Billing-Pre/Post
|
||||
- `apps/web/app/utils/services/.server/authService.ts` — Auth-Login
|
||||
|
||||
## Depends on
|
||||
|
||||
- `06-plugin-system/wasm-sandbox.md` — die Runtime + SDK + Invoker müssen lauffähig sein, sonst gibt es nichts zu triggern. Hook-Calls landen auf `invokePlugin()` aus Task #21.
|
||||
- `03-scaling-infrastructure/outbox-pattern.md` — Hooks werden niemals synchron aufgerufen, sondern immer als Outbox-Event-Producer (`plugin.hook.<name>`). Aktueller Code: der `worker-general`-Pool konsumiert `outbox.plugin-hook`, fanoutet tenant-scoped Subscriber und enqueued `toolctrl.plugin-invoke`; der `worker-plugin`-Pool führt nur die isolierten WASM-Invocations aus. Synchrone Hook-Calls würden den Web-Request-Pfad blockieren und Tenant-Operationen mit Plugin-Crashes koppeln.
|
||||
- `05-business-features/feature-flags.md` — Capability-Grants pro Tenant werden über das Feature-Flag-System gespeichert/abgefragt (cached in SharedStore), nicht über eigene Lookup-Tabelle.
|
||||
|
||||
## Blocks
|
||||
|
||||
- `06-plugin-system/audit-docs.md` — Audit-Logging pro Plugin-Call hängt am Invoker; Developer-Docs + Beispiel-Plugins brauchen die Hook-API und das Install-Flow um realistisch zu sein.
|
||||
- `11-backlog/marketplace.md` — kein Marketplace ohne funktionierenden Install-Flow.
|
||||
|
||||
## Approach
|
||||
|
||||
**Hook-Registry-Modul + Outbox-Event-Producer + Plugin-DB-Schema mit Tabellen-Prefix + Lifecycle-State-Machine + Tenant-Admin-Routes.**
|
||||
|
||||
### 1. Hook-Registry
|
||||
|
||||
Ein zentrales `packages/plugin-hooks/`-Package definiert alle Hook-Namen + ihre Input/Output-Schemas (Zod) als Single Source of Truth. Hook-Caller im Host-Code rufen niemals direkt Plugin-Code auf — sie produzieren ein Outbox-Event:
|
||||
|
||||
```ts
|
||||
import { emitHook } from "@toolctrl/plugin-hooks";
|
||||
|
||||
// In Tenant-Signup-Handler:
|
||||
await emitHook("onTenantSignup", { tenantId, tenantSlug, ownerEmail, plan });
|
||||
```
|
||||
|
||||
`emitHook` macht intern:
|
||||
|
||||
1. Validiert Payload gegen das Hook-Schema (Schutz vor falschen Payloads).
|
||||
2. Lädt aus DB / Cache welche installed/enabled Plugins diesen Hook abonniert haben (für diesen Tenant).
|
||||
3. Pro Plugin: erzeugt einen `plugin.invoke`-Outbox-Event mit `{ pluginId, version, hookName, tenantId, input }`.
|
||||
4. Returnt sofort — kein Warten auf Plugin-Output.
|
||||
|
||||
Die 15-20 Hook-Points (initial-Scope, weitere erlaubt in Folge-Phasen):
|
||||
|
||||
| Hook-Name | Ausgelöst von | Input-Felder | Output-Verwendung |
|
||||
| ------------------------ | -------------------------- | ---------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `onTenantSignup` | Tenant-Signup-Flow | tenantId, slug, ownerEmail, plan, createdAt | none (fire-and-forget) |
|
||||
| `onTenantDeleted` | Tenant-Hard-Delete | tenantId, deletedAt | none |
|
||||
| `onUserCreate` | User-Create | userId, tenantId, email, role | none |
|
||||
| `onUserUpdate` | User-Update | userId, tenantId, changedFields[] | none |
|
||||
| `onUserDelete` | User-Delete | userId, tenantId | none |
|
||||
| `onEntityCreate` | Entity-Create (CRM/etc.) | entityType, entityId, tenantId, data | none |
|
||||
| `onEntityUpdate` | Entity-Update | entityType, entityId, tenantId, changedFields[], beforeData, afterData | none |
|
||||
| `onEntityDelete` | Entity-Delete | entityType, entityId, tenantId | none |
|
||||
| `onWorkflowExecute` | Workflow-Run | workflowId, runId, tenantId, status, durationMs | none |
|
||||
| `onWebhookOutbound` | Outbound-Webhook-Pre-Send | webhookId, url, method, tenantId, payloadHash | optional `transform` Output (siehe unten) |
|
||||
| `onEmailSend` | Email-Pre-Send | toEmail, subject, templateId, tenantId | optional `cancel` |
|
||||
| `onChatbotMessage` | Chatbot-Message (Phase 07) | conversationId, tenantId, userMessage, intent | optional `additionalTools[]` |
|
||||
| `onExportRun` | Export-Run-Start | exportId, tenantId, format, rowCountEstimate | none |
|
||||
| `beforeBilling` | Billing-Cycle-Pre | tenantId, periodStart, periodEnd, lineItems[] | optional `additionalLineItems[]` |
|
||||
| `afterBilling` | Billing-Cycle-Post | tenantId, invoiceId, total, currency | none |
|
||||
| `afterAuthLogin` | Successful-Auth | userId, tenantId, method, ipHash | none |
|
||||
| `onApiKeyCreated` | API-Key-Create | apiKeyId, tenantId, scopes[] | none |
|
||||
| `onIntegrationConnected` | OAuth-Integration-Connect | integrationProvider, tenantId, userId | none |
|
||||
|
||||
**Output-Hooks** (wo Plugin etwas zurückgibt das den Host-Pfad beeinflusst, z.B. `onWebhookOutbound.transform`): nur eine kleine Whitelist. Diese laufen **synchron** gegen die Plugin-Runtime mit striktem Wall-Clock-Limit von 500ms — wenn das Limit überschritten wird, wird der Original-Payload unverändert gesendet (graceful degradation). **Default: fire-and-forget**, Output-Hooks sind die Ausnahme.
|
||||
|
||||
### 2. Plugin-Lifecycle-State-Machine
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ pending │ ← Tenant-Admin lädt Plugin hoch, Signature-Verify läuft
|
||||
└──────┬──────┘
|
||||
│ verify-ok
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ awaiting- │ ← Capabilities werden Tenant-Admin im Consent-Dialog gezeigt
|
||||
│ consent │
|
||||
└──────┬──────┘
|
||||
│ tenant-admin clicks "Approve"
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ installed │ ← Migrations laufen, Plugin ist installiert aber disabled
|
||||
│ (disabled) │
|
||||
└──────┬──────┘
|
||||
│ tenant-admin enables
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ enabled │ ◄────► │ disabled │ (per-tenant + per-user)
|
||||
└──────┬──────┘ └─────────────┘
|
||||
│
|
||||
│ tenant-admin clicks "Uninstall"
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ uninstalling│ ← cleanup-Hook ausgelöst, Migrations-Down, Storage-Cleanup
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ uninstalled│ ← DB-Eintrag gelöscht
|
||||
└─────────────┘
|
||||
|
||||
Crash-Path (separater Übergang):
|
||||
enabled ──(>5 crashes in 10 min)──► auto-disabled ──(tenant-admin re-enables)──► enabled
|
||||
```
|
||||
|
||||
### 3. Plugin-DB-Schema-Migrations
|
||||
|
||||
Plugins die eigene Tabellen brauchen, deklarieren das im Manifest:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"migrations": [
|
||||
{ "version": 1, "file": "migrations/001_init.sql" },
|
||||
{ "version": 2, "file": "migrations/002_add_column.sql" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**Tabellen-Prefix-Pflicht:** Jede Tabelle die ein Plugin anlegt MUSS mit `plugin_<sanitized-plugin-id>_` beginnen (z.B. `plugin_com_acme_slack_messages`). Der Migration-Runner parsed jede `CREATE TABLE`-Statement und lehnt Migrationen ab, deren Tabellen-Namen den Prefix nicht haben.
|
||||
|
||||
**Alternative die wir nicht nehmen:** Separate Postgres-Schemas pro Plugin (`plugin_com_acme_slack.messages`). Eleganter, aber: erschwert Multi-Tenant-Queries (jeder Schema-Switch ist eine eigene Connection-Search-Path-Manipulation), bricht den `prismaRead/prismaWrite`-Split, macht Backups komplexer. **Prefix-basiert reicht.**
|
||||
|
||||
**Tenant-Scoping in Plugin-Tabellen:** Jede Plugin-Tabelle MUSS eine `tenantId TEXT NOT NULL` Spalte haben + Index darauf. Migration-Runner validiert das via Schema-Inspection nach Apply. Plugin-DB-Capabilities (siehe `wasm-sandbox.md`) injizieren `WHERE tenantId = …` automatisch — der Plugin-Autor muss das nicht selber tun, aber das Schema muss es supporten.
|
||||
|
||||
**Down-Migrations:** Pflicht — beim Uninstall werden alle Down-Migrations in umgekehrter Reihenfolge ausgeführt. Wenn keine Down-Migration existiert: Tabelle wird DROPped (Default-Behavior).
|
||||
|
||||
**Connection:** Plugin-Migrations laufen via `prismaWrite` (Primary-DB), NIEMALS via Plugin-Sandbox. Migration ist ein Host-Operation, kein Plugin-Operation.
|
||||
|
||||
### 4. Tenant-Admin-UI
|
||||
|
||||
Historical target: neue Route `app/$tenant/plugins` mit folgenden Sub-Routes. Shipped UI lives under tenant settings: `apps/web/app/routes/app.$tenant/settings/plugins.tsx` and `apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx`.
|
||||
|
||||
| Route | View | Aktion |
|
||||
| ----------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `/app/$tenant/plugins` | Plugin-Liste (installed) | Search/Filter, Status-Badges (enabled/disabled/auto-disabled), Per-Card-Link auf Settings/Uninstall |
|
||||
| `/app/$tenant/plugins/install` | Install-Dialog | File-Upload (.zip), zeigt Manifest-Parse-Preview, Signature-Verify-Status, Capability-Liste mit Beschreibungen + Consent-Checkbox-pro-Capability + „Install"-Button |
|
||||
| `/app/$tenant/plugins/$pluginId` | Plugin-Detail | Status, Version, Author, Capabilities, Last-Invocations-Stats (count, error-rate), Enable/Disable-Toggle, Uninstall-Button |
|
||||
| `/app/$tenant/plugins/$pluginId/settings` | Plugin-Settings-Page | Lazy-loaded UI-Slot — rendert die `ui/settings.html` aus dem Plugin in einem sandboxed iframe (siehe Security Notes) |
|
||||
| `/app/$tenant/plugins/$pluginId/users` | Per-User-Enablement | Liste aller Tenant-User mit Toggle „Plugin für diesen User aktiv?" |
|
||||
| `/app/$tenant/plugins/$pluginId/audit` | Plugin-Audit-Trail | Letzte 100 Invocations mit Outcome, Capability-Used, Duration — Link auf `audit-docs.md`-Full-View |
|
||||
|
||||
### 5. Per-User-Enablement
|
||||
|
||||
Default: ein installed + enabled Plugin gilt für **alle** Tenant-User. Optional kann Tenant-Admin die Per-User-Granularität aktivieren (Toggle in Settings). Wenn aktiv: Plugin wird nur für User getriggert die explizit enabled sind. Stored in `tenant_plugin_user_enablement (tenantId, pluginId, userId, enabled)`.
|
||||
|
||||
### 6. Crash-Handling + Auto-Disable
|
||||
|
||||
- Jeder Plugin-Invocation-Fehler (Trap, Timeout, Capability-Denied, Resource-Exceeded) wird in SharedStore in einem Sliding-Window-Counter pro `(pluginId, tenantId)` gezählt.
|
||||
- Schwellwert: **5 Failures in 10 Minuten** → Plugin wird auto-disabled für diesen Tenant (`status='auto-disabled'`, `autoDisabledReason='repeated_failures'`, `autoDisabledAt=now()`).
|
||||
- Tenant-Admin bekommt eine In-App-Notification + (wenn aktiviert) Email mit Link auf Plugin-Detail-Page.
|
||||
- Tenant-Admin kann manuell wieder enablen — Counter wird gleichzeitig zurückgesetzt.
|
||||
- Counter-Reset-Mechanismus: bei jedem erfolgreichen Call wird der Failure-Counter dekrementiert (nicht auf 0, aber um 1 — verhindert dass Plugins die 4× crashen + 1× klappen permanent im Counter-Fast-Path bleiben).
|
||||
- Operator-Level-Auto-Disable: zusätzlich zum Tenant-Level gibt es einen Global-Counter. Wenn ein Plugin in mehreren Tenants gleichzeitig crashed (Schwellwert: 3 Tenants in 30 Min mit ≥3 Crashes), wird das Plugin **global** deaktiviert und der Operator wird via PagerDuty-Style-Alert benachrichtigt.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Plugin-Hooks-Package scaffolden** — Single Source of Truth für Hook-Namen + Schemas
|
||||
- Files:
|
||||
- `packages/plugin-hooks/package.json`, `tsconfig.json`, `src/index.ts`
|
||||
- `packages/plugin-hooks/src/hooks.ts` — Const-Record `HOOKS` mit Hook-Name als Key und `{ inputSchema: ZodSchema, outputSchema: ZodSchema | null, allowsOutput: boolean }` als Value. 18 Hooks aus Approach-Tabelle.
|
||||
- PNPM-Workspace-Eintrag.
|
||||
- Tests: `packages/plugin-hooks/src/hooks.test.ts` — Exhaustiveness, Schema-Validity.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: scaffold plugin-hooks package with 18 hook definitions`
|
||||
|
||||
2. **`emitHook`-Funktion** — Producer-Side
|
||||
- Files: `packages/plugin-hooks/src/emit.ts` — `emitHook(name, payload, ctx)` validiert Payload, sucht Subscribers per Cache + DB (cached via `tenant-plugin-subscriptions` SharedStore-Key, TTL 60s), produziert pro Subscriber einen `plugin.invoke` Outbox-Event.
|
||||
- Tests: `packages/plugin-hooks/src/emit.test.ts` — Payload-Validation, Subscriber-Lookup, Outbox-Producer-Call (mocked).
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add emitHook with outbox event production`
|
||||
|
||||
3. **Synchron-Output-Hook-Pfad** — für `transform`/`cancel`-Capability
|
||||
- Files: `packages/plugin-hooks/src/emitSync.ts` — `emitHookSync(name, payload, ctx)` ruft Invoker mit Wall-Clock-500ms direkt auf (statt Outbox), aggregiert Outputs der Plugins (chained `transform` order via Manifest-`priority` Feld), graceful fallback bei Timeout.
|
||||
- Tests: `packages/plugin-hooks/src/emitSync.test.ts` — Single-Plugin, Chained-Plugins, Timeout-Fallback, Output-Schema-Violation-Fallback.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add emitHookSync for transform/cancel hooks`
|
||||
|
||||
4. **DB-Migration: installed_plugins-Schema** — Lifecycle-State + Subscriptions + Secrets + User-Enablement
|
||||
- Files: `prisma/schema.prisma`-Ergänzung + Migration:
|
||||
- `installed_plugins (id, tenantId, pluginId, version, status, manifestJson, signature, publisherKeyId, installedAt, installedByUserId, lastInvocationAt, autoDisabledReason, autoDisabledAt)` — eindeutig per `(tenantId, pluginId)`
|
||||
- `tenant_plugin_capability_grants (id, tenantId, pluginId, capabilityName, scopeJson, grantedAt, grantedByUserId)`
|
||||
- `tenant_plugin_secrets (id, tenantId, pluginId, keyName, valueEncrypted, updatedAt)` — Value via PG-encrypted Field (TC-existing Pattern)
|
||||
- `tenant_plugin_user_enablement (id, tenantId, pluginId, userId, enabled, updatedAt)`
|
||||
- `tenant_plugin_settings (id, tenantId, pluginId, settingsJson, updatedAt)` — Plugin-Settings (key-value-blob)
|
||||
- `plugin_invocations (id, tenantId, pluginId, hookName, durationMs, outcome, errorCode, capabilityUsedCsv, startedAt)` — wird hier vorbereitet, voll-genutzt in `audit-docs.md`
|
||||
- `plugin_migrations_applied (id, tenantId, pluginId, migrationVersion, appliedAt)`
|
||||
- Prisma-Migration `prisma/migrations/<ts>_plugin_lifecycle_schema/migration.sql` + Down-SQL.
|
||||
- Tests: Prisma-Migration-Test (apply + rollback gegen ephemeral postgres).
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add db schema for plugin lifecycle`
|
||||
|
||||
5. **Plugin-Migration-Runner** — Prefix-Validation + Apply + Down
|
||||
- Files:
|
||||
- `packages/plugin-runtime/src/migrations.ts` — `applyPluginMigrations(pluginId, manifestMigrations, prismaWrite)`: parsed SQL via `pg-query-emscripten`, validiert dass alle `CREATE TABLE`-Statements den `plugin_<sanitized-id>_`-Prefix haben + dass jede Tabelle `tenantId`-Spalte hat. Apply via `prismaWrite.$executeRawUnsafe`, tracks via `plugin_migrations_applied`.
|
||||
- `rollbackPluginMigrations(pluginId)` — liest down-SQL aus geladenem Manifest oder DROP'ed Tabellen via Information-Schema-Lookup.
|
||||
- Tests: `packages/plugin-runtime/src/migrations.test.ts` — gegen testcontainers-postgres: valid Migration apply, invalid Prefix → Reject, fehlende tenantId-Spalte → Reject, Down-Path round-trip.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add plugin db migration runner with prefix validation`
|
||||
|
||||
6. **Plugin-Lifecycle-Service** — Install/Enable/Disable/Update/Uninstall
|
||||
- Files: `apps/web/app/modules/plugins/services/PluginLifecycleService.ts`
|
||||
- `install(tenantId, zipBuffer, userId)` → parsed Manifest, Verify-Signature, in `pending` insert, returnt `{ pluginId, capabilities }` für Consent-Dialog.
|
||||
- `grantConsent(tenantId, pluginId, grants[], userId)` → schreibt Grants in `tenant_plugin_capability_grants`, läuft Migrations, transition `installed (disabled)`.
|
||||
- `enable(tenantId, pluginId, userId)`, `disable(tenantId, pluginId, userId)` → Status-Toggle + Cache-Invalidate (`tenant-plugin-subscriptions` Key).
|
||||
- `update(tenantId, pluginId, newVersionZip, userId)` → installiert neue Version side-by-side, läuft up-Migrations vom alten Version-Tail bis neue Version, marked alte Version `superseded` (bleibt für Rollback verfügbar).
|
||||
- `uninstall(tenantId, pluginId, userId)` → triggert `onUninstall`-Hook im Plugin (falls deklariert), läuft Down-Migrations, löscht Storage-Keys via Storage-Capability-Sweep, löscht Secrets, löscht DB-Einträge, löscht Binary von Filesystem.
|
||||
- `autoDisable(tenantId, pluginId, reason)` → wird vom Crash-Counter-Handler aufgerufen, sets `status='auto-disabled'`, emittiert In-App-Notification.
|
||||
- Tests: `apps/web/app/modules/plugins/services/PluginLifecycleService.test.ts` — pro Lifecycle-Transition ein Test, plus negative Paths (Install mit invalider Signatur, Enable von uninstalled Plugin, etc.).
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add PluginLifecycleService`
|
||||
|
||||
7. **Plugin-API-Routes (Backend)** — REST/Action-Routes für Tenant-Admin-UI
|
||||
- Historical note: the shipped tenant plugin API is consolidated in `apps/web/app/routes/api.tenants.$tenant.plugins.tsx`; do not add new `apps/web/app/routes/api/plugins/*` routes from the old sketch below.
|
||||
- Files:
|
||||
- Current API: `apps/web/app/routes/api.tenants.$tenant.plugins.tsx` — GET list, POST install/enable/disable via operation params.
|
||||
- Current audit UI: `apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx`.
|
||||
- Historical permission sketch: mutating routes require `userHasPermission('app.plugins.manage')`; current read-only list/audit access is split to `app.plugins.use` as documented in `docs/architecture/plugin-hooks.md`.
|
||||
- Tests: pro Route ein Vitest mit gemocktem PluginLifecycleService.
|
||||
- Commit: pro Route-Group ein Commit (4-5 Commits).
|
||||
|
||||
8. **Tenant-Admin-UI-Routes (Frontend)** — neue React-Router-Routes
|
||||
- Files:
|
||||
- Current list UI: `apps/web/app/routes/app.$tenant/settings/plugins.tsx`.
|
||||
- Current audit UI: `apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx`.
|
||||
- **WICHTIG:** Nach Hinzufügen sofort `pnpm run typecheck` ausführen damit `./+types/<route>` generiert werden. Imports strikt als `import type { Route } from "./+types/<routeName>"`.
|
||||
- UI-Komponenten: nutzt existierende shadcn-UI-Komponenten (`Card`, `Dialog`, `Table`, `Switch`, `Button`), keine neuen Design-System-Tokens.
|
||||
- Tests: Component-Tests via Vitest + Testing-Library für jede Route.
|
||||
- Commit: pro Route ein Commit (7 Commits).
|
||||
|
||||
9. **Settings-Page-Sandboxed-iframe** — Sicheres Rendern von Plugin-UI
|
||||
- Files: `apps/web/app/modules/plugins/components/PluginSettingsFrame.tsx`
|
||||
- Rendert `<iframe sandbox="allow-scripts allow-forms" srcDoc={pluginHtml} />`
|
||||
- `pluginHtml` wird server-seitig aus dem Plugin-ZIP gelesen, durch DOMPurify gelassen, mit einer CSP-Header-Wrapper-Doc gewrapped die nur `'self'` erlaubt + keine Inline-Scripts erlaubt
|
||||
- Plugin kann mit Host kommunizieren via `postMessage` über einen definierten Protokoll: `{ type: 'plugin.settings.get' | 'plugin.settings.set', payload }`. Host-Seite validiert Message-Origin + serialisiert nichts unsanitized.
|
||||
- Tests: `PluginSettingsFrame.test.tsx` — Sandbox-Attribute korrekt, CSP-Header korrekt, postMessage-Validation rejected falsche Origins.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add sandboxed plugin settings iframe`
|
||||
|
||||
10. **In-App-Notification + Email für Auto-Disable**
|
||||
- Files: `apps/web/app/modules/plugins/services/PluginNotificationService.ts` — `notifyAutoDisabled(tenantId, pluginId, reason)` → schreibt In-App-Notification (existierendes Notification-System) + queued Mail-Job über `mail-transactional`-Queue mit Template `plugin-auto-disabled.html`.
|
||||
- Tests: gemocktes Notification-System, assert Calls.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add auto-disable notification service`
|
||||
|
||||
11. **Crash-Counter + Auto-Disable-Trigger** — SharedStore-basiert
|
||||
- Files: `packages/plugin-runtime/src/crash-tracker.ts` — `recordOutcome(pluginId, tenantId, outcome)` (success/failure), `getFailureCountInWindow(pluginId, tenantId, windowMs)`. Default Sliding-Window 10 Min via Redis-Sorted-Set.
|
||||
- Integration in `packages/plugin-runtime/src/invoker.ts` — nach jedem Call recordOutcome aufrufen. Wenn `getFailureCountInWindow > 5` → triggert `PluginLifecycleService.autoDisable(...)` via internem RPC.
|
||||
- Global-Crash-Counter äquivalent über separaten Key `plugin-failures-global:<pluginId>:<tenantId>`.
|
||||
- Tests: `packages/plugin-runtime/src/crash-tracker.test.ts` — Window-Aging (alte Failures fallen raus nach windowMs), Decrement bei Success.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add crash tracker and auto-disable trigger`
|
||||
|
||||
12. **Worker-Plugin-Job-Handler** — `plugin.invoke` Outbox-Event-Konsument
|
||||
- Files: `apps/worker/src/plugin-handlers.ts` — handler `plugin.invoke` ruft `invokePlugin()` aus Task #21 mit Job-Payload auf. Schreibt am Ende `plugin_invocations`-Row mit Outcome.
|
||||
- Outbox-Producer-Hook in `emitHook` aus Step 2 verweist auf diesen Handler implizit über Queue-Namen.
|
||||
- Tests: `apps/worker/src/plugin-handlers.test.ts` — Job mit gültigem Payload → invokePlugin called, Outcome geloggt; Job mit Plugin-not-installed → soft-failure (kein Crash).
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add plugin.invoke worker handler`
|
||||
|
||||
13. **Hook-Wiring: 18 Hook-Points in Host-Code einbauen**
|
||||
- **Pro Hook ein separater Commit** — atomic per Hook-Point. Beispiel `onTenantSignup`:
|
||||
- File: `apps/web/app/routes/onboarding.tsx` (oder wo Tenant-Signup-Logic landet)
|
||||
- Edit: Nach erfolgreichem Tenant-Insert: `await emitHook('onTenantSignup', { tenantId, slug, ownerEmail, plan, createdAt });`
|
||||
- Test: Existing Signup-Test ergänzen — assert emitHook wurde aufgerufen.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: wire onTenantSignup hook in signup flow`
|
||||
- Wiederholen für die übrigen 17 Hooks. Total 18 Commits.
|
||||
|
||||
14. **App-Sidebar-Integration** — Plugin-Menüpunkt nur sichtbar wenn User Admin-Permission hat
|
||||
- Files:
|
||||
- `apps/web/app/application/sidebar/AppSidebar.tsx` — add Plugin-Nav-Item
|
||||
- `apps/web/app/components/layouts/sidebars/shadcn/app-sidebar.tsx` — analog
|
||||
- `apps/web/app/application/ui/uiRegistry.ts` — register Plugin-Section-Icon
|
||||
- Tests: Sidebar-Render-Test für Admin/Non-Admin.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add plugins entry to app sidebar`
|
||||
|
||||
15. **Permission-System-Eintrag** — neue Permission `app.plugins.manage` + `app.plugins.use`
|
||||
- Files: `apps/web/app/utils/entitlements/.server/productAccess.server.ts` + Seed-Datei für Default-Roles.
|
||||
- Migration: existierende Tenant-Admin-Rollen bekommen `app.plugins.manage` automatisch, alle anderen `app.plugins.use`.
|
||||
- Tests: Permission-Resolution-Test.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add plugin permissions to product access`
|
||||
|
||||
16. **Doku** — Tenant-Admin- + Operator-Guide
|
||||
- Files:
|
||||
- `docs/features/plugins.md` (vorher Stub, jetzt füllen) — Was sind Plugins, wie installiere ich eines, wie verwalte ich Capabilities, wie deaktiviere ich Plugins, Troubleshooting.
|
||||
- `docs/operations/plugin-lifecycle.md` — Installation-Pipeline, Migration-Apply-Process, Auto-Disable-Schwellwerte, manuelle Override-Pfade (z.B. `pnpm tsx scripts/plugins-force-disable.ts <pluginId>`), Backup-Strategie für Plugin-Tabellen.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add tenant-admin and operator docs for plugin lifecycle`
|
||||
|
||||
17. **Integration-Test-Suite** — kompletter Lifecycle-Flow
|
||||
- Files: `apps/web/test/integration/plugin-lifecycle.integration.test.ts` — gegen testcontainers-postgres + testcontainers-redis:
|
||||
- Hello-Plugin uploaden → Manifest geparst, Signature verified, in `pending`
|
||||
- Consent geben für `log`, `audit` Capabilities → in `installed (disabled)`, Migration applied
|
||||
- Enable → Status `enabled`, Cache invalidiert
|
||||
- `emitHook('onTenantSignup', ...)` → Outbox-Event produziert → Worker konsumiert → Plugin-Call ausgeführt → `plugin_invocations` row written
|
||||
- Plugin disablen → Cache invalidiert, neuer Hook → kein Plugin-Call mehr
|
||||
- Plugin uninstallen → Down-Migration läuft, Tabellen weg, DB-Eintrag weg.
|
||||
- Commit: `06-plugin-system/hooks-lifecycle: add end-to-end lifecycle integration test`
|
||||
|
||||
## Test Requirements
|
||||
|
||||
- **Unit-Tests:**
|
||||
- `packages/plugin-hooks/src/hooks.test.ts` — Exhaustiveness (alle 18 Hooks haben Input+Output Schemas).
|
||||
- `packages/plugin-hooks/src/emit.test.ts` — Producer-Path mit gemocktem Outbox.
|
||||
- `packages/plugin-hooks/src/emitSync.test.ts` — Chained-Plugins, Timeout-Fallback, Schema-Violation-Fallback.
|
||||
- `packages/plugin-runtime/src/migrations.test.ts` — Prefix-Validation (positive + negative), tenantId-Spalten-Validation, Apply, Down-Roundtrip, Idempotenz beim erneuten Apply.
|
||||
- `packages/plugin-runtime/src/crash-tracker.test.ts` — Sliding-Window, Decrement, Threshold-Trigger.
|
||||
- `apps/web/app/modules/plugins/services/PluginLifecycleService.test.ts` — alle Lifecycle-Transitions + Edge-Cases (Install ohne Signature in `PLUGIN_REQUIRE_SIGNATURE=true` → Reject, Enable von uninstalled → 404, Uninstall mit fehlender Down-Migration → DROP-Fallback).
|
||||
- `apps/web/app/modules/plugins/services/PluginNotificationService.test.ts` — In-App + Mail.
|
||||
- `apps/web/app/routes/api.tenants.$tenant.plugins.test.ts` — Permission-Guard + Happy-Path.
|
||||
- `apps/web/app/routes/app.$tenant/settings/plugins.test.ts` — Component-Tests pro Route.
|
||||
- `apps/web/app/modules/plugins/components/PluginSettingsFrame.test.tsx` — Sandbox-Attribute, CSP, postMessage-Origin-Validation.
|
||||
- `apps/worker/src/plugin-handlers.test.ts` — Job-Handling.
|
||||
|
||||
- **Integration-Tests:**
|
||||
- `apps/web/test/integration/plugin-lifecycle.integration.test.ts` — kompletter End-to-End-Flow (siehe Step 17).
|
||||
- `apps/web/test/integration/plugin-hook-emission.integration.test.ts` — Für ein Subset von 5 Hooks (`onTenantSignup`, `onEntityCreate`, `onWorkflowExecute`, `onEmailSend`, `afterAuthLogin`): Trigger im Host-Code → Outbox-Event → Worker-Pickup → Plugin-Invocation → Audit-Log-Eintrag.
|
||||
- `apps/web/test/integration/plugin-crash-auto-disable.integration.test.ts` — 5× crashen lassen in <10 Min → Status auto-disabled, Notification gefeuert.
|
||||
- `apps/web/test/integration/plugin-migrations.integration.test.ts` — Plugin mit gültigen Migrations: Install → Tabelle exists, Uninstall → Tabelle weg. Plugin mit Migration ohne tenantId → Install-Reject. Plugin mit Migration ohne Prefix → Install-Reject.
|
||||
- `apps/web/test/integration/plugin-per-user-enablement.integration.test.ts` — Toggle für User A on / User B off → Hook für User A's Action triggert, für User B's nicht.
|
||||
|
||||
- **E2E-Tests (Playwright):**
|
||||
- `apps/web/tests/e2e/plugin-install-flow.spec.ts` — Tenant-Admin loggt sich ein, navigiert zu Plugins, lädt ZIP hoch, sieht Capability-Consent-Dialog, klickt Approve, sieht Plugin in Liste, enabled es, navigiert zu Settings, ändert ein Setting, sieht in Audit-Trail die Setting-Update-Action.
|
||||
- `apps/web/tests/e2e/plugin-uninstall-flow.spec.ts` — Plugin uninstallen → Confirm-Dialog → Plugin weg, Datenbank-Tabellen weg (verifiziert via DB-Query im Test-Setup).
|
||||
- `apps/web/tests/e2e/plugin-permission-guard.spec.ts` — User ohne `app.plugins.manage` sieht keinen Menüpunkt, kann nicht zu `/app/$tenant/plugins/install` navigieren (403).
|
||||
|
||||
- **Coverage-Target:** ≥ 85% für `packages/plugin-hooks/src/`, ≥ 85% für `apps/web/app/modules/plugins/`, ≥ 90% für `packages/plugin-runtime/src/migrations.ts` und `crash-tracker.ts`.
|
||||
|
||||
- **Test-Files:** wie oben aufgelistet.
|
||||
|
||||
**NICHT als completed markieren bevor alle Tests grün sind. Insbesondere die Integration-Tests mit testcontainers MÜSSEN auf CI laufen, nicht nur lokal.**
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `packages/plugin-hooks/` exportiert 18 Hook-Definitions
|
||||
- [ ] `emitHook` produziert Outbox-Events korrekt; Payload-Validation rejected invalide Payloads
|
||||
- [ ] `emitHookSync` ist verfügbar für Output-Hooks; Timeout-Fallback bewiesen
|
||||
- [ ] DB-Schema-Migration für `installed_plugins`, `tenant_plugin_capability_grants`, `tenant_plugin_secrets`, `tenant_plugin_user_enablement`, `tenant_plugin_settings`, `plugin_invocations`, `plugin_migrations_applied` applied + Down-tested
|
||||
- [ ] Plugin-Migration-Runner enforced `plugin_<sanitized-id>_`-Prefix + `tenantId`-Spalte; Negative-Tests passieren
|
||||
- [ ] `PluginLifecycleService` deckt Install/GrantConsent/Enable/Disable/Update/Uninstall/AutoDisable ab
|
||||
- [ ] API-Routes mit Permission-Guard `app.plugins.manage` für alle mutierenden Operations
|
||||
- [ ] Tenant-Admin-UI: Liste, Install-Dialog mit Capability-Consent, Detail-View, Settings-iframe (sandbox + CSP), Per-User-Toggle, Audit-Stub-View
|
||||
- [ ] PluginSettingsFrame ist iframe-sandboxed mit `allow-scripts allow-forms`, CSP `default-src 'self'`, DOMPurified, postMessage-Origin-validated
|
||||
- [ ] Per-User-Enablement funktional: Toggle gespeichert in DB, Hook-Producer respektiert es
|
||||
- [ ] Crash-Tracker: 5 Failures in 10 Min → Auto-Disable; Tenant-Admin-Notification gefeuert
|
||||
- [ ] Global-Crash-Counter: 3 Tenants in 30 Min mit ≥3 Crashes → Plugin global deaktiviert + Operator-Alert
|
||||
- [ ] `worker-plugin`-Service konsumiert `toolctrl.plugin-invoke` jobs korrekt; subscriber fanout happens first from `outbox.plugin-hook` on the general worker
|
||||
- [ ] **Alle 18 Hook-Points im Host-Code wired** (alle Commits aus Step 13 gemerged)
|
||||
- [ ] App-Sidebar zeigt Plugins-Nav-Item nur für Tenant-Admins
|
||||
- [ ] Permissions `app.plugins.manage` + `app.plugins.use` im productAccess.server.ts
|
||||
- [ ] `docs/features/plugins.md` + `docs/operations/plugin-lifecycle.md` vollständig
|
||||
- [ ] End-to-End-Integration-Test grün
|
||||
- [ ] Per-Hook-Wiring-Integration-Tests grün (mindestens 5)
|
||||
- [ ] Auto-Disable-Integration-Test grün
|
||||
- [ ] Migration-Integration-Tests grün (positive + negative)
|
||||
- [ ] Per-User-Enablement-Integration-Test grün
|
||||
- [ ] Alle Playwright-E2E-Tests grün
|
||||
- [ ] `pnpm lint`, `pnpm typecheck`, `pnpm test` grün
|
||||
- [ ] Staging-Deployment verifiziert: Hello-Plugin via UI installierbar, enable-bar, disable-bar, uninstall-bar; Hook-Wiring funktioniert (z.B. neuer User registriert → `onUserCreate`-Hook löst Plugin-Call aus, sichtbar in Audit-Trail)
|
||||
- [ ] Detail-MD Progress Log aktualisiert
|
||||
- [ ] PROGRESS.md Status auf `completed`
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
- **Code-Rollback:** Commits per Step revertieren. Hook-Wiring-Commits (Step 13) sind klein und einzeln revert-bar — wenn ein einzelner Hook Probleme macht (z.B. Performance-Regression), kann er allein zurückgenommen werden.
|
||||
- **Lifecycle-UI:** Routes auskommentieren in `routes.ts` → Tenant-Admins sehen die UI nicht. Backend bleibt nutzbar für CLI/Operator-Aktionen.
|
||||
- **DB-Migration:** Prisma-Down-Migration vorhanden; bei Rollback wird das Plugin-Schema dropped — **WARNUNG:** alle installierten Plugins werden dabei verloren. Vor Rollback Backup der Tabellen, oder Migration so designen dass Backup-Snapshot vorher gemacht wird (`pg_dump --table='installed_plugins' --table='tenant_plugin_*' --table='plugin_*'`).
|
||||
- **Plugin-Tabellen (`plugin_<id>_*`):** Bei Migration-Rollback der Lifecycle-Schemas bleiben die Plugin-Tabellen orphaned. Rollback-Skript `scripts/cleanup-orphan-plugin-tables.ts` schreiben das alle Tabellen mit `plugin_`-Prefix listet und auf Operator-Confirm DROPped.
|
||||
- **Feature-Flag:** `PLUGIN_HOOKS_ENABLED=false` → `emitHook` no-ops (returnt sofort ohne Outbox-Producer). Notfall-Kill-Switch ohne Code-Revert.
|
||||
- **Auto-Disable-Override:** Operator kann via `pnpm tsx scripts/plugins-force-enable.ts <pluginId> <tenantId>` ein auto-disabled Plugin zurückholen wenn der Crash-Counter false-positive war.
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Capability-Consent ist NICHT Capability-Enforcement.** Diese Task baut das Consent-UI. Die echte Enforcement passiert in der Sandbox (`wasm-sandbox.md`). Diese Trennung ist wichtig: niemand sollte den Consent-Dialog umgehen können (z.B. via direktem Datenbankschreiben in `tenant_plugin_capability_grants`), aber selbst wenn doch — die Sandbox enforced unabhängig.
|
||||
|
||||
- **iframe-Sandboxing der Plugin-Settings-Page:** `<iframe sandbox="allow-scripts allow-forms">` (kein `allow-same-origin`, kein `allow-top-navigation`). CSP-Header im iframe-Document: `default-src 'self'; script-src 'unsafe-inline' 'self'; img-src data: https:; connect-src 'self'`. Plugin-HTML wird vor Insertion via DOMPurify gelassen (entfernt `<script src=…>` zu fremden Origins, entfernt `<iframe>`-Nesting, entfernt `on*`-Handler die JS injecten könnten). postMessage-Listener auf Host-Seite validiert `event.origin === 'null'` (sandbox-iframe) und das Message-Schema (Zod). Plugin kann via postMessage NIEMALS auf Host-Window-Properties zugreifen.
|
||||
|
||||
- **ZIP-Upload-Security:** Plugin-ZIP wird via `adm-zip` oder `yauzl` mit Limits geparsed (max 50 MiB komprimiert, max 200 MiB unkomprimiert — verhindert ZIP-Bombs). Nur `plugin.json`, `plugin.wasm`, `ui/**`, `migrations/**` Files werden extrahiert; alle anderen Files werden ignoriert (verhindert Path-Traversal). Path-Traversal-Detection (`../` in Entry-Namen) → Reject.
|
||||
|
||||
- **Permission-Guard:** Alle mutierenden API-Routes (Install, Enable, Disable, Uninstall, Consent, Settings-Schreibend, Secret-Setzen, Per-User-Toggle) prüfen `app.plugins.manage`. Lese-Routes prüfen mindestens `app.plugins.use`. Niemand außer Tenant-Admins sollte Plugins installieren können — auch nicht Tenant-User mit Custom-Rollen, außer explizit gewährt. Cross-Tenant-Calls werden auf API-Ebene durch Tenant-Scoped-Loader verhindert.
|
||||
|
||||
- **Secret-Storage:** Plugin-Secrets werden via Tenant-Encryption-Key encrypted (existierendes Pattern in der Boilerplate). Secret-Read-Capability hat `keyPattern`-Scope — Plugin kann nur Secrets lesen deren Key matcht. Secret-Write-API ist write-only (kein Read-Endpoint im Backend für GET-Secret); Tenant-Admin sieht nur Maske im UI.
|
||||
|
||||
- **Plugin-Lifecycle-Audit:** Jede Install/Enable/Disable/Uninstall-Aktion wird in das existierende Audit-Log geschrieben (NICHT das Plugin-spezifische `plugin_invocations`). Tenant-Admin kann nachvollziehen wann wer was getan hat.
|
||||
|
||||
- **Migration-Security:** SQL aus Plugin-Migrations wird nicht direkt durch `$executeRawUnsafe` ohne Inspection laufen gelassen — Parser validiert Statements gegen Allowlist (`CREATE TABLE`, `CREATE INDEX`, `ALTER TABLE ADD COLUMN`, `DROP TABLE`, `DROP INDEX`, `DROP COLUMN`). Verbotene Statements: `DROP DATABASE`, `GRANT`, `REVOKE`, `CREATE USER`, `COPY`, `pg_*`-Functions, jede Statement die nicht auf `plugin_<sanitized-id>_*` Tabellen target. Statements mit semicolon-injection-Verdacht (mehrere Statements in einer Datei nicht klar separiert) → Reject.
|
||||
|
||||
- **Plugin-Auto-Disable-DoS-Vector:** Ein Tenant-User mit Schreibrechten könnte versuchen, ein Plugin mit absichtlichen Fehler-Triggers (z.B. invalid Inputs) zu „bombardieren" um es auto-disabled zu kriegen. **Mitigation:** Crash-Counter zählt nur **Plugin-internal** Failures (Trap, Resource-Exceeded, Capability-Denied auf Plugin-Internal-Calls), NICHT „Plugin returnt Error-Output". Reine Output-Errors zählen separat und triggern nicht Auto-Disable. Falls dennoch der Verdacht aufkommt: Tenant-Admin sieht die Crash-Reasons im UI und kann re-enablen.
|
||||
|
||||
- **Update-Path-Security:** Plugin-Update läuft die SAME Signature-Verify wie Install. Wenn Publisher-Key zwischenzeitlich revoked wurde — Update wird abgelehnt, alte Version bleibt aktiv (oder wird abgeschaltet via Operator-Override).
|
||||
|
||||
- **iframe-CSRF:** Settings-API-Routes nutzen das existierende CSRF-Token-Pattern; postMessage aus iframe an Host muss den Token mitschicken — Host validiert.
|
||||
|
||||
## Subagent Recommendations
|
||||
|
||||
- **Planning:** `Plan`-Subagent mit dieser Detail-MD + `wasm-sandbox.md` als Inputs → produziert Subtask-Mapping auf die 17 Steps und schlägt Parallelisierungs-Gruppen vor. Insbesondere Step 13 (18 Hook-Wirings) kann von 3-4 Subagents parallel angegangen werden, einer pro Modul-Bereich (Tenant/User, Entity, Workflow/Webhook, Email/Auth/Billing).
|
||||
- **Research:** `Explore`-Subagent zur Inventur aller existierenden Hook-Stellen im Code: „Suche im Codebase nach allen Stellen wo Tenants/User/Entities/Workflows/Webhooks/Emails/Auth/Billing-Operations passieren. Liste pro Hook-Name aus der Tabelle den exakten file:line wo das Wiring rein soll." Output landet als Anhang in dieser Detail-MD oder als separate `WIRING-INVENTORY.md`.
|
||||
- **Implementation:**
|
||||
- 1 `general-purpose`-Subagent für Steps 1-3 (Hooks-Package — eng zusammenhängend).
|
||||
- 1 `general-purpose`-Subagent für Step 4 (DB-Schema-Migration — Prisma-Pattern).
|
||||
- 1 `general-purpose`-Subagent für Step 5 (Migration-Runner — komplex, isoliert).
|
||||
- 1 `general-purpose`-Subagent für Step 6 (Lifecycle-Service — komplex, isoliert).
|
||||
- 1 `general-purpose`-Subagent für Steps 7-8 (Routes — repetitiv, kann gebündelt werden).
|
||||
- 1 `general-purpose`-Subagent für Step 9 (Sandboxed-iframe — Security-kritisch, isoliert).
|
||||
- 1 `general-purpose`-Subagent für Steps 10-11 (Notification + Crash-Tracker).
|
||||
- 1 `general-purpose`-Subagent für Step 12 (Worker-Handler).
|
||||
- **3-4 parallele `general-purpose`-Subagents für Step 13** (Hook-Wiring).
|
||||
- 1 `general-purpose`-Subagent für Steps 14-15 (Sidebar + Permissions).
|
||||
- 1 `general-purpose`-Subagent für Step 16 (Doku).
|
||||
- **Tests:** Separater `general-purpose`-Subagent für Integration-Tests (Step 17 + die Integration-Tests aus Test Requirements). Playwright-E2E-Subagent separat.
|
||||
- **Verification:** `gsd-verifier`-Subagent über die Acceptance-Criteria.
|
||||
- **Code-Review:** `gsd-code-reviewer` insbesondere für Step 9 (iframe-Sandboxing — Security-kritisch) und Step 5 (SQL-Migration-Runner — Injection-Vektor).
|
||||
|
||||
## Staging-Deployment-Steps
|
||||
|
||||
1. Branch `agent/06-hooks-lifecycle` pushen.
|
||||
2. Coolify-Auto-Deploy auf Staging triggert. **WICHTIG:** Database-Migration läuft automatisch via Coolify-Pre-Deploy-Hook (oder manuell vor Deploy: `pnpm prisma migrate deploy`).
|
||||
3. Verifiziere dass `worker-plugin`-Container den `toolctrl.plugin-invoke` Job-Handler registriert hat:
|
||||
```bash
|
||||
ssh staging-host "docker logs worker-plugin --tail 50 | grep -i 'plugin-invoke'"
|
||||
```
|
||||
4. Tenant-Admin-Login auf Staging via `https://staging.toolctrl.de/login` mit Test-Account.
|
||||
5. Navigate zu `/app/test-tenant/plugins` → Liste leer (erwartet).
|
||||
6. Klick „Install Plugin" → Upload `hello-plugin-signed.zip` (aus `wasm-sandbox.md`-Staging-Steps).
|
||||
7. Capability-Consent-Dialog erscheint → alle Capabilities anhaken → „Approve".
|
||||
8. Plugin erscheint in Liste mit Status „Installed (Disabled)".
|
||||
9. Toggle „Enable" → Status wechselt zu „Enabled".
|
||||
10. Trigger Hook via Test-Endpoint (entweder real durch Tenant-Signup eines neuen Test-Tenants oder via temporärem `/api/internal/plugin-test/emit-hook` Endpoint):
|
||||
```bash
|
||||
curl -X POST https://staging.toolctrl.de/api/internal/plugin-test/emit-hook \
|
||||
-H "X-Internal-Test-Token: $TOKEN" \
|
||||
-d '{"hook":"onTenantSignup","payload":{"tenantId":"test-tenant","slug":"test","ownerEmail":"a@b.c","plan":"free","createdAt":"2026-05-16T00:00:00Z"}}'
|
||||
```
|
||||
11. Nach 2-3 Sek `/app/test-tenant/plugins/com.toolctrl.hello/audit` öffnen → Invocation-Entry sichtbar mit `outcome=success`, `durationMs<500`, `capabilityUsed=log,audit`.
|
||||
12. Plugin in Settings → setze ein Test-Setting → Verify postMessage funktioniert (Browser-DevTools-Console).
|
||||
13. Per-User-Toggle: User A enable, User B disable → trigger Hook für User B's Action → kein Invocation in Audit.
|
||||
14. Crash-Test: temporär `crash-plugin.zip` (Plugin das immer Trap'ed) installieren + enablen → 5× Hook triggern → erwartet: Status auf „Auto-Disabled", Notification-Bell zeigt Eintrag.
|
||||
15. Uninstall „crash-plugin" → bestätigen → verschwindet aus Liste, DB-Verify dass `plugin_com_toolctrl_crash_*`-Tabellen weg sind:
|
||||
```bash
|
||||
ssh staging-host "psql -d toolctrl -c '\dt plugin_*'"
|
||||
```
|
||||
16. Update-Path: Hello-Plugin v1.0.0 → upgrade zu v1.1.0 (mit zusätzlicher Migration) → Status bleibt enabled, Migration wird applied, alte Version in `superseded`.
|
||||
17. Coolify-Health-Check für neue Routes verifizieren (`/app/$tenant/plugins` returnt 200).
|
||||
18. Vor Production: temporäre `/api/internal/plugin-test/*` Endpoints entfernen (siehe `wasm-sandbox.md` Staging-Notes).
|
||||
|
||||
docker-compose-Snippet (Anhang `docs/operations/plugin-lifecycle.md`) bleibt identisch zum `wasm-sandbox.md`-Snippet plus zusätzlich:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
worker-plugin:
|
||||
environment:
|
||||
PLUGIN_AUTO_DISABLE_THRESHOLD: 5
|
||||
PLUGIN_AUTO_DISABLE_WINDOW_MS: 600000
|
||||
PLUGIN_GLOBAL_AUTO_DISABLE_TENANTS: 3
|
||||
PLUGIN_GLOBAL_AUTO_DISABLE_WINDOW_MS: 1800000
|
||||
PLUGIN_HOOKS_ENABLED: "true"
|
||||
```
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
- **Hook count scoped to 6 (not 18).** Foundation pass wires the highest-value hook points (`tenant.created`, `user.created`, `entity.created`, `entity.updated`, `entity.deleted`, `workflow.executed`). The remaining hook points stay in the plan but are deferred so the foundation can ship incrementally instead of the original 17-step roadmap.
|
||||
- **No separate `packages/plugin-hooks` package.** The registry + emit helper live in `packages/plugin-runtime/src/hooks/` to avoid an extra workspace package; the boundary stays clean because the helper accepts an injected publisher function.
|
||||
- **Producer emits one `plugin.hook.<name>` outbox event, not one per subscriber.** Subscriber fan-out now runs on the general worker via `outbox.plugin-hook` and enqueues idempotent `toolctrl.plugin-invoke` jobs on the isolated plugin queue. Keeps the producer cheap and avoids hitting the Plugin table inside hot request paths.
|
||||
- **Capability consent shipped for the current admin surface.** Tenant settings
|
||||
ships a list-level approval path for missing manifest-declared grants with
|
||||
explicit review confirmation and stale-manifest digest validation. The fuller
|
||||
guided marketplace install dialog remains deferred. The Sandbox already
|
||||
enforces capabilities at runtime via #21's gateway.
|
||||
- **`emitHookSync` / output-hooks deferred.** No `transform` / `cancel` capability shipped in this pass — only fire-and-forget hooks. Output hooks land alongside the chatbot capability work in Phase 07.
|
||||
- **Per-user enablement model shipped, gating not wired.** `PluginUserConfig` table + migration exist; the producer side that filters hooks by user opt-in is a follow-up commit once user identity is plumbed through `RowsApi`.
|
||||
- **Settings sandboxed iframe shipped for settings UI.** `PluginSettingsFrame`
|
||||
provides the sandbox/CSP/postMessage boundary for tenant plugin settings.
|
||||
Marketplace packaging and broader UI-slot coverage remain follow-ups.
|
||||
- **Global cross-tenant crash counter deferred.** Crash tracker only enforces per-tenant auto-disable for the foundation. Operator-level alerting via PagerDuty lands with #23.
|
||||
|
||||
## Progress Log
|
||||
|
||||
- 2026-05-17 22:33 — `72ccf4d9` — `06/plugin-hooks: add hook registry with zod payload schemas`
|
||||
- 2026-05-17 22:42 — `aacdc600` — `06/plugin-hooks: instrument tenant.created hook in tenant create flow`
|
||||
- 2026-05-17 22:45 — `c1831cc6` — `06/plugin-hooks: instrument user.created hook in user register`
|
||||
- 2026-05-17 22:48 — `2a47391f` — `06/plugin-hooks: instrument entity.created/updated/deleted hooks`
|
||||
- 2026-05-17 22:51 — `d1d1a036` — `06/plugin-hooks: add plugin lifecycle service and tenant API routes`
|
||||
- 2026-05-17 22:53 — `9b5f9eaf` — `06/plugin-hooks: add plugin db migration applier with prefix validation`
|
||||
- 2026-05-17 22:55 — `3ce43c20` — `06/plugin-hooks: add tenant-admin plugins settings ui`
|
||||
- 2026-05-17 22:58 — `ce9e12c0` — `06/plugin-hooks: add crash tracker with auto-disable`
|
||||
- 2026-05-17 22:59 — `e0f60033` — `06/plugin-hooks: add PluginUserConfig prisma model and migration`
|
||||
- 2026-05-17 23:02 — `9fc8fb57` — `06/plugin-hooks: add cli plugin lifecycle commands`
|
||||
- 2026-05-17 23:03 — `8901218d` — `06/plugin-hooks: document hooks lifecycle architecture`
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
(Optional, am Ende des Tasks — insbesondere: welche Hook-Points haben sich als hinderlich erwiesen, welche Capability-Granularität war zu fein/grob, welche Lifecycle-Edge-Cases wurden gefunden, Findings aus dem Sandboxed-iframe-Setup, Migrations-Performance-Beobachtungen.)
|
||||
@@ -0,0 +1,80 @@
|
||||
# Phase 06 — Security Findings
|
||||
|
||||
This directory holds per-finding remediation detail documents produced after the **internal self-audit** (`docs/security/plugin-internal-findings.md`) and after the **external pen-test** (Cure53 / SySS / secfault / etc., per `docs/security/plugin-audit-rfp.md`).
|
||||
|
||||
## Folder structure
|
||||
|
||||
```
|
||||
security-findings/
|
||||
├── README.md # this file
|
||||
├── F-001-capability-consent.md # one file per Critical/High finding
|
||||
├── F-002-per-instruction-fuel.md
|
||||
├── F-003-audit-hash-chain.md
|
||||
├── ...
|
||||
└── external/
|
||||
└── 2026-MM-DD-<firm>-F-<id>.md # external-audit findings
|
||||
```
|
||||
|
||||
## When to create a file here
|
||||
|
||||
Create a per-finding file for **every Critical and High** finding from either source. Medium / Low findings can be tracked as one-line entries in the parent inventory (`docs/security/plugin-internal-findings.md` or the external sanitised report) until they are scheduled for remediation, at which point they get promoted to a file here.
|
||||
|
||||
A finding file must contain:
|
||||
|
||||
1. **ID + severity** — `F-NNN` (internal) or `<firm>-<NNN>` (external). Severity = Critical/High/Medium/Low + CVSS or DREAD if the auditor provided.
|
||||
2. **Asset** — link to the matching `A-NN` in `docs/security/plugin-threat-model.md`.
|
||||
3. **Description** — what's wrong, what an attacker can do, business impact.
|
||||
4. **Reproduction** — minimal repro steps the auditor / engineer can follow.
|
||||
5. **Remediation plan** — design + concrete file/line targets.
|
||||
6. **Status** — `open`, `in-progress`, `remediated`, `accepted-residual` (with operator sign-off), or `wont-fix` (with explicit justification + acceptance).
|
||||
7. **Remediation commits** — list of SHAs once landed.
|
||||
8. **Re-test result** — auditor's re-test verdict for external findings, or the responsible engineer's signed-off internal review for internal findings.
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
# F-NNN: <short title>
|
||||
|
||||
**Severity:** Critical | High | Medium | Low
|
||||
**Asset:** [A-NN](../../../security/plugin-threat-model.md#1-asset-catalogue)
|
||||
**Source:** internal self-audit (2026-MM-DD) | <firm> <date>
|
||||
**Status:** open | in-progress | remediated | accepted-residual | wont-fix
|
||||
|
||||
## Description
|
||||
|
||||
…
|
||||
|
||||
## Reproduction
|
||||
|
||||
…
|
||||
|
||||
## Remediation plan
|
||||
|
||||
…
|
||||
|
||||
## Remediation commits
|
||||
|
||||
| SHA | Summary |
|
||||
|-----|---------|
|
||||
|
||||
## Re-test result
|
||||
|
||||
…
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
1. **Open.** Finding is recorded here as a stub with description + reproduction.
|
||||
2. **In-progress.** A PR or commit chain is targeting it; the file lists the WIP branch / commits.
|
||||
3. **Remediated.** All commits landed on `main`; re-test scheduled.
|
||||
4. **Re-tested.** Auditor (external) or self-audit (internal) confirms the gap is closed. Status updated to `remediated`.
|
||||
5. **Sign-off** (Critical/High only). The operator and lead auditor co-sign `docs/security/plugin-runtime-signoff.md` — only after **all** Critical and High items reach `remediated` (or `accepted-residual` with co-signature).
|
||||
|
||||
## Cross-references
|
||||
|
||||
- `docs/security/plugin-threat-model.md` — the threat-model asset catalogue (`A-NN`).
|
||||
- `docs/security/plugin-attack-surface.md` — the inventory of host bindings / DB tables / routes.
|
||||
- `docs/security/plugin-internal-findings.md` — the running list of internal-self-audit findings.
|
||||
- `docs/security/plugin-audit-rfp.md` — the external-audit scope of work.
|
||||
- `docs/security/plugin-runtime-signoff.md` — the joint sign-off the production rollout gate.
|
||||
- `docs/improvement-plan/BLOCKERS.md` § B-006 — the operator-procurement blocker for the external audit.
|
||||
@@ -0,0 +1,538 @@
|
||||
# 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`)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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/`:
|
||||
```yaml
|
||||
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):
|
||||
```bash
|
||||
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`):
|
||||
```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`):
|
||||
```bash
|
||||
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):
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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`):
|
||||
```bash
|
||||
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.)
|
||||
Reference in new issue
Block a user