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

49 KiB
Raw Blame History

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:

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:

{
  "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:
    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):
    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:
    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:

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.)