// Phase 06 #21 — Plugin runtime metadata. // // One row per installed plugin per tenant. The .wasm binary lives on // disk (managed by the operator and verified via wasmSha256); this // table only stores the metadata the worker needs to look up + verify // + invoke a plugin: which version is installed, who signed it, what // capabilities the manifest declared, and whether it is currently // enabled. // // Hot-reload and side-by-side version updates are intentionally // deferred. Per-tenant capability grants are modeled by // TenantPluginCapabilityGrant below, and hook subscriptions are declared in // the signed plugin manifest. model Plugin { id String @id @default(cuid()) tenantId String pluginId String version String manifest Json /// Hex SHA-256 of the .wasm binary at install time. Worker re-hashes /// on load and refuses to invoke when this does not match. wasmSha256 String /// Ed25519 public key (hex) recorded from the manifest at install. signaturePublicKey String enabled Boolean @default(false) installedAt DateTime @default(now()) updatedAt DateTime @updatedAt userConfigs PluginUserConfig[] capabilityGrants TenantPluginCapabilityGrant[] @@unique([tenantId, pluginId]) @@index([tenantId, enabled]) @@index([pluginId, version]) } // Phase 06 / F-001 — explicit per-tenant capability consent. // // Phase 22 originally deferred the consent dialog and let every // manifest-declared capability run implicitly. F-001 closes that gap: // installs no longer auto-enable; the lifecycle service requires a // matching `TenantPluginCapabilityGrant` row for every capability the // manifest declares before the Plugin row can transition to // `enabled = true`. The UI surface that drives the operator-facing // dialog is intentionally NOT shipped in this commit (separate task); // the backend stays usable from CLI / API today. // // One row per (tenantId, pluginId, capabilityName). `scopeJson` carries // the per-capability scope the operator approved (e.g. allowed-hosts // list for http.outbound) — when null, the manifest's declared scope // applies as-is. model TenantPluginCapabilityGrant { id String @id @default(cuid()) tenantId String pluginId String capabilityName String scopeJson Json? grantedAt DateTime @default(now()) /// User id of the operator who approved the grant; null for installs /// approved via the CLI / automation path. grantedByUserId String? plugin Plugin @relation(fields: [pluginId, tenantId], references: [pluginId, tenantId], onDelete: Cascade) @@unique([tenantId, pluginId, capabilityName]) @@index([tenantId, pluginId]) } // Phase 06 #22 — per-user plugin enablement + config blob. // // Default: an installed + enabled Plugin row applies to every user in // the tenant. When a plugin needs per-user opt-in (e.g. a Slack // notifier that asks the user for their own channel) the host writes // a PluginUserConfig row and the hook producer respects `enabled` // before fanning out the invocation. // // `configJson` is a free-form per-user settings blob (Slack channel, // preferred locale, etc.). Plugins read it through the plugin sandbox // host-binding once that capability lands; for the foundation only the // host UI writes it. model PluginUserConfig { id String @id @default(cuid()) pluginId String userId String enabled Boolean @default(true) configJson Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt plugin Plugin @relation(fields: [pluginId], references: [id], onDelete: Cascade) @@unique([pluginId, userId]) @@index([userId]) } // Phase 06 #23 — durable per-invocation audit row. // // One row per `toolctrl.plugin-invoke` job outcome. Capability-use rows // reference the same `pluginInvocationId` so operators can correlate the // high-level invocation outcome with the individual host capability // calls emitted during sandbox execution. The row intentionally stores // outcome metadata only, not raw plugin output or input payloads. // No FK is declared yet so pre-existing capability-use rows and retry // batches remain valid; correlate records by this stable invocation id. model PluginInvocation { id String @id tenantId String pluginId String version String hookName String outcome String failureReason String? message String? durationMs Int fuelUsed Int? createdAt DateTime @default(now()) @@index([tenantId, createdAt(sort: Desc)]) @@index([tenantId, pluginId, createdAt(sort: Desc)]) @@index([outcome, createdAt(sort: Desc)]) } // Phase 06 #23 — per-capability-use audit row. // // Every successful (or denied / errored) host-fn call inside a plugin // invocation appends a row here so a tenant admin can answer "what did // this plugin actually DO?" — independent of what the manifest said it // might do. The data is intentionally minimal: capability name, a // hashed pointer to the payload (no raw body — DSGVO + storage bloat), // outcome, and a timestamp. Retention is governed by // `PLUGIN_AUDIT_RETENTION_DAYS` (cron-cleaned by the worker). // // Rows are batch-written from the capability gateway via the outbox // pattern — the gateway buffers per-invocation events and emits one // `plugin.capability-use.batch` outbox event at sandbox close so we // never block the plugin-hot-path on the DB. model PluginCapabilityUse { id String @id @default(cuid()) /// PluginInvocation row id. No FK yet; rows are correlated by value so /// existing capability-use batches remain migration-safe. pluginInvocationId String tenantId String pluginId String capability String /// SHA-256 of the host-fn arg-shape (URL+method for http.outbound, /// key for storage.*, etc.) — lets forensics cross-check without /// retaining tenant payload bodies. payloadHash String? outcome String errorCode String? durationMs Int? timestamp DateTime @default(now()) /// F-003 — per-tenant hash chain. `prevHash` is the `rowHash` of the /// preceding row in this tenant's chain (or NULL for the genesis /// row). `rowHash` is SHA-256(prevHash || canonical(row)). A verifier /// can walk the chain and detect tampering: any modified or deleted /// row breaks the chain at that point. Nullable for back-compat with /// pre-F-003 rows; new inserts always populate both fields. prevHash String? rowHash String? @@index([tenantId, timestamp(sort: Desc)]) @@index([tenantId, pluginId, timestamp(sort: Desc)]) @@index([pluginInvocationId]) @@index([timestamp], name: "plugin_capability_use_timestamp_idx") }