Files
HiveGrid-Wireframes/hivegrid-cloud/prisma/schema/plugins.prisma
T

169 lines
6.8 KiB
Plaintext

// 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")
}